├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .php_cs
├── .scrutinizer.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── composer.json
├── doc
├── Makefile
├── _theme
│ └── sphinx_rtd_theme
│ │ ├── __init__.py
│ │ ├── breadcrumbs.html
│ │ ├── footer.html
│ │ ├── layout.html
│ │ ├── layout_old.html
│ │ ├── search.html
│ │ ├── searchbox.html
│ │ ├── static
│ │ ├── css
│ │ │ ├── badge_only.css
│ │ │ └── theme.css
│ │ ├── fonts
│ │ │ ├── FontAwesome.otf
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.svg
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ └── fontawesome-webfont.woff
│ │ └── js
│ │ │ └── theme.js
│ │ ├── theme.conf
│ │ └── versions.html
├── api.rst
├── conf.py
├── index.rst
├── mistakes.rst
├── symfony.rst
├── tags
│ ├── attr-append.rst
│ ├── attr.rst
│ ├── autoescape.rst
│ ├── block.rst
│ ├── capture.rst
│ ├── content.rst
│ ├── embed.rst
│ ├── extends.rst
│ ├── filter.rst
│ ├── for.rst
│ ├── if.rst
│ ├── import.rst
│ ├── include.rst
│ ├── index.rst
│ ├── macro.rst
│ ├── omit.rst
│ ├── replace.rst
│ ├── sandbox.rst
│ ├── set.rst
│ ├── spaceless.rst
│ └── use.rst
└── templates.rst
├── phpunit.xml.dist
├── src
├── Attribute.php
├── Attribute
│ ├── AttrAppendAttribute.php
│ ├── AttrAttribute.php
│ ├── BaseAttribute.php
│ ├── BlockInnerAttribute.php
│ ├── BlockOuterAttribute.php
│ ├── CaptureAttribute.php
│ ├── ContentAttribute.php
│ ├── ElseAttribute.php
│ ├── ElseIfAttribute.php
│ ├── ExtendsAttribute.php
│ ├── IfAttribute.php
│ ├── InternalIDAttribute.php
│ ├── OmitAttribute.php
│ ├── ReplaceAttribute.php
│ └── SetAttribute.php
├── Compiler.php
├── EventDispatcher
│ ├── AbstractEvent.php
│ ├── CompilerEvents.php
│ ├── SourceEvent.php
│ └── TemplateEvent.php
├── EventSubscriber
│ ├── AbstractTwigExpressionSubscriber.php
│ ├── ContextAwareEscapingSubscriber.php
│ ├── CustomNamespaceRawSubscriber.php
│ ├── CustomNamespaceSubscriber.php
│ ├── DOMMessSubscriber.php
│ ├── FixHtmlEntitiesInExpressionSubscriber.php
│ ├── FixTwigExpressionSubscriber.php
│ ├── IDNodeSubscriber.php
│ └── ReplaceDoctypeAsTwigExpressionSubscriber.php
├── Exception.php
├── Extension.php
├── Extension
│ ├── AbstractExtension.php
│ ├── CoreExtension.php
│ └── FullCompatibilityTwigExtension.php
├── Helper
│ ├── DOMHelper.php
│ └── ParserHelper.php
├── Node.php
├── Node
│ ├── BlockNode.php
│ ├── EmbedNode.php
│ ├── ExtendsNode.php
│ ├── ImportNode.php
│ ├── IncludeNode.php
│ ├── MacroNode.php
│ ├── OmitNode.php
│ └── UseNode.php
├── SourceAdapter.php
├── SourceAdapter
│ ├── HTML5Adapter.php
│ ├── XHTMLAdapter.php
│ └── XMLAdapter.php
├── Template.php
├── Twital.php
├── TwitalLoader.php
├── TwitalLoaderTrait.php
├── TwitalLoaderTwigGte3.php
└── TwitalLoaderTwigLt3.php
└── tests
├── Tests
├── ContextAwareEscapingTest.php
├── CoreAttributeTest.php
├── CoreNodesTest.php
├── DynamicAttrAttributeTest.php
├── Event
│ ├── SourceEventTest.php
│ └── TemplateEventTest.php
├── ExpressionParserTest.php
├── FullCompatibilityTwigTest.php
├── Html5CoreNodesTest.php
├── Html5DynamicAttrAttributeTest.php
├── TwitalLoaderTest.php
├── XhtmlCoreNodesTest.php
├── XmlCoreNodesTest.php
└── templates
│ ├── base-01.twig
│ ├── base-01.xml
│ ├── doctype-01.html.twig
│ ├── doctype-01.twig
│ ├── doctype-01.xml
│ ├── doctype-02.htm
│ ├── doctype-02.twig
│ ├── embed-01.twig
│ ├── embed-01.xml
│ ├── embed-02.twig
│ ├── embed-02.xml
│ ├── empty.twig
│ ├── empty.xml
│ ├── extends-00.twig
│ ├── extends-00.xml
│ ├── extends-01.twig
│ ├── extends-01.xml
│ ├── extends-02.twig
│ ├── extends-02.xml
│ ├── extends-03.twig
│ ├── extends-03.xml
│ ├── extends-as-attributes-inner.twig
│ ├── extends-as-attributes-inner.xml
│ ├── extends-as-attributes-use.twig
│ ├── extends-as-attributes-use.xml
│ ├── extends-as-attributes.twig
│ ├── extends-as-attributes.xml
│ ├── logger.html.twig
│ ├── macro-01.html.twig
│ ├── macro-01.twig
│ ├── macro-01.xml
│ ├── use-01.twig
│ ├── use-01.xml
│ ├── use-02.twig
│ ├── use-02.xml
│ ├── web_profiler_js.html.twig
│ ├── widget-header.twig
│ ├── xmldeclaration-01.html.twig
│ ├── xmldeclaration-01.twig
│ └── xmldeclaration-01.xml
└── bootstrap.php
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push: ~
5 | pull_request: ~
6 |
7 | jobs:
8 | phpunit:
9 | name: PHPUnit on ${{ matrix.php }} and Twig ${{ matrix.twig }} ${{ matrix.dependencies }}
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | dependencies: ['', 'lowest']
14 | php: [ '7.1', '7.2', '7.3', '7.4', '8.0', '8.1' ]
15 | twig: [ ^1.0, ^2.0, ^3.0 ]
16 | exclude:
17 | - php: '7.1'
18 | twig: ^3.0
19 | steps:
20 | - name: Update code
21 | uses: actions/checkout@v2
22 |
23 | - name: Setup PHP
24 | uses: shivammathur/setup-php@v2
25 | with:
26 | php-version: ${{ matrix.php }}
27 | extensions: dom
28 | coverage: pcov
29 | tools: composer:v2
30 |
31 | - name: Install dependencies
32 | run: composer update --no-progress --with "twig/twig:${{ matrix.twig }}"
33 |
34 | - name: Install lowest dependencies
35 | run: composer update --no-progress --prefer-lowest --root-reqs --with "twig/twig:${{ matrix.twig }}"
36 | if: matrix.dependencies
37 |
38 | - name: Run tests
39 | run: vendor/bin/phpunit --no-coverage
40 | if: matrix.php != '8.0'
41 |
42 | - name: Run tests with code coverage
43 | run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
44 | if: matrix.php == '8.0'
45 |
46 | - name: Upload code coverage tu Scrutinizer
47 | run: php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover
48 | if: matrix.php == '8.0'
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.settings
2 | /.buildpath
3 | /.project
4 | /.php_cs.cache
5 | /vendor
6 | .idea
7 | composer.lock
8 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | in(__DIR__)
5 | ;
6 |
7 | if (PHP_MAJOR_VERSION < 7) {
8 | $finder->notName('TwitalLoaderTwigGte3.php');
9 | }
10 |
11 | return PhpCsFixer\Config::create()
12 | ->setRules(array(
13 | '@PSR2' => true,
14 | 'array_syntax' => array('syntax' => 'long'),
15 | 'no_unused_imports' => true,
16 | 'ordered_imports' => true,
17 | 'ordered_class_elements' => true,
18 | ))
19 | ->setFinder($finder)
20 | ;
21 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | tools:
2 | php_mess_detector: true
3 | php_analyzer:
4 | config:
5 | parameter_reference_check: { enabled: false }
6 | checkstyle: { enabled: false, no_trailing_whitespace: true, naming: { enabled: true, local_variable: '^[a-z][a-zA-Z0-9]*$', abstract_class_name: ^Abstract|Factory$, utility_class_name: 'Utils?$', constant_name: '^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$', property_name: '^[a-z][a-zA-Z0-9]*$', method_name: '^(?:[a-z]|__)[a-zA-Z0-9]*$', parameter_name: '^[a-z][a-zA-Z0-9]*$', interface_name: '^[A-Z][a-zA-Z0-9]*Interface$', type_name: '^[A-Z][a-zA-Z0-9]*$', exception_name: '^[A-Z][a-zA-Z0-9]*Exception$', isser_method_name: '^(?:is|has|should|may|supports)' } }
7 | unreachable_code: { enabled: false }
8 | check_access_control: { enabled: false }
9 | typo_checks: { enabled: false }
10 | check_variables: { enabled: false }
11 | check_calls: { enabled: true, too_many_arguments: true, missing_argument: true, argument_type_checks: lenient }
12 | suspicious_code: { enabled: false, overriding_parameter: false, overriding_closure_use: false, parameter_closure_use_conflict: false, parameter_multiple_times: false, non_existent_class_in_instanceof_check: false, non_existent_class_in_catch_clause: false, assignment_of_null_return: false, non_commented_switch_fallthrough: false, non_commented_empty_catch_block: false, overriding_private_members: false, use_statement_alias_conflict: false, precedence_in_condition_assignment: false }
13 | dead_assignments: { enabled: false }
14 | verify_php_doc_comments: { enabled: false, parameters: false, return: false, suggest_more_specific_types: false, ask_for_return_if_not_inferrable: false, ask_for_param_type_annotation: false }
15 | loops_must_use_braces: { enabled: false }
16 | check_usage_context: { enabled: true, foreach: { value_as_reference: true, traversable: true } }
17 | simplify_boolean_return: { enabled: false }
18 | phpunit_checks: { enabled: false }
19 | reflection_checks: { enabled: false }
20 | precedence_checks: { enabled: true, assignment_in_condition: true, comparison_of_bit_result: true }
21 | basic_semantic_checks: { enabled: false }
22 | unused_code: { enabled: false }
23 | deprecation_checks: { enabled: false }
24 | useless_function_calls: { enabled: false }
25 | metrics_lack_of_cohesion_methods: { enabled: false }
26 | metrics_coupling: { enabled: true, stable_code: { namespace_prefixes: { }, classes: { } } }
27 | doctrine_parameter_binding: { enabled: false }
28 | doctrine_entity_manager_injection: { enabled: false }
29 | symfony_request_injection: { enabled: false }
30 | doc_comment_fixes: { enabled: false }
31 | reflection_fixes: { enabled: false }
32 | use_statement_fixes: { enabled: true, remove_unused: true, preserve_multiple: false, preserve_blanklines: false, order_alphabetically: false }
33 | php_code_sniffer: true
34 | sensiolabs_security_checker: true
35 | php_cpd: true
36 | php_loc: true
37 | php_pdepend: true
38 | external_code_coverage: true
39 | filter:
40 | paths:
41 | - src/*
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute to Twital
2 |
3 | Thank you for contributing to Twital!
4 |
5 | Before we can merge your Pull-Request here are some guidelines that you need to follow. These guidelines exist not to annoy you, but to keep the code base clean, unified and future proof.
6 |
7 | ## We only accept PRs to "master"
8 |
9 | Our branching strategy is "everything to master first", even bugfixes and we then merge
10 | them into the stable branches. You should only open pull requests against the master branch.
11 | Otherwise we cannot accept the PR.
12 |
13 |
14 | ## Coding Standard
15 |
16 | We use PSR-1 and PSR-2:
17 |
18 | * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md
19 | * https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
20 |
21 |
22 | ## Unit-Tests
23 |
24 | Please try to add a test for your pull-request.
25 |
26 | You can run the unit-tests by calling ``phpunit`` from the root of the project.
27 |
28 | ## Travis
29 |
30 | We automatically run your pull request through [Travis CI](http://www.travis-ci.org).
31 | If you break the tests, we cannot merge your code,
32 | so please make sure that your code is working before opening up a Pull-Request.
33 |
34 | ## Issues and Bugs
35 |
36 | To create a new issue, you can use the GitHub issue tracking system.
37 | Please, try to distinguish between Twig and Twital issues.
38 |
39 | ## Getting merged
40 |
41 | Please allow us time to review your pull requests.
42 | We will give our best to review everything as fast as possible,
43 | but cannot always live up to our own expectations.
44 |
45 | Thank you very much again for your contribution!
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Asmir Mustafic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/goetas/twital)
2 | [](https://scrutinizer-ci.com/g/goetas/twital/)
3 | [](https://scrutinizer-ci.com/g/goetas/twital/)
4 | [](https://raw.githubusercontent.com/goetas/twital/master/LICENSE)
5 | [](https://packagist.org/packages/goetas/twital)
6 |
7 | What is Twital?
8 | ==============
9 |
10 | Twital is a template engine built on top of [Twig](http://twig.sensiolabs.org/)
11 | (a template engine for PHP and default template engine on Symfony)
12 | that adds some shortcuts and makes Twig's syntax more suitable for HTML based (XML, HTML5, XHTML, SGML) templates.
13 | Twital takes inspiration from [PHPTal](http://phptal.org/), [TAL](http://en.wikipedia.org/wiki/Template_Attribute_Language)
14 | and [AngularJS](http://angularjs.org/) or [Vue.js](https://vuejs.org/) (just for some aspects),
15 | mixing their language syntaxes with the powerful Twig templating engine system.
16 |
17 | Twital is fully compatible with Twig, all Twig templates can be rendered using Twital.
18 |
19 | To better understand the Twital's benefits, consider the following **Twital** template, which
20 | simply shows a list of users from an array:
21 |
22 | ```xml
23 |
24 |
25 | {{ user.name }}
26 |
27 |
28 | ```
29 |
30 | To do the same thing using Twig, you need:
31 |
32 | ```jinja
33 | {% if users %}
34 |
35 | {% for user in users %}
36 |
37 | {{ user.name }}
38 |
39 | {% endfor %}
40 |
41 | {% endif %}
42 | ```
43 |
44 | As you can see, the Twital template is **more readable**, **less verbose** and
45 | and **you don't have to worry about opening and closing block instructions**
46 | (they are inherited from the HTML structure).
47 |
48 |
49 | One of the main advantages of Twital is the *implicit* presence of control statements, which makes
50 | templates more readable and less verbose. Furthermore, it has all Twig functionalities,
51 | such as template inheritance, translations, looping, filtering, escaping, etc.
52 |
53 | If some Twig functionality is not directly available for Twital,
54 | you can **freely mix Twig and Twital** syntaxes.
55 |
56 | In the example below, we have mixed Twital and Twig syntaxes to use Twig custom tags:
57 |
58 | ```xml
59 |
60 | {% custom_tag %}
61 | {{ someUnsafeVariable }}
62 | {% endcustom_tag %}
63 |
64 | ```
65 |
66 | When needed, you can extend from a Twig template:
67 |
68 | ```xml
69 |
70 |
71 |
72 | Hello {{name}}!
73 |
74 |
75 |
76 | ```
77 |
78 | You can also extend from Twig a Twital template:
79 | ```jinja
80 | {% extends "layout.twital" %}
81 |
82 | {% block content %}
83 | Hello {{name}}!
84 | {% endblock %}
85 |
86 |
87 | ```
88 |
89 | A presentation of Twital features and advantages is available on [this presentation](https://goetas.bitbucket.io/twital-02-08-2016-berlin-ug/#/).
90 |
91 |
92 | Installation
93 | ------------
94 |
95 | The recommended ways install Twital is via [Composer](https://getcomposer.org/).
96 |
97 |
98 | ```bash
99 | composer require goetas/twital
100 | ```
101 |
102 | Documentation
103 | -------------
104 |
105 | Go here http://twital.readthedocs.org/ to read a more detailed documentation about Twital.
106 |
107 |
108 | Getting started
109 | ---------------
110 |
111 | First, you have to create a file that contains your template
112 | (named for example `demo.twital.html`):
113 |
114 | ```xml
115 |
116 | Hello {{ name }}
117 |
118 | ```
119 |
120 | Afterwards, you have to create a PHP script that instantiate the required objects:
121 |
122 | ```php
123 | require_once '/path/to/composer/vendor/autoload.php';
124 | use Goetas\Twital\TwitalLoader;
125 |
126 | $fileLoader = new Twig_Loader_Filesystem('/path/to/templates');
127 | $twitalLoader = new TwitalLoader($fileLoader);
128 |
129 | $twig = new Twig_Environment($twitalLoader);
130 | echo $twig->render('demo.twital.html', array('name' => 'John'));
131 | ```
132 |
133 | That's it!
134 |
135 |
136 | Symfony Users
137 | --------------
138 |
139 | If you are a [Symfony](http://symfony.com/) user, you can add Twital to your project using the
140 | [TwitalBundle](https://github.com/goetas/twital-bundle).
141 |
142 | The bundle integrates all most common functionalities as Assetic, Forms, Translations, Routing, etc.
143 |
144 | Twig Users
145 | ----------
146 |
147 | Starting from version Twital 1.0.0, both twig 1.x and 2.x versions are supported.
148 |
149 |
150 | ## Note
151 |
152 | The code in this project is provided under the
153 | [MIT](https://opensource.org/licenses/MIT) license.
154 | For professional support
155 | contact [goetas@gmail.com](mailto:goetas@gmail.com)
156 | or visit [https://www.goetas.com](https://www.goetas.com)
157 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "goetas/twital",
3 | "description" : "PHP templating engine that combines Twig and PHPTal power points",
4 | "authors" : [{
5 | "name" : "Asmir Mustafic",
6 | "email" : "goetas@gmail.com"
7 | }
8 | ],
9 | "keywords" : [
10 | "php",
11 | "xml",
12 | "templating",
13 | "twig"
14 | ],
15 | "homepage" : "https://github.com/goetas/twital",
16 | "license" : "MIT",
17 | "require" : {
18 | "php" : "^7.1|^8.0",
19 | "ext-dom": "*",
20 | "masterminds/html5" : "^2.1.2",
21 | "twig/twig" : "^1.43|^2.13|^3.0",
22 | "symfony/event-dispatcher" : "^3.4|^4.4|^5.1|^6.0"
23 | },
24 | "require-dev" : {
25 | "phpunit/phpunit" : "^7.5|^8.0|^9.0",
26 | "friendsofphp/php-cs-fixer": "^2.19",
27 | "scrutinizer/ocular": "^1.3",
28 | "symfony/var-dumper": "^3.4|^4.4|^5.1|^6.0"
29 | },
30 | "autoload" : {
31 | "psr-4" : {
32 | "Goetas\\Twital\\" : "src/"
33 | }
34 | },
35 | "autoload-dev" : {
36 | "psr-4" : {
37 | "Goetas\\Twital\\Tests\\" : "tests/Tests"
38 | }
39 | },
40 | "extra" : {
41 | "branch-alias" : {
42 | "dev-master" : "1.x-dev"
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/__init__.py:
--------------------------------------------------------------------------------
1 | """Sphinx ReadTheDocs theme.
2 |
3 | From https://github.com/ryan-roemer/sphinx-bootstrap-theme.
4 |
5 | """
6 | import os
7 |
8 | VERSION = (0, 1, 5)
9 |
10 | __version__ = ".".join(str(v) for v in VERSION)
11 | __version_full__ = __version__
12 |
13 |
14 | def get_html_theme_path():
15 | """Return list of HTML theme paths."""
16 | cur_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
17 | return cur_dir
18 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/breadcrumbs.html:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/footer.html:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/layout.html:
--------------------------------------------------------------------------------
1 | {# TEMPLATE VAR SETTINGS #}
2 | {%- set url_root = pathto('', 1) %}
3 | {%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
4 | {%- if not embedded and docstitle %}
5 | {%- set titlesuffix = " — "|safe + docstitle|e %}
6 | {%- else %}
7 | {%- set titlesuffix = "" %}
8 | {%- endif %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {% block htmltitle %}
17 | {{ title|striptags|e }}{{ titlesuffix }}
18 | {% endblock %}
19 |
20 | {# FAVICON #}
21 | {% if favicon %}
22 |
23 | {% endif %}
24 |
25 | {# CSS #}
26 |
27 |
28 | {# JS #}
29 | {% if not embedded %}
30 |
31 |
40 | {%- for scriptfile in script_files %}
41 |
42 | {%- endfor %}
43 |
44 | {% if use_opensearch %}
45 |
46 | {% endif %}
47 |
48 | {% endif %}
49 |
50 | {# RTD hosts these file themselves, so just load on non RTD builds #}
51 | {% if not READTHEDOCS %}
52 |
53 |
54 | {% endif %}
55 |
56 | {# STICKY NAVIGATION #}
57 | {% if theme_sticky_navigation %}
58 |
63 | {% endif %}
64 |
65 | {% for cssfile in css_files %}
66 |
67 | {% endfor %}
68 |
69 | {%- block linktags %}
70 | {%- if hasdoc('about') %}
71 |
73 | {%- endif %}
74 | {%- if hasdoc('genindex') %}
75 |
77 | {%- endif %}
78 | {%- if hasdoc('search') %}
79 |
80 | {%- endif %}
81 | {%- if hasdoc('copyright') %}
82 |
83 | {%- endif %}
84 |
85 | {%- if parents %}
86 |
87 | {%- endif %}
88 | {%- if next %}
89 |
90 | {%- endif %}
91 | {%- if prev %}
92 |
93 | {%- endif %}
94 | {%- endblock %}
95 | {%- block extrahead %} {% endblock %}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {# SIDE NAV, TOGGLES ON MOBILE #}
106 |
107 |
111 |
112 |
121 |
122 |
123 |
124 |
125 |
126 | {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
127 |
128 |
129 | {{ project }}
130 |
131 |
132 |
133 | {# PAGE CONTENT #}
134 |
135 |
136 | {% include "breadcrumbs.html" %}
137 |
138 | {% block body %}{% endblock %}
139 |
140 | {% include "footer.html" %}
141 |
142 |
143 |
144 |
145 |
146 |
147 | {% include "versions.html" %}
148 |
149 |
150 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/search.html:
--------------------------------------------------------------------------------
1 | {#
2 | basic/search.html
3 | ~~~~~~~~~~~~~~~~~
4 |
5 | Template for the search page.
6 |
7 | :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS.
8 | :license: BSD, see LICENSE for details.
9 | #}
10 | {%- extends "layout.html" %}
11 | {% set title = _('Search') %}
12 | {% set script_files = script_files + ['_static/searchtools.js'] %}
13 | {% block extrahead %}
14 |
17 | {# this is used when loading the search index using $.ajax fails,
18 | such as on Chrome for documents on localhost #}
19 |
20 | {{ super() }}
21 | {% endblock %}
22 | {% block body %}
23 |
24 |
25 |
26 | {% trans %}Please activate JavaScript to enable the search
27 | functionality.{% endtrans %}
28 |
29 |
30 |
31 |
32 | {% if search_performed %}
33 | {{ _('Search Results') }}
34 | {% if not search_results %}
35 | {{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}
36 | {% endif %}
37 | {% endif %}
38 |
39 | {% if search_results %}
40 |
41 | {% for href, caption, context in search_results %}
42 |
43 | {{ caption }}
44 | {{ context|e }}
45 |
46 | {% endfor %}
47 |
48 | {% endif %}
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/searchbox.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/css/badge_only.css:
--------------------------------------------------------------------------------
1 | .font-smooth,.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:"\f02d"}.icon-book:before{content:"\f02d"}.fa-caret-down:before{content:"\f0d7"}.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}}
2 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goetas/twital/20dc10457d24257342e4a447961b5ab9e6376b5f/doc/_theme/sphinx_rtd_theme/static/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goetas/twital/20dc10457d24257342e4a447961b5ab9e6376b5f/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goetas/twital/20dc10457d24257342e4a447961b5ab9e6376b5f/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goetas/twital/20dc10457d24257342e4a447961b5ab9e6376b5f/doc/_theme/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/static/js/theme.js:
--------------------------------------------------------------------------------
1 | $( document ).ready(function() {
2 | // Shift nav in mobile when clicking the menu.
3 | $(document).on('click', "[data-toggle='wy-nav-top']", function() {
4 | $("[data-toggle='wy-nav-shift']").toggleClass("shift");
5 | $("[data-toggle='rst-versions']").toggleClass("shift");
6 | });
7 | // Close menu when you click a link.
8 | $(document).on('click', ".wy-menu-vertical .current ul li a", function() {
9 | $("[data-toggle='wy-nav-shift']").removeClass("shift");
10 | $("[data-toggle='rst-versions']").toggleClass("shift");
11 | });
12 | $(document).on('click', "[data-toggle='rst-current-version']", function() {
13 | $("[data-toggle='rst-versions']").toggleClass("shift-up");
14 | });
15 | // Make tables responsive
16 | $("table.docutils:not(.field-list)").wrap("
");
17 | });
18 |
19 | window.SphinxRtdTheme = (function (jquery) {
20 | var stickyNav = (function () {
21 | var navBar,
22 | win,
23 | stickyNavCssClass = 'stickynav',
24 | applyStickNav = function () {
25 | if (navBar.height() <= win.height()) {
26 | navBar.addClass(stickyNavCssClass);
27 | } else {
28 | navBar.removeClass(stickyNavCssClass);
29 | }
30 | },
31 | enable = function () {
32 | applyStickNav();
33 | win.on('resize', applyStickNav);
34 | },
35 | init = function () {
36 | navBar = jquery('nav.wy-nav-side:first');
37 | win = jquery(window);
38 | };
39 | jquery(init);
40 | return {
41 | enable : enable
42 | };
43 | }());
44 | return {
45 | StickyNav : stickyNav
46 | };
47 | }($));
48 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = css/theme.css
4 |
5 | [options]
6 | typekit_id = hiw1hhg
7 | analytics_id =
8 | sticky_navigation = False
9 |
--------------------------------------------------------------------------------
/doc/_theme/sphinx_rtd_theme/versions.html:
--------------------------------------------------------------------------------
1 | {% if READTHEDOCS %}
2 | {# Add rst-badge after rst-versions for small badge style. #}
3 |
4 |
5 | Read the Docs
6 | v: {{ current_version }}
7 |
8 |
9 |
10 |
11 | Versions
12 | {% for slug, url in versions %}
13 | {{ slug }}
14 | {% endfor %}
15 |
16 |
17 | Downloads
18 | {% for type, url in downloads %}
19 | {{ type }}
20 | {% endfor %}
21 |
22 |
23 | On Read the Docs
24 |
25 | Project Home
26 |
27 |
28 | Builds
29 |
30 |
31 |
32 | Free document hosting provided by
Read the Docs .
33 |
34 |
35 |
36 | {% endif %}
37 |
38 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | What is Twital?
2 | ###############
3 |
4 | Twital is a small "plugin" for Twig_ (a template engine for PHP)
5 | that adds some shortcuts and makes Twig's syntax more suitable for HTML based (XML, HTML5, XHTML, SGML) templates.
6 | Twital takes inspiration from PHPTal_, TAL_ and AngularJS_ (just for some aspects),
7 | mixing their language syntaxes with the powerful Twig templating engine system.
8 |
9 |
10 | To better understand the Twital's benefits, consider the following **Twital** template, which
11 | simply shows a list of users from an array:
12 |
13 | .. code-block:: xml+jinja
14 |
15 |
16 |
17 | {{ user.name }}
18 |
19 |
20 |
21 | To do the same thing using Twig, you need:
22 |
23 | .. code-block:: xml+jinja
24 |
25 | {% if users %}
26 |
27 | {% for user in users %}
28 |
29 | {{ user.name }}
30 |
31 | {% endfor %}
32 |
33 | {% endif %}
34 |
35 |
36 | As you can see, the Twital template is **more readable**, **less verbose** and
37 | and **you don't have to worry about opening and closing block instructions**
38 | (they are inherited from the HTML structure).
39 |
40 |
41 | One of the main advantages of Twital is the *implicit* presence of control statements, which makes
42 | templates more readable and less verbose. Furthermore, it has all Twig functionalities,
43 | such as template inheritance, translations, looping, filtering, escaping, etc.
44 | Here you can find a :doc:`complete list of Twital attributes and nodes.`.
45 |
46 | If some Twig functionality is not directly available for Twital,
47 | you can **freely mix Twig and Twital** syntaxes.
48 |
49 | In the example below, we have mixed Twital and Twig syntaxes to use Twig custom tags:
50 |
51 | .. code-block:: xml+jinja
52 |
53 |
54 | {% custom_tag %}
55 | {{ someUnsafeVariable }}
56 | {% endcustom_tag %}
57 |
58 |
59 |
60 | Installation
61 | ************
62 |
63 | There are two recommended ways to install Twital via Composer_:
64 |
65 | * using the ``composer require`` command:
66 |
67 | .. code-block:: bash
68 |
69 | composer require 'goetas/twital:0.1.*'
70 |
71 | * adding the dependency to your ``composer.json`` file:
72 |
73 | .. code-block:: js
74 |
75 | "require": {
76 | ..
77 | "goetas/twital":"0.1.*",
78 | ..
79 | }
80 |
81 |
82 | Getting started
83 | ***************
84 |
85 | First, you have to create a file that contains your template
86 | (named for example ``demo.twital.html``):
87 |
88 | .. code-block:: xml+jinja
89 |
90 |
91 | Hello {{ name }}
92 |
93 |
94 | Afterwards, you have to create a PHP script that instantiate the required objects:
95 |
96 | .. code-block:: php
97 |
98 | render('demo.twital.html', array('name' => 'John'));
108 |
109 |
110 | That's all!
111 |
112 |
113 | .. note::
114 |
115 | Since Twital uses Twig to compile and render templates,
116 | their performance is the same.
117 |
118 | Contents
119 | ********
120 |
121 | .. toctree::
122 | :maxdepth: 3
123 | :hidden:
124 |
125 | tags/index
126 | templates
127 | api
128 | mistakes
129 | symfony
130 |
131 | Contributing
132 | ************
133 |
134 | This is an open source project: contributions are welcome. If your are interested,
135 | you can contribute to documentation, source code, test suite or anything else!
136 |
137 | To start contributing right now, go to https://github.com/goetas/twital and fork
138 | it!
139 |
140 | To improve your contributing experience, you can take a look into https://github.com/goetas/twital/blob/master/CONTRIBUTING.md
141 | inside the root directory of Twital GIT repository.
142 |
143 | Symfony2 Users
144 | **************
145 |
146 | If you are a Symfony2_ user, you can add Twital to your project using the
147 | TwitalBundle_.
148 |
149 | The bundle integrates all most common functionalities as Assetic, Forms, Translations, Routing, etc.
150 |
151 |
152 | Note
153 | ****
154 |
155 | I'm sorry for the *terrible* english fluency used inside the documentation, I'm trying to improve it.
156 | Pull Requests are welcome.
157 |
158 |
159 | .. _Twig: http://twig.sensiolabs.org/
160 | .. _TwitalBundle: https://github.com/goetas/twital-bundle
161 | .. _Symfony2: http://symfony.com
162 | .. _Composer: https://getcomposer.org/
163 | .. _TAL: http://en.wikipedia.org/wiki/Template_Attribute_Language
164 | .. _PHPTal: http://phptal.org/
165 | .. _AngularJS: http://angularjs.org/
166 |
--------------------------------------------------------------------------------
/doc/mistakes.rst:
--------------------------------------------------------------------------------
1 | Common mistakes and tricks
2 | --------------------------
3 |
4 | Since Twital internally uses XML, you need to pay attention to some aspects while writing a template.
5 | All templates must be XML valid (some exceptions are allowed...).
6 |
7 | - All templates must have **one** root node.
8 | When needed, you can use the `t:omit` node to enclose other nodes.
9 |
10 | .. code-block:: xml
11 |
12 |
13 | one
14 | two
15 |
16 |
17 |
18 | - A template must be well formatted (opening and closing nodes, entities, DTD, etc...).
19 | Some aspects as namespaces, HTML5 & HTML entities, non-self closing tags can be "repaired",
20 | but it is recommended to be closer to XML as much as possible.
21 |
22 | The example below lacks the `br` self closing slash, but using the HTML5 source adapter it can be omitted.
23 |
24 | .. code-block:: html
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | - The usage of `&` must follow XML syntax rules.
34 |
35 | .. code-block:: html
36 |
37 |
38 | &
39 | <
40 | >
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | - To be compatible with all browsers, the use of the `script` tag should be combined with `CDATA` sections and script comments.
49 |
50 | .. code-block:: html
51 |
52 |
59 |
66 |
--------------------------------------------------------------------------------
/doc/symfony.rst:
--------------------------------------------------------------------------------
1 | Symfony Users
2 | #############
3 |
4 | If you are a Symfony_ user, the most convenient way to integrate Twital into your project is using the
5 | TwitalBundle_.
6 |
7 | The bundle integrates all most common Symfony functionalities as Assetic, Forms, Translations etc.
8 |
9 | .. _TwitalBundle: https://github.com/goetas/twital-bundle
10 | .. _Symfony: https://symfony.com
11 |
--------------------------------------------------------------------------------
/doc/tags/attr-append.rst:
--------------------------------------------------------------------------------
1 | ``attr-append``
2 | ===============
3 |
4 | Twital allows you to create HTML/XML attributes in a very simple way.
5 |
6 | `t:attr-append` is a different version of `t:attr`:
7 | it allows to append content to existing attributes instead of replacing it.
8 |
9 | .. code-block:: xml+jinja
10 |
11 |
12 | class will be "row even" if 'i' is odd.
13 |
14 |
15 | In the same way of `t:attr`, `condition` and the value of attribute can be any valid Twig expression.
16 |
17 | .. code-block:: xml+jinja
18 |
19 |
21 | class will be "row EVEN" if 'i' is odd.
22 |
23 |
24 |
25 | When not needed, you can omit the condition instruction.
26 |
27 | .. code-block:: xml+jinja
28 |
29 |
30 | Class will be "row even"
31 |
32 |
--------------------------------------------------------------------------------
/doc/tags/attr.rst:
--------------------------------------------------------------------------------
1 | ``attr``
2 | ========
3 |
4 | Twital allows you to create HTML/XML attributes in a very simple way.
5 | You do not have to mess up with control structures inside HTML tags.
6 |
7 | Let's see how does it work:
8 |
9 | .. code-block:: xml+jinja
10 |
11 |
12 | My Company
13 |
14 |
15 |
16 | Here we add conditionally an attribute based on the value of the `condition` expression.
17 |
18 |
19 | You can use any Twig test expression as **condition** and **attribute value**,
20 | but the attribute name must be a litteral.
21 |
22 | .. code-block:: xml+jinja
23 |
24 |
27 | Here wins the last class that condition will be evaluated to true.
28 |
29 |
30 | When not needed, you can omit the condition instruction.
31 |
32 | .. code-block:: xml+jinja
33 |
34 |
35 | Class will be "row"
36 |
37 |
38 | .. tip::
39 |
40 | `attr-append`
41 |
42 |
43 | To set an HTML5 boolean attribute, just use booleans as ``true`` or ``false``.
44 |
45 | .. code-block:: xml+jinja
46 |
47 |
48 | My Company
49 |
50 |
51 | The previous template will be rendered as:
52 |
53 | .. code-block:: html
54 |
55 |
56 | My Company
57 |
58 |
59 | .. note::
60 |
61 | Since XML does not have the concept of "boolean attributes",
62 | this feature may break your output if you are using XML.
63 |
64 |
65 |
66 | To to remove and already defined attribute, use ``false`` as attribute value
67 |
68 | .. code-block:: xml+jinja
69 |
70 |
71 | My Company
72 |
73 |
74 | The previous template will be rendered as:
75 |
76 | .. code-block:: html
77 |
78 |
79 | My Company
80 |
81 |
82 |
--------------------------------------------------------------------------------
/doc/tags/autoescape.rst:
--------------------------------------------------------------------------------
1 | ``autoescape``
2 | ==============
3 |
4 | The Twital instruction for Twig ``autoescape`` tag is ``t:autoescape`` attribute.
5 |
6 |
7 | Whether automatic escaping is enabled or not, you can mark a section of a
8 | template to be escaped or not by using the ``autoescape`` tag.
9 |
10 | To see how to use it, take a look at this example:
11 |
12 | .. code-block:: xml+jinja
13 |
14 |
15 | Everything will be automatically escaped in this block
16 | using the HTML strategy
17 |
18 |
19 |
20 | Everything will be automatically escaped in this block
21 | using the HTML strategy
22 |
23 |
24 |
25 | Everything will be automatically escaped in this block
26 | using the js escaping strategy
27 |
28 |
29 |
30 | Everything will be outputted as is in this block
31 |
32 |
33 | When automatic escaping is enabled, everything is escaped by default, except for
34 | values explicitly marked as safe. Those can be marked in the template by using
35 | the Twig ``raw`` filter:
36 |
37 | .. code-block:: xml+jinja
38 |
39 |
40 | {{ safe_value|raw }}
41 |
42 |
43 |
--------------------------------------------------------------------------------
/doc/tags/block.rst:
--------------------------------------------------------------------------------
1 | ``block``
2 | =========
3 |
4 |
5 | The Twital instruction for Twig ``block`` tag is ``t:block`` node.
6 |
7 | To see how to use it, consider the following base template named ``layout.html.twital``:
8 |
9 | .. code-block:: xml+jinja
10 |
11 |
12 |
13 | Hello world!
14 |
15 |
16 | Hello!
17 |
18 |
19 |
20 |
21 | To improve the greeting message, we can extend it using the ``t:textends`` node,
22 | so we can create a new template called ``hello.html.twital``.
23 |
24 | .. code-block:: xml+jinja
25 |
26 |
27 |
28 | Hello {{name}}!
29 |
30 |
31 |
32 | As you can see, we have overwritten the content of the ``content`` block with a new one.
33 | To do this, we have used a ``t:block`` node.
34 |
35 | Of course, if needed, you can also **call the parent block** from inside. It is simple:
36 |
37 | .. code-block:: xml+jinja
38 |
39 |
40 |
41 | {{parent()}}
42 | Hello {{name}}!
43 |
44 |
45 |
46 | .. note::
47 |
48 | To learn more about template inheritance, you can read the
49 | `Twig official documentation `_
50 |
--------------------------------------------------------------------------------
/doc/tags/capture.rst:
--------------------------------------------------------------------------------
1 | ``capture``
2 | ===========
3 |
4 | This attribute acts as a ``set`` tag and allows to 'capture' chunks of text into a variable:
5 |
6 | .. code-block:: xml+jinja
7 |
8 |
11 |
12 |
13 | All contents inside "pagination" div will be captured and saved inside a variable named `foo`.
14 |
15 | .. note::
16 |
17 | For more information about the ``set`` tag, please refer to `Twig official documentation `_.
18 |
--------------------------------------------------------------------------------
/doc/tags/content.rst:
--------------------------------------------------------------------------------
1 | ``content``
2 | ===========
3 |
4 | This attribute allows to replace the content of a note with the content of a variable.
5 |
6 | Suppose to have a variable ``foo`` with a value ``My name is John`` and the following template:
7 |
8 | .. code-block:: xml+jinja
9 |
10 |
13 |
14 |
15 | The output will be:
16 |
17 | .. code-block:: xml+jinja
18 |
19 |
20 |
21 |
22 | This can be useful to put come "test" content in your templates that will have a nice aspect on WYSIWYG
23 | editors, but at runtime will be replaced by real data coming from variables.
24 |
--------------------------------------------------------------------------------
/doc/tags/embed.rst:
--------------------------------------------------------------------------------
1 | ``embed``
2 | =========
3 |
4 | The Twital instruction for Twig ``embed`` tag is ``t:embed`` node.
5 |
6 | The embed tag combines the behaviour of include and extends.
7 | It allows you to include another template's contents, just like include does.
8 | But, it also allows you to override any block defined inside the included template,
9 | like when extending a template.
10 |
11 | To learn about the usefulness of `embed`, you can read the official documentation.
12 |
13 | Now, let's see how to use it, take a look to this example:
14 |
15 | .. code-block:: xml+jinja
16 |
17 |
18 |
19 | Some content for the left teaser box
20 |
21 |
22 | Some content for the right teaser box
23 |
24 |
25 |
26 | You can add additional variables by passing them after the ``with`` attribute:
27 |
28 | .. code-block:: xml+jinja
29 |
30 |
31 | ...
32 |
33 |
34 |
35 | You can disable the access to the current context by using the ``only`` attribute:
36 |
37 | .. code-block:: xml+jinja
38 |
39 |
40 | ...
41 |
42 |
43 | You can mark an include with ``ignore-missing`` attribute in which case Twital
44 | will ignore the statement if the template to be included does not exist.
45 |
46 | .. code-block:: xml+jinja
47 |
48 |
49 | ...
50 |
51 |
52 | ``ignore-missing`` can not be an expression; it has to be evaluated only at compile time.
53 |
54 |
55 | To use Twig expressions as template name you have to use a namespace prefix on 'form' attribute:
56 |
57 | .. code-block:: xml+jinja
58 |
59 |
60 | ...
61 |
62 |
63 | ...
64 |
65 |
66 | .. note::
67 |
68 | For more information about the ``embed`` tag, please refer to `Twig official documentation `_.
69 |
70 | .. seealso:: :doc:`include<../tags/include>`
71 |
--------------------------------------------------------------------------------
/doc/tags/extends.rst:
--------------------------------------------------------------------------------
1 | ``extends``
2 | ===========
3 |
4 |
5 | The Twital instruction for Twig ``extends`` tag is ``t:extends`` node.
6 | To see how to use it, take a look at this example:
7 |
8 |
9 | Consider the following base template named ``layout.html.twital``.
10 | Here we are creating a simple page that says hello to someone.
11 |
12 | With the `t:block` attribute we mark the body content as extensibile.
13 |
14 | .. code-block:: xml+jinja
15 |
16 |
17 |
18 | Hello world!
19 |
20 |
21 |
22 | Hello!
23 |
24 |
25 |
26 |
27 |
28 | To improve the greating message, we can extend it using the ``t:textends`` node,
29 | so we can create a new template called ``hello.html.twital``.
30 |
31 | .. code-block:: xml+jinja
32 |
33 |
34 |
35 | Hello {{name}}!
36 |
37 |
38 |
39 | As you can see, we have overwritten the content of the ``content`` block with a new one.
40 | To do this, we have used a ``t:block`` node.
41 |
42 | You can also **extend a Twig Template**, so you can mix Twig and Twital Templates.
43 |
44 | .. code-block:: xml+jinja
45 |
46 |
47 |
48 | Hello {{name}}!
49 |
50 |
51 |
52 |
53 | Sometimes it's useful to obtain the layout **template name from a variable**:
54 | to do this you have to add the Twital namespace to attribute name:
55 |
56 | .. code-block:: xml+jinja
57 |
58 |
59 |
60 | Hello {{name}}!
61 |
62 |
63 |
64 | Now ``hello.html.twital`` can inherit dynamically from different templates.
65 | Now the tempalte name can be any valid Twig expression.
66 |
67 | .. note::
68 |
69 | To learn more about template inheritance, you can read
70 | the `Twig official documentation `_.
71 |
--------------------------------------------------------------------------------
/doc/tags/filter.rst:
--------------------------------------------------------------------------------
1 | ``filter``
2 | ==========
3 |
4 | The Twital instruction for Twig ``filter`` tag is ``t:filter`` attribute.
5 |
6 | To see how to use it, take a look at this example:
7 |
8 | .. code-block:: xml+jinja
9 |
10 |
11 | This text becomes uppercase
12 |
13 |
14 |
15 | This text becomes uppercase
16 |
17 |
18 |
19 | .. note::
20 |
21 | To learn more about the `filter` tab, you can read the
22 | `Twig official documentation `_.
23 |
--------------------------------------------------------------------------------
/doc/tags/for.rst:
--------------------------------------------------------------------------------
1 | ``for``
2 | =======
3 |
4 | The Twital instruction for Twig's ``for`` tag is the ``t:for`` attribute.
5 |
6 |
7 | Loop over each item in a sequence. For example, to display a list of users
8 | provided in a variable called ``users``:
9 |
10 | .. code-block:: xml+jinja
11 |
12 | Members
13 |
14 |
15 | {{ user.username }}
16 |
17 |
18 |
19 | .. note::
20 |
21 | For more information about the ``for`` tag, please refer to `Twig official documentation `_.
--------------------------------------------------------------------------------
/doc/tags/if.rst:
--------------------------------------------------------------------------------
1 | ``if``
2 | ======
3 |
4 | The Twital instruction for Twig's ``if`` tag is the``t:if`` attribute.
5 |
6 | .. code-block:: xml+jinja
7 |
8 |
9 | Our website is in maintenance mode. Please, come back later.
10 |
11 |
12 |
13 | ``elseif`` and ``else`` are not *well* supported, but you can always combine Twital with Twig.
14 |
15 | .. code-block:: xml+jinja
16 |
17 |
18 | {%if online_users == 1%}
19 | one user
20 | {% else %}
21 | {{online_users}} users
22 | {% endif %}
23 |
24 |
25 | But if you are really interested to use ``elseif`` and ``else`` tags with Twital
26 | you can do it anyway.
27 |
28 | .. code-block:: xml+jinja
29 |
30 |
31 | I'm online
32 |
33 |
34 | I'm invisible
35 |
36 |
37 | I'm offline
38 |
39 |
40 | This syntax will work if there are no non-space charachters between the ``p`` tags.
41 |
42 | This example will not work:
43 |
44 | .. code-block:: xml+jinja
45 |
46 |
47 | I'm online
48 |
49 |
50 |
51 | I'm offline
52 |
53 |
54 |
55 | I'm online
56 |
57 | some text...
58 |
59 | I'm offline
60 |
61 |
62 | .. note::
63 |
64 | To learn more about the Twig ``if`` tag, please refer to `Twig official documentation `_.
65 |
--------------------------------------------------------------------------------
/doc/tags/import.rst:
--------------------------------------------------------------------------------
1 | ``import``
2 | ==========
3 |
4 | The Twital instruction for Twig ``import`` tag is ``t:import`` node.
5 |
6 |
7 | Since Twig supports putting often used code into :doc:`macros<../tags/macro>`. These
8 | macros can go into different templates and get imported from there.
9 |
10 | There are two ways to import templates: (1) you can import the complete template into a variable or (2)
11 | request specific macros from it.
12 |
13 | Imagine that we have a helper module that renders forms (called ``forms.html``):
14 |
15 | .. code-block:: xml+jinja
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | To use your macro, you can do something like this:
26 |
27 | .. code-block:: xml+jinja
28 |
29 |
30 |
31 | Username
32 | {{ forms.input('username') }}
33 | Password
34 | {{ forms.input('password', null, 'password') }}
35 | {{ forms.textarea('comment') }}
36 |
37 |
38 | If you want to import your macros directly into your template (without referring to it with a variable):
39 |
40 | .. code-block:: xml+jinja
41 |
42 |
43 |
44 | Username
45 | {{ input_field('username') }}
46 | Password
47 | {{ input_field('password', '', 'password') }}
48 |
49 | {{ textarea('comment') }}
50 |
51 | .. tip::
52 |
53 | To import macros from the current file, use the special ``_self`` variable
54 | for the source.
55 |
56 | .. note::
57 |
58 | For more information about the ``import`` tag, please refer to
59 | `Twig official documentation `_.
60 |
61 | .. seealso:: :doc:`macro<../tags/macro>`
62 |
--------------------------------------------------------------------------------
/doc/tags/include.rst:
--------------------------------------------------------------------------------
1 | ``include``
2 | ===========
3 |
4 | The ``include`` statement includes a template and returns the rendered content
5 | of that file into the current namespace:
6 |
7 | .. code-block:: xml+jinja
8 |
9 |
10 | Body
11 |
12 |
13 | A little bit different syntax to include a template can be:
14 |
15 | .. code-block:: xml+jinja
16 |
17 |
18 |
Fake news content
19 |
Lorem ipsum
20 |
21 |
22 | In this case, the content of div will be replaced with the content of template 'news.html'.
23 |
24 |
25 | You can add additional variables by passing them after the ``with`` attribute:
26 |
27 | .. code-block:: xml+jinja
28 |
29 |
30 |
31 |
32 | You can disable the access to the current context by using the ``only`` attribute:
33 |
34 | .. code-block:: xml+jinja
35 |
36 |
37 |
38 | You can mark an include with the ``ignore-missing`` attribute in which case Twital
39 | will ignore the statement if the template to be included does not exist.
40 |
41 | .. code-block:: xml+jinja
42 |
43 |
44 |
45 | ``ignore-missing`` can not be an expression; it has to be evauluated only at compile time.
46 |
47 |
48 | To use Twig expressions as template name you have to use a namespace prefix on 'form' attribute:
49 |
50 | .. code-block:: xml+jinja
51 |
52 |
53 |
54 |
55 | .. note::
56 |
57 | For more information about the ``include`` tag, please refer to
58 | `Twig official documentation `_.
59 |
--------------------------------------------------------------------------------
/doc/tags/index.rst:
--------------------------------------------------------------------------------
1 | Tags reference
2 | ==============
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | if
8 | for
9 | set
10 | block
11 | extends
12 | embed
13 | include
14 | import
15 | macro
16 | use
17 | sandbox
18 | autoescape
19 | capture
20 | filter
21 | spaceless
22 | omit
23 | attr
24 | attr-append
25 | content
26 | replace
27 |
--------------------------------------------------------------------------------
/doc/tags/macro.rst:
--------------------------------------------------------------------------------
1 | ``macro``
2 | =========
3 |
4 | The Twital instruction for Twig ``macro`` tag is ``t:macro`` node.
5 |
6 | To declare a macro inside Twital, the syntax is:
7 |
8 | .. code-block:: xml+jinja
9 |
10 |
11 |
12 |
13 |
14 |
15 | To use a macro inside your Twital template, take a look at the :doc:``import<../tags/import>`` attribute.
16 |
17 | .. note::
18 |
19 | For more information about the ``macro`` tag, please refer to
20 | `Twig official documentation `__.
21 |
--------------------------------------------------------------------------------
/doc/tags/omit.rst:
--------------------------------------------------------------------------------
1 | ``omit``
2 | ========
3 |
4 | This attribute asks the Twital parser to ignore the elements' open and close tag,
5 | its content will still be evaluated.
6 |
7 | .. code-block:: xml+jinja
8 |
9 |
10 | {{ username }}
11 |
12 |
13 |
14 | {{ username }}
15 |
16 |
17 |
18 | This attribute is useful when you want to create element optionally,
19 | e.g. hide a link if certain condition is met.
20 |
--------------------------------------------------------------------------------
/doc/tags/replace.rst:
--------------------------------------------------------------------------------
1 | ``replace``
2 | ===========
3 |
4 | This attribute acts in a similar way to ``content`` attribute,
5 | instead of replacing the content of a node, will replace the node itself.
6 |
7 | Suppose to have a variable ``foo`` with a value ``My name is John`` and the following template:
8 |
9 | .. code-block:: xml+jinja
10 |
11 |
14 |
15 |
16 | The output will be:
17 |
18 | .. code-block:: xml+jinja
19 |
20 | My name is John
21 |
22 |
23 | This can be useful to put come "test" content in your templates that will have a nice aspect on WYSIWYG
24 | editors, but at runtime will be replaced by real data coming from variables.
25 |
--------------------------------------------------------------------------------
/doc/tags/sandbox.rst:
--------------------------------------------------------------------------------
1 | ``sandbox``
2 | ===========
3 |
4 | The Twital instruction for Twig ``import`` tag is ``t:sandbox`` node or the ``t:sandbox`` attribute.
5 |
6 | The ``sandbox`` tag can be used to enable the sandboxing mode for an included
7 | template, when sandboxing is not enabled globally for the Twig environment:
8 |
9 | .. code-block:: xml+jinja
10 |
11 |
12 | {% include 'user.html' %}
13 |
14 |
15 |
16 | {% include 'user.html' %}
17 |
18 |
19 |
20 | .. note::
21 |
22 | For more information about the ``sandbox`` tag, please refer to
23 | `Twig official documentation `_.
24 |
--------------------------------------------------------------------------------
/doc/tags/set.rst:
--------------------------------------------------------------------------------
1 | ``set``
2 | =======
3 |
4 | The Twital instruction for Twig ``set`` tag is the ``t:set`` attribute.
5 |
6 |
7 | You can use ``set`` to assign variables. The syntax to use the ``set`` attribute is:
8 |
9 | .. code-block:: xml+jinja
10 |
11 | Hello {{ name }}
12 | Hello {{ foo.bas }}
13 |
14 | Hello {{ name }} {{ surname }}
15 |
16 |
17 | .. note::
18 |
19 | For more information about the ``set`` tag, please refer to
20 | `Twig official documentation `_.
21 |
--------------------------------------------------------------------------------
/doc/tags/spaceless.rst:
--------------------------------------------------------------------------------
1 | ``spaceless``
2 | =============
3 |
4 | The Twital instruction for Twig ``spaceless`` tag is ``t:spaceless`` node or the ``t:spaceless`` attribute.
5 |
6 |
7 | .. code-block:: xml+jinja
8 |
9 |
10 | {% include 'user.html' %}
11 |
12 |
13 |
14 | {% include 'user.html' %}
15 |
16 |
17 |
18 | .. note::
19 |
20 | For more information about the ``spaceless`` tag, please refer to
21 | `Twig official documentation `_.
22 |
--------------------------------------------------------------------------------
/doc/tags/use.rst:
--------------------------------------------------------------------------------
1 | ``use``
2 | =======
3 |
4 | The Twital instruction for Twig ``use`` tag is ``t:use`` node.
5 |
6 | This is a fature that allows a/the horizontal reuse of templates.
7 | To learn more about it, you can read the official documentation.
8 |
9 | Let's see how does it work:
10 |
11 | .. code-block:: xml+jinja
12 |
13 |
14 |
15 |
16 | ...
17 |
18 |
19 |
20 | You can create some aliases for block inside "used" template to avoid name conflicting:
21 |
22 | .. code-block:: xml+jinja
23 |
24 |
25 |
26 |
27 |
28 | {{ block('sidebar_original') }}
29 |
30 |
31 |
32 | .. note::
33 |
34 | For more information about the ``use`` tag, please refer to
35 | `Twig official documentation `_.
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 | ./tests/
21 |
22 |
23 |
24 |
25 | src
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Attribute.php:
--------------------------------------------------------------------------------
1 |
8 | *
9 | */
10 | interface Attribute
11 | {
12 | /**
13 | * Stop processing current node children.
14 | *
15 | * @var int
16 | */
17 | const STOP_NODE = 1;
18 |
19 | /**
20 | * Stop processing current node attributes.
21 | *
22 | * @var int
23 | */
24 | const STOP_ATTRIBUTE = 2;
25 |
26 | /**
27 | *
28 | * @param \DOMAttr $att
29 | * @param Compiler $context
30 | * @return int|null Bitmask of {Attribute::STOP_NODE} and {Attribute::STOP_ATTRIBUTE}
31 | */
32 | public function visit(\DOMAttr $att, Compiler $context);
33 | }
34 |
--------------------------------------------------------------------------------
/src/Attribute/AttrAppendAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class AttrAppendAttribute extends AttrAttribute
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 | $expressions = ParserHelper::staticSplitExpression($att->value, ",");
18 |
19 | $attributes = array();
20 | foreach ($expressions as $k => $expression) {
21 | $expressions[$k] = $attrExpr = self::splitAttrExpression($expression);
22 | $attNode = null;
23 | if (!isset($attributes[$attrExpr['name']])) {
24 | $attributes[$attrExpr['name']] = array();
25 | }
26 | if ($node->hasAttribute($attrExpr['name'])) {
27 | $attNode = $node->getAttributeNode($attrExpr['name']);
28 | $node->removeAttributeNode($attNode);
29 | $attributes[$attrExpr['name']][] = "'" . addcslashes($attNode->value, "'") . "'";
30 | }
31 | if ($attrExpr['test'] === "true" || $attrExpr['test'] === "1") {
32 | unset($expressions[$k]);
33 | $attributes[$attrExpr['name']][] = $attrExpr['expr'];
34 | }
35 | }
36 |
37 | $code = array();
38 |
39 | $varName = self::getVarname($node);
40 | $code[] = $context->createControlNode("if $varName is not defined");
41 | $code[] = $context->createControlNode("set $varName = {" . ParserHelper::implodeKeyedDouble(",", $attributes, true) . " }");
42 | $code[] = $context->createControlNode("else");
43 |
44 | foreach ($attributes as $attribute => $values) {
45 | $code[] = $context->createControlNode("if {$varName}['{$attribute}'] is defined");
46 | $code[] = $context->createControlNode("set $varName = $varName|merge({ '$attribute' : ({$varName}['{$attribute}']|merge([" . implode(",", $values) . "])) })");
47 | $code[] = $context->createControlNode("else");
48 | $code[] = $context->createControlNode("set $varName = $varName|merge({ '$attribute' : [" . implode(",", $values) . "]})");
49 | $code[] = $context->createControlNode("endif");
50 | }
51 |
52 | $code[] = $context->createControlNode("endif");
53 |
54 | foreach ($expressions as $attrExpr) {
55 | $code[] = $context->createControlNode("if {$attrExpr['test']}");
56 | $code[] = $context->createControlNode(
57 | "set {$varName} = {$varName}|merge({ '{$attrExpr['name']}':{$varName}.{$attrExpr['name']}|merge([{$attrExpr['expr']}]) })"
58 | );
59 | $code[] = $context->createControlNode("endif");
60 | }
61 |
62 | $this->addSpecialAttr($node, $varName, $code);
63 | $node->removeAttributeNode($att);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Attribute/AttrAttribute.php:
--------------------------------------------------------------------------------
1 |
13 | *
14 | */
15 | class AttrAttribute implements Attribute
16 | {
17 | public static function getVarname(\DOMElement $node)
18 | {
19 | return "__a" . sha1($node->getAttributeNS(Twital::NS, '__internal-id__') . spl_object_hash($node));
20 | }
21 |
22 | public function visit(\DOMAttr $att, Compiler $context)
23 | {
24 | $node = $att->ownerElement;
25 | $expressions = ParserHelper::staticSplitExpression($att->value, ",");
26 |
27 | $attributes = array();
28 | foreach ($expressions as $k => $expression) {
29 | $expressions[$k] = $attrExpr = self::splitAttrExpression($expression);
30 | $attNode = null;
31 |
32 | if ($node->hasAttribute($attrExpr['name'])) {
33 | $attNode = $node->getAttributeNode($attrExpr['name']);
34 | $node->removeAttributeNode($attNode);
35 | }
36 |
37 | if ($attrExpr['test'] === "true" || $attrExpr['test'] === "1") {
38 | unset($expressions[$k]);
39 | $attributes[$attrExpr['name']] = "[{$attrExpr['expr']}]";
40 | } elseif ($attNode) {
41 | $attributes[$attrExpr['name']] = "['" . addcslashes($attNode->value, "'") . "']";
42 | } else {
43 | $attributes[$attrExpr['name']] = "[]";
44 | }
45 | }
46 |
47 | $varName = self::getVarname($node);
48 | $code = array();
49 | $code[] = $context->createControlNode("if $varName is not defined");
50 | $code[] = $context->createControlNode("set $varName = {" . ParserHelper::implodeKeyed(",", $attributes, true) . " }");
51 | $code[] = $context->createControlNode("else");
52 | $code[] = $context->createControlNode("set $varName = $varName|merge({" . ParserHelper::implodeKeyed(",", $attributes, true) . "})");
53 | $code[] = $context->createControlNode("endif");
54 |
55 | foreach ($expressions as $attrExpr) {
56 | $code[] = $context->createControlNode("if {$attrExpr['test']}");
57 | $code[] = $context->createControlNode("set {$varName} = {$varName}|merge({ '{$attrExpr['name']}':[{$attrExpr['expr']}] })");
58 | $code[] = $context->createControlNode("endif");
59 | }
60 |
61 | $this->addSpecialAttr($node, $varName, $code);
62 | $node->removeAttributeNode($att);
63 | }
64 |
65 | public static function splitAttrExpression($str)
66 | {
67 | $parts = ParserHelper::staticSplitExpression($str, "?");
68 | if (count($parts) == 1) {
69 | $attr = self::findAttrParts($parts[0]);
70 | $attr['test'] = 'true';
71 |
72 | return $attr;
73 | } elseif (count($parts) == 2) {
74 | $attr = self::findAttrParts($parts[1]);
75 | $attr['test'] = $parts[0];
76 |
77 | return $attr;
78 | } else {
79 | throw new Exception(__CLASS__ . "::splitAttrExpression error in '$str'");
80 | }
81 | }
82 |
83 | protected function addSpecialAttr(\DOMElement $node, $varName, array $code)
84 | {
85 | $node->setAttribute("__attr__", $varName);
86 |
87 | $ref = $node;
88 | foreach (array_reverse($code) as $line) {
89 | $node->parentNode->insertBefore($line, $ref);
90 | $ref = $line;
91 | }
92 | }
93 |
94 | protected static function findAttrParts($str)
95 | {
96 | $mch = array();
97 | if (preg_match("/^([a-z_][a-z0-9\\-_]*:[a-z][a-z0-9\\-_]*)\\s*=\\s*/i", $str, $mch)) {
98 | return array(
99 | 'name' => $mch[1],
100 | 'expr' => trim(substr($str, strlen($mch[0])))
101 | );
102 | } elseif (preg_match("/^([a-z_][a-z0-9\\-_]*)\\s*=\\s*/i", $str, $mch)) {
103 | return array(
104 | 'name' => $mch[1],
105 | 'expr' => trim(substr($str, strlen($mch[0])))
106 | );
107 | } else {
108 | throw new Exception(__CLASS__ . "::findAttrParts error in '$str'");
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Attribute/BaseAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class BaseAttribute implements AttributeBase
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 |
18 | $pi = $context->createControlNode("{$att->localName} " . html_entity_decode($att->value));
19 | $node->parentNode->insertBefore($pi, $node);
20 |
21 | $pi = $context->createControlNode("end{$att->localName}");
22 |
23 | $node->parentNode->insertBefore($pi, $node->nextSibling); // insert after
24 |
25 | $node->removeAttributeNode($att);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Attribute/BlockInnerAttribute.php:
--------------------------------------------------------------------------------
1 | foo' into '{% block name%}foo{% endblock %}
'
11 | * @author Asmir Mustafic
12 | *
13 | */
14 | class BlockInnerAttribute implements AttributeBase
15 | {
16 | public function visit(\DOMAttr $att, Compiler $context)
17 | {
18 | $node = $att->ownerElement;
19 | $node->removeAttributeNode($att);
20 |
21 | // create sandbox and append it to the node
22 | $sandbox = $node->ownerDocument->createElementNS(Twital::NS, "sandbox");
23 |
24 | // move all child to sandbox to sandbox
25 | while ($node->firstChild) {
26 | $child = $node->removeChild($node->firstChild);
27 | $sandbox->appendChild($child);
28 | }
29 | $node->appendChild($sandbox);
30 |
31 | //$context->compileAttributes($node);
32 | $context->compileChilds($sandbox);
33 |
34 |
35 | $start = $context->createControlNode("block " . $att->value);
36 | $end = $context->createControlNode("endblock");
37 |
38 | $sandbox->insertBefore($start, $sandbox->firstChild);
39 | $sandbox->appendChild($end);
40 |
41 | DOMHelper::replaceWithSet($sandbox, iterator_to_array($sandbox->childNodes));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Attribute/BlockOuterAttribute.php:
--------------------------------------------------------------------------------
1 | foo' into '{% block name%}foo
{% endblock %}'
11 | *
12 | * @author Asmir Mustafic
13 | *
14 | */
15 | class BlockOuterAttribute implements AttributeBase
16 | {
17 | public function visit(\DOMAttr $att, Compiler $context)
18 | {
19 | $node = $att->ownerElement;
20 | $node->removeAttributeNode($att);
21 |
22 | // create sandbox
23 | $sandbox = $node->ownerDocument->createElementNS(Twital::NS, "sandbox");
24 | $node->parentNode->insertBefore($sandbox, $node);
25 |
26 | // move to sandbox
27 | $node->parentNode->removeChild($node);
28 | $sandbox->appendChild($node);
29 |
30 | $context->compileAttributes($node);
31 | $context->compileChilds($node);
32 |
33 |
34 | $start = $context->createControlNode("block " . $att->value);
35 | $end = $context->createControlNode("endblock");
36 |
37 | $sandbox->insertBefore($start, $sandbox->firstChild);
38 | $sandbox->appendChild($end);
39 |
40 | DOMHelper::replaceWithSet($sandbox, iterator_to_array($sandbox->childNodes));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Attribute/CaptureAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class CaptureAttribute implements Attribute
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 |
18 | $pi = $context->createControlNode("set " . html_entity_decode($att->value));
19 | $node->parentNode->insertBefore($pi, $node);
20 |
21 | $pi = $context->createControlNode("endset");
22 |
23 | $node->parentNode->insertBefore($pi, $node->nextSibling); // insert after
24 |
25 | $node->removeAttributeNode($att);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Attribute/ContentAttribute.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class ContentAttribute implements Attribute
14 | {
15 | public function visit(\DOMAttr $att, Compiler $context)
16 | {
17 | $node = $att->ownerElement;
18 | DOMHelper::removeChilds($node);
19 | $pi = $context->createPrintNode(html_entity_decode($att->value));
20 | $node->appendChild($pi);
21 |
22 | $node->removeAttributeNode($att);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Attribute/ElseAttribute.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class ElseAttribute implements AttributeBase
14 | {
15 | public function visit(\DOMAttr $att, Compiler $context)
16 | {
17 | $node = $att->ownerElement;
18 |
19 | if (!$prev = IfAttribute::findPrevElement($node)) {
20 | throw new Exception("The attribute 'elseif' must be the very next sibling of an 'if' of 'elseif' attribute");
21 | }
22 |
23 | $pi = $context->createControlNode("else");
24 | $node->parentNode->insertBefore($pi, $node);
25 |
26 | $pi = $context->createControlNode("endif");
27 | $node->parentNode->insertBefore($pi, $node->nextSibling);
28 |
29 | $node->removeAttributeNode($att);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Attribute/ElseIfAttribute.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class ElseIfAttribute implements AttributeBase
15 | {
16 | public function visit(\DOMAttr $att, Compiler $context)
17 | {
18 | $node = $att->ownerElement;
19 |
20 | if (!$prev = IfAttribute::findPrevElement($node)) {
21 | throw new Exception("The attribute 'elseif' must be the very next sibling of an 'if' of 'elseif' attribute");
22 | }
23 |
24 | $pi = $context->createControlNode("elseif " . html_entity_decode($att->value));
25 | $node->parentNode->insertBefore($pi, $node);
26 |
27 | if (!($nextElement = IfAttribute::findNextElement($node)) || (!$nextElement->hasAttributeNS(Twital::NS, 'elseif') && !$nextElement->hasAttributeNS(Twital::NS, 'else'))) {
28 | $pi = $context->createControlNode("endif");
29 | $node->parentNode->insertBefore($pi, $node->nextSibling); // insert after
30 | } else {
31 | IfAttribute::removeWhitespace($node);
32 | }
33 |
34 | $node->removeAttributeNode($att);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Attribute/ExtendsAttribute.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class ExtendsAttribute implements AttributeBase
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 |
18 | $filename = '"' . $att->value . '"';
19 |
20 |
21 | $xp = new \DOMXPath($att->ownerDocument);
22 | $xp->registerNamespace("t", Twital::NS);
23 |
24 | $candidates = array();
25 | foreach ($xp->query(".//*[@t:block-inner or @t:block-outer]|.//t:*", $node) as $blockNode) {
26 | $ancestors = $xp->query("ancestor::*[@t:block-inner or @t:block-outer or @t:extends]", $blockNode);
27 |
28 | if ($ancestors->length === 1) {
29 | $candidates[] = $blockNode;
30 | }
31 | }
32 |
33 | // having block-inner makes no sense when child of an t:extends (t:extend can have only t:block child)
34 | // so lets convert them to t:block nodes
35 | $candidates = $this->convertBlockInnerIntoBlock($candidates, $node);
36 |
37 | foreach ($candidates as $candidate) {
38 | if ($candidate->parentNode !== $node) {
39 | $candidate->parentNode->removeChild($candidate);
40 | $node->appendChild($candidate);
41 | }
42 | }
43 | if ($candidates) {
44 | foreach (iterator_to_array($node->childNodes) as $k => $item) {
45 | if (!in_array($item, $candidates, true)) {
46 | $node->removeChild($item);
47 | }
48 | }
49 | }
50 |
51 | $context->compileChilds($node);
52 |
53 | $set = iterator_to_array($node->childNodes);
54 | if (count($set)) {
55 | $n = $node->ownerDocument->createTextNode("\n");
56 | array_unshift($set, $n);
57 | }
58 | $ext = $context->createControlNode("extends {$filename}");
59 | array_unshift($set, $ext);
60 |
61 | DOMHelper::replaceWithSet($node, $set);
62 | }
63 |
64 | /**
65 | * @param array $candidates
66 | * @param \DOMNode $node
67 | * @return array
68 | */
69 | private function convertBlockInnerIntoBlock(array $candidates, \DOMNode $node)
70 | {
71 | /**
72 | * @var $candidate \DOMElement
73 | */
74 | foreach ($candidates as $k => $candidate) {
75 | if ($candidate->hasAttributeNS(Twital::NS, "block-inner")) {
76 | $blockName = $candidate->getAttributeNS(Twital::NS, "block-inner");
77 |
78 | $block = $node->ownerDocument->createElementNS(Twital::NS, "block");
79 | $block->setAttribute("name", $blockName);
80 |
81 | $candidate->parentNode->insertBefore($block, $candidate);
82 |
83 | // move all child to the new block node
84 | while ($candidate->firstChild) {
85 | $child = $candidate->removeChild($candidate->firstChild);
86 | $block->appendChild($child);
87 | }
88 | $candidate->parentNode->removeChild($candidate);
89 | $candidates[$k] = $block;
90 | }
91 | }
92 |
93 | return $candidates;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Attribute/IfAttribute.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class IfAttribute implements AttributeBase
14 | {
15 | public function visit(\DOMAttr $att, Compiler $context)
16 | {
17 | $node = $att->ownerElement;
18 | $pi = $context->createControlNode("if " . html_entity_decode($att->value));
19 | $node->parentNode->insertBefore($pi, $node);
20 |
21 | if (!($nextElement = self::findNextElement($node)) || (!$nextElement->hasAttributeNS(Twital::NS, 'elseif') && !$nextElement->hasAttributeNS(Twital::NS, 'else'))) {
22 | $pi = $context->createControlNode("endif");
23 | $node->parentNode->insertBefore($pi, $node->nextSibling); // insert after
24 | } else {
25 | self::removeWhitespace($node);
26 | }
27 | $node->removeAttributeNode($att);
28 | }
29 |
30 | public static function removeWhitespace(\DOMElement $element)
31 | {
32 | while ($el = $element->nextSibling) {
33 | if ($el instanceof \DOMText) {
34 | $element->parentNode->removeChild($el);
35 | } else {
36 | break;
37 | }
38 | }
39 | }
40 |
41 | public static function findNextElement(\DOMElement $element)
42 | {
43 | $next = $element;
44 | while ($next = $next->nextSibling) {
45 | if ($next instanceof \DOMText && trim($next->textContent)) {
46 | return null;
47 | }
48 | if ($next instanceof \DOMElement) {
49 | return $next;
50 | }
51 | }
52 |
53 | return null;
54 | }
55 |
56 | public static function findPrevElement(\DOMElement $element)
57 | {
58 | $prev = $element;
59 | while ($prev = $prev->previousSibling) {
60 | if ($prev instanceof \DOMText && trim($prev->textContent)) {
61 | return null;
62 | }
63 | if ($prev instanceof \DOMElement) {
64 | return $prev;
65 | }
66 | }
67 |
68 | return null;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Attribute/InternalIDAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class InternalIDAttribute implements AttributeBase
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $att->ownerElement->removeAttributeNode($att);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Attribute/OmitAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class OmitAttribute implements Attribute
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 |
18 | $pi = $context->createControlNode("set __tmp_omit = " . html_entity_decode($att->value));
19 | $node->parentNode->insertBefore($pi, $node);
20 |
21 | $pi = $context->createControlNode("if not __tmp_omit");
22 | $node->parentNode->insertBefore($pi, $node);
23 |
24 | $pi = $context->createControlNode("endif");
25 | if ($node->firstChild) {
26 | $node->insertBefore($pi, $node->firstChild);
27 | } else {
28 | $node->appendChild($pi);
29 | }
30 |
31 | $pi = $context->createControlNode("if not __tmp_omit");
32 | $node->appendChild($pi);
33 |
34 | $pi = $context->createControlNode("endif");
35 |
36 | if ($node->parentNode->nextSibling) {
37 | $node->parentNode->insertBefore($pi, $node->parentNode->nextSibling);
38 | } else {
39 | $node->parentNode->appendChild($pi);
40 | }
41 |
42 | $node->removeAttributeNode($att);
43 |
44 | if ($att->value == "true" || $att->value == "1") {
45 | foreach (iterator_to_array($node->attributes) as $att) {
46 | $node->removeAttributeNode($att);
47 | }
48 | }
49 |
50 | return Attribute::STOP_ATTRIBUTE;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Attribute/ReplaceAttribute.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class ReplaceAttribute implements Attribute
13 | {
14 | public function visit(\DOMAttr $att, Compiler $context)
15 | {
16 | $node = $att->ownerElement;
17 | $pi = $context->createPrintNode(html_entity_decode($att->value));
18 |
19 | $node->parentNode->replaceChild($pi, $node);
20 |
21 | $node->removeAttributeNode($att);
22 | return Attribute::STOP_NODE;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Attribute/SetAttribute.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class SetAttribute implements Attribute
14 | {
15 | public function visit(\DOMAttr $att, Compiler $context)
16 | {
17 | $node = $att->ownerElement;
18 |
19 | $sets = ParserHelper::staticSplitExpression(html_entity_decode($att->value), ",");
20 | foreach ($sets as $set) {
21 | $pi = $context->createControlNode("set " . $set);
22 | $node->parentNode->insertBefore($pi, $node);
23 | }
24 |
25 | $node->removeAttributeNode($att);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Compiler.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | */
9 | class Compiler
10 | {
11 | /**
12 | *
13 | * @var array
14 | */
15 | protected $lexerOptions;
16 |
17 | /**
18 | *
19 | * @var \DOMDocument
20 | */
21 | protected $document;
22 |
23 | /**
24 | *
25 | * @var Twital
26 | */
27 | protected $twital;
28 |
29 | public function __construct(Twital $twital, array $lexerOptions = array())
30 | {
31 | $this->twital = $twital;
32 |
33 | $this->lexerOptions = array_merge(array(
34 | 'tag_block' => array(
35 | '{%',
36 | '%}'
37 | ),
38 | 'tag_variable' => array(
39 | '{{',
40 | '}}'
41 | )
42 | ), $lexerOptions);
43 | }
44 |
45 | /**
46 | *
47 | * @return \DOMDocument
48 | */
49 | public function getDocument()
50 | {
51 | return $this->document;
52 | }
53 |
54 | /**
55 | *
56 | * @param string $content
57 | * @return \DOMCDATASection
58 | */
59 | public function createPrintNode($content)
60 | {
61 | $printPart = $this->getLexerOption('tag_variable');
62 |
63 | return $this->document->createCDATASection("__[__{$printPart[0]} {$content} {$printPart[1]}__]__");
64 | }
65 |
66 | /**
67 | *
68 | * @param string $content
69 | * @return \DOMCDATASection
70 | */
71 | public function createControlNode($content)
72 | {
73 | $printPart = $this->getLexerOption('tag_block');
74 |
75 | return $this->document->createCDATASection("__[__{$printPart[0]} " . $content . " {$printPart[1]}__]__");
76 | }
77 |
78 | /**
79 | * @param \DOMDocument $doc
80 | * @return void
81 | */
82 | public function compile(\DOMDocument $doc)
83 | {
84 | $this->document = $doc;
85 | $this->compileChilds($doc);
86 | }
87 |
88 | public function compileElement(\DOMElement $node)
89 | {
90 | $nodes = $this->twital->getNodes();
91 | if (isset($nodes[$node->namespaceURI][$node->localName])) {
92 | $nodes[$node->namespaceURI][$node->localName]->visit($node, $this);
93 | } elseif (isset($nodes[$node->namespaceURI]['__base__'])) {
94 | $nodes[$node->namespaceURI]['__base__']->visit($node, $this);
95 | } else {
96 | if ($node->namespaceURI === Twital::NS) {
97 | throw new Exception("Can't handle the {$node->namespaceURI}#{$node->localName} node at line " . $node->getLineNo());
98 | }
99 | if ($this->compileAttributes($node)) {
100 | $this->compileChilds($node);
101 | }
102 | }
103 | }
104 |
105 | public function compileAttributes(\DOMNode $node)
106 | {
107 | $attributes = $this->twital->getAttributes();
108 | $continueNode = true;
109 | foreach (iterator_to_array($node->attributes) as $attr) {
110 | if (!$attr->ownerElement) {
111 | continue;
112 | } elseif (isset($attributes[$attr->namespaceURI][$attr->localName])) {
113 | $attPlugin = $attributes[$attr->namespaceURI][$attr->localName];
114 | } elseif (isset($attributes[$attr->namespaceURI]['__base__'])) {
115 | $attPlugin = $attributes[$attr->namespaceURI]['__base__'];
116 | } elseif ($attr->namespaceURI === Twital::NS) {
117 | throw new Exception("Can't handle the {$attr->namespaceURI}#{$attr->localName} attribute on {$node->namespaceURI}#{$node->localName} node at line " . $attr->getLineNo());
118 | } else {
119 | continue;
120 | }
121 |
122 | $return = $attPlugin->visit($attr, $this);
123 | if ($return !== null) {
124 | $continueNode = $continueNode && !($return & Attribute::STOP_NODE);
125 | if ($return & Attribute::STOP_ATTRIBUTE) {
126 | break;
127 | }
128 | }
129 | }
130 |
131 | return $continueNode;
132 | }
133 |
134 | public function compileChilds(\DOMNode $node)
135 | {
136 | foreach (iterator_to_array($node->childNodes) as $child) {
137 | if ($child instanceof \DOMElement) {
138 | $this->compileElement($child);
139 | }
140 | }
141 | }
142 |
143 | private function getLexerOption($name)
144 | {
145 | return $this->lexerOptions[$name];
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/EventDispatcher/AbstractEvent.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | class SourceEvent extends AbstractEvent
12 | {
13 | /**
14 | *
15 | * @var Twital
16 | */
17 | protected $twital;
18 | /**
19 | *
20 | * @var string
21 | */
22 | protected $template;
23 |
24 | public function __construct(Twital $twital, $template)
25 | {
26 | $this->twital = $twital;
27 | $this->template = $template;
28 | }
29 |
30 | /**
31 | * @return \Goetas\Twital\Twital
32 | */
33 | public function getTwital()
34 | {
35 | return $this->twital;
36 | }
37 |
38 | /**
39 | * @return string
40 | */
41 | public function getTemplate()
42 | {
43 | return $this->template;
44 | }
45 |
46 | /**
47 | * @param string $template
48 | * @return void
49 | */
50 | public function setTemplate($template)
51 | {
52 | $this->template = $template;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/EventDispatcher/TemplateEvent.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class TemplateEvent extends AbstractEvent
13 | {
14 | /**
15 | *
16 | * @var Twital
17 | */
18 | protected $twital;
19 | /**
20 | *
21 | * @var Template
22 | */
23 | protected $template;
24 |
25 | public function __construct(Twital $twital, Template $template)
26 | {
27 | $this->twital = $twital;
28 | $this->template = $template;
29 | }
30 |
31 | /**
32 | * @return \Goetas\Twital\Twital
33 | */
34 | public function getTwital()
35 | {
36 | return $this->twital;
37 | }
38 |
39 | /**
40 | * @return \Goetas\Twital\Template
41 | */
42 | public function getTemplate()
43 | {
44 | return $this->template;
45 | }
46 |
47 | /**
48 | * @param \Goetas\Twital\Template $template
49 | * @return void
50 | */
51 | public function setTemplate(Template $template)
52 | {
53 | $this->template = $template;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/EventSubscriber/AbstractTwigExpressionSubscriber.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | abstract class AbstractTwigExpressionSubscriber implements EventSubscriberInterface
12 | {
13 | const REGEX_STRING = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
14 |
15 | protected $placeholderFormat = '';
16 | protected $options = array();
17 | protected $regexes = array();
18 |
19 | public function __construct(array $placeholder = array('[_TWITAL_[', ']_TWITAL_]'), array $options = array())
20 | {
21 | $this->placeholderFormat = $placeholder[0] . '%s' . $placeholder[1];
22 |
23 | $this->options = array_merge(array(
24 | 'tag_block' => array('{%', '%}'),
25 | 'tag_variable' => array('{{', '}}'),
26 | 'tag_comment' => array('{#', '#}'),
27 | ), $options);
28 |
29 | $this->regexes = array(
30 | 'twig_start' => '{('.preg_quote($this->options['tag_block'][0]).'|'.preg_quote($this->options['tag_variable'][0]).'|'.preg_quote($this->options['tag_comment'][0]).')}',
31 | 'placeholder' => '{('.preg_quote($placeholder[0]).'(.+)'.preg_quote($placeholder[1]).')}siuU',
32 | 'twig_inner_'.$this->options['tag_block'][0] => '{('.self::REGEX_STRING.'|([^"\']*?'.preg_quote($this->options['tag_block'][1]).')|[^"\']+?)}si',
33 | 'twig_inner_'.$this->options['tag_variable'][0] => '{('.self::REGEX_STRING.'|([^"\']*?'.preg_quote($this->options['tag_variable'][1]).')|[^"\']+?)}si',
34 | 'twig_inner_'.$this->options['tag_comment'][0] => '{((.*?'.preg_quote($this->options['tag_comment'][1]).'))}si',
35 | );
36 | }
37 |
38 | protected function processTwig($template, \CLosure $processor)
39 | {
40 | $offset = 0;
41 | while (preg_match($this->regexes['twig_start'], $template, $matches, PREG_OFFSET_CAPTURE, $offset)) {
42 | $twig = '';
43 | list($buffer, $from) = $matches[0];
44 | $offset = $from + strlen($buffer);
45 | $pattern = $this->regexes['twig_inner_' . $buffer];
46 | while (preg_match($pattern, $template, $inners, PREG_OFFSET_CAPTURE, $offset)) {
47 | $buffer .= $inners[0][0];
48 | $offset += strlen($inners[0][0]);
49 | if (isset($inners[2])) {
50 | $twig = $buffer;
51 | break;
52 | }
53 | }
54 |
55 | if (!$twig) {
56 | continue;
57 | }
58 |
59 | $replacement = $processor($twig, $template, $from);
60 | $template = substr_replace($template, $replacement, $from, $offset - $from);
61 | $offset = $from + strlen($replacement);
62 | }
63 |
64 | return $template;
65 | }
66 |
67 | protected function processPlaceholder($template, \Closure $processor)
68 | {
69 | return preg_replace_callback($this->regexes['placeholder'], $processor, $template);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/EventSubscriber/ContextAwareEscapingSubscriber.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class ContextAwareEscapingSubscriber implements EventSubscriberInterface
14 | {
15 | const REGEX_STRING = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
16 |
17 | protected $options = array();
18 | protected $placeholder = array();
19 |
20 | public function __construct(array $placeholder = array('[_TWITAL_[', ']_TWITAL_]'), array $options = array())
21 | {
22 | $this->placeholder = array(
23 | '[_TWITAL_[',
24 | ']_TWITAL_]'
25 | );
26 |
27 | $this->options = array_merge(array(
28 | 'tag_block' => array('{%', '%}'),
29 | 'tag_variable' => array('{{', '}}'),
30 | ), $options);
31 | }
32 |
33 | public static function getSubscribedEvents()
34 | {
35 | return array(
36 | CompilerEvents::PRE_DUMP => 'addEscaping'
37 | );
38 | }
39 |
40 | public function addEscaping(TemplateEvent $event)
41 | {
42 | $doc = $event->getTemplate()->getDocument();
43 |
44 | $xp = new \DOMXPath($doc);
45 | $xp->registerNamespace("xh", "http://www.w3.org/1999/xhtml");
46 |
47 | $this->escapeScript($doc, $xp);
48 | $this->escapeStyle($doc, $xp);
49 | $this->escapeUrls($doc, $xp);
50 | }
51 |
52 | /**
53 | *
54 | * Used only to achieve HHVM compatibility. Sett https://github.com/facebook/hhvm/issues/2810
55 | */
56 | private function xpathQuery(\DOMXPath $xp, $expression, \DOMNode $contextnode = null, $registerNodeNS = true)
57 | {
58 | if (defined('HHVM_VERSION') && HHVM_VERSION_ID < 30500) {
59 | return $xp->query($expression, $contextnode);
60 | } else {
61 | return $xp->query($expression, $contextnode, $registerNodeNS);
62 | }
63 | }
64 |
65 | private function escapeUrls(\DOMDocument $doc, \DOMXPath $xp)
66 | {
67 | $regex = '{' . preg_quote($this->options['tag_variable'][0]) . '((' . self::REGEX_STRING . '|[^"\']*)+)' . preg_quote($this->options['tag_variable'][1]) . '}siuU';
68 |
69 | // special attr escaping
70 | $res = $this->xpathQuery($xp, "(//xh:*/@href|//xh:*/@src)[contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
71 | foreach ($res as $node) {
72 |
73 | // if the twig variable is at the beginning of attribute, we should skip it
74 | if (preg_match('{^' . preg_quote($this->options['tag_variable'][0]) . '((' . self::REGEX_STRING . '|[^"\']*)+)' . preg_quote($this->options['tag_variable'][1]) . '}siuU', str_replace($this->placeholder, '', $node->value))) {
75 | continue;
76 | }
77 |
78 | if (substr($node->value, 0, 11) == "javascript:" && $node->name == "href") {
79 | $newValue = preg_replace($regex, "{$this->options['tag_variable'][0]} (\\1) | escape('js') {$this->options['tag_variable'][1]}", $node->value);
80 | } else {
81 | $newValue = preg_replace($regex, "{$this->options['tag_variable'][0]} (\\1) | escape('url') {$this->options['tag_variable'][1]}", $node->value);
82 | }
83 |
84 | $node->value = htmlspecialchars($newValue, ENT_COMPAT, 'UTF-8');
85 | }
86 | }
87 |
88 | private function escapeStyle(\DOMDocument $doc, \DOMXPath $xp)
89 | {
90 | /**
91 | * @var \DOMNode[] $res
92 | */
93 | $res = $this->xpathQuery($xp, "//xh:style[not(@type) or @type = 'text/css'][contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
94 |
95 | foreach ($res as $node) {
96 | $node->insertBefore($doc->createTextNode("{$this->options['tag_block'][0]} autoescape 'css' {$this->options['tag_block'][1]}"), $node->firstChild);
97 | $node->appendChild($doc->createTextNode("{$this->options['tag_block'][0]} endautoescape {$this->options['tag_block'][1]}"));
98 | }
99 | }
100 |
101 | private function escapeScript(\DOMDocument $doc, \DOMXPath $xp)
102 | {
103 | /**
104 | * @var \DOMNode[] $res
105 | */
106 | $res = $this->xpathQuery($xp, "//xh:script[not(@type) or @type = 'text/javascript'][contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
107 | foreach ($res as $node) {
108 | $node->insertBefore($doc->createTextNode("{$this->options['tag_block'][0]} autoescape 'js' {$this->options['tag_block'][1]}"), $node->firstChild);
109 | $node->appendChild($doc->createTextNode("{$this->options['tag_block'][0]} endautoescape {$this->options['tag_block'][1]}"));
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/EventSubscriber/CustomNamespaceRawSubscriber.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class CustomNamespaceRawSubscriber implements EventSubscriberInterface
14 | {
15 | protected $customNamespaces = array();
16 |
17 | public function __construct(array $customNamespaces)
18 | {
19 | $this->customNamespaces = $customNamespaces;
20 | }
21 |
22 | public static function getSubscribedEvents()
23 | {
24 | return array(
25 | CompilerEvents::PRE_LOAD => 'addCustomNamespace',
26 | CompilerEvents::POST_DUMP => 'removeCustomNamespaces',
27 | );
28 | }
29 |
30 | public function addCustomNamespace(SourceEvent $event)
31 | {
32 | $xml = $event->getTemplate();
33 | $mch = null;
34 | if (preg_match('~<(([a-z0-9\-_]+):)?([a-z0-9\-_]+)~i', $xml, $mch, PREG_OFFSET_CAPTURE)) {
35 | $addPos = $mch[0][1] + strlen($mch[0][0]);
36 | foreach ($this->customNamespaces as $prefix => $ns) {
37 | if (!preg_match('/\sxmlns:([a-z0-9\-]+)="' . preg_quote($ns, '/') . '"/', $xml) && !preg_match('/\sxmlns:([a-z0-9\-]+)=".*?"/', $xml)) {
38 | $xml = substr_replace($xml, ' xmlns:' . $prefix . '="' . $ns . '"', $addPos, 0);
39 | }
40 | }
41 |
42 | $event->setTemplate($xml);
43 | }
44 | }
45 |
46 | public function removeCustomNamespaces(SourceEvent $event)
47 | {
48 | $template = $event->getTemplate();
49 | foreach ($this->customNamespaces as $prefix => $ns) {
50 | $template = preg_replace('#<(.*) xmlns:' . $prefix . '="' . $ns . '"(.*)>#mi', "<\\1\\2>", $template);
51 | }
52 | $event->setTemplate($template);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/EventSubscriber/CustomNamespaceSubscriber.php:
--------------------------------------------------------------------------------
1 |
13 | *
14 | */
15 | class CustomNamespaceSubscriber implements EventSubscriberInterface
16 | {
17 | protected $customNamespaces = array();
18 |
19 | public function __construct(array $customNamespaces)
20 | {
21 | $this->customNamespaces = $customNamespaces;
22 | }
23 |
24 | public static function getSubscribedEvents()
25 | {
26 | return array(
27 | CompilerEvents::POST_LOAD => 'addCustomNamespace',
28 | CompilerEvents::POST_DUMP => 'removeCustomNamespaces',
29 | );
30 | }
31 |
32 | public function addCustomNamespace(TemplateEvent $event)
33 | {
34 | foreach (iterator_to_array($event->getTemplate()->getDocument()->childNodes) as $child) {
35 | if ($child instanceof \DOMElement) {
36 | DOMHelper::checkNamespaces($child, $this->customNamespaces);
37 | }
38 | }
39 | }
40 |
41 | public function removeCustomNamespaces(SourceEvent $event)
42 | {
43 | $template = $event->getTemplate();
44 | foreach ($this->customNamespaces as $prefix => $ns) {
45 | $template = preg_replace('#<(.*) xmlns:' . $prefix . '="' . $ns . '"(.*)>#mi', "<\\1\\2>", $template);
46 | }
47 | $event->setTemplate($template);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/EventSubscriber/DOMMessSubscriber.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class DOMMessSubscriber implements EventSubscriberInterface
14 | {
15 | public static function getSubscribedEvents()
16 | {
17 | return array(
18 | CompilerEvents::POST_DUMP => array(
19 | array(
20 | 'removeCdata'
21 | ),
22 | array(
23 | 'fixAttributes'
24 | )
25 | )
26 | );
27 | }
28 |
29 | public function removeCdata(SourceEvent $event)
30 | {
31 | $event->setTemplate(str_replace(array(
32 | ""
34 | ), "", $event->getTemplate()));
35 | }
36 |
37 | public function fixAttributes(SourceEvent $event)
38 | {
39 | $event->setTemplate(preg_replace_callback('/ __attr__="(__a[0-9a-f]+)"/', function ($mch) {
40 | return '{% for ____ak,____av in ' . $mch[1] . ' %}{% if (____av|length > 0) and not (____av|length == 1 and ____av[0] is same as(false)) %} {{____ak|raw}}{% if ____av|length > 1 or ____av[0] is not same as(true) %}="{{ ____av|join(\'\') }}"{% endif %}{% endif %}{% endfor %}';
41 | }, $event->getTemplate()));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/EventSubscriber/FixHtmlEntitiesInExpressionSubscriber.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class FixHtmlEntitiesInExpressionSubscriber extends AbstractTwigExpressionSubscriber
13 | {
14 | public static function getSubscribedEvents()
15 | {
16 | return array(
17 | CompilerEvents::PRE_LOAD => 'addPlaceholder',
18 | CompilerEvents::POST_DUMP => 'removePlaceholder',
19 | );
20 | }
21 |
22 | /**
23 | *
24 | * @param SourceEvent $event
25 | */
26 | public function addPlaceholder(SourceEvent $event)
27 | {
28 | $source = $event->getTemplate();
29 | $format = $this->placeholderFormat;
30 |
31 | $source = $this->processTwig($source, function ($twig) use ($format) {
32 | return sprintf($format, htmlspecialchars($twig, ENT_COMPAT, 'UTF-8'));
33 | });
34 |
35 | $event->setTemplate($source);
36 | }
37 |
38 | /**
39 | *
40 | * @param SourceEvent $event
41 | */
42 | public function removePlaceholder(SourceEvent $event)
43 | {
44 | $source = $event->getTemplate();
45 |
46 | $source = $this->processPlaceholder($source, function ($matches) {
47 | return html_entity_decode($matches[2], ENT_COMPAT, 'UTF-8');
48 | });
49 |
50 | $event->setTemplate($source);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/EventSubscriber/FixTwigExpressionSubscriber.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class FixTwigExpressionSubscriber extends AbstractTwigExpressionSubscriber
13 | {
14 | protected $placeholders = array();
15 |
16 | public function __construct($placeholder = array('twital', 'twital'), array $options = array())
17 | {
18 | parent::__construct($placeholder, $options);
19 |
20 | $this->regexes = array_merge($this->regexes, array(
21 | 'placeholder' => '{( ?)(' . preg_quote($placeholder[0]) . '[a-z0-9]+?' . preg_quote($placeholder[1]) . ')}iu',
22 | ));
23 | }
24 |
25 | public static function getSubscribedEvents()
26 | {
27 | return array(
28 | CompilerEvents::PRE_LOAD => array('addPlaceholder', 128),
29 | CompilerEvents::POST_DUMP => array('removePlaceholder', -128),
30 | );
31 | }
32 |
33 | /**
34 | *
35 | * @param SourceEvent $event
36 | */
37 | public function addPlaceholder(SourceEvent $event)
38 | {
39 | $source = $event->getTemplate();
40 | $format = $this->placeholderFormat;
41 | $placeholders = array();
42 |
43 | $source = $this->processTwig($source, function ($twig, $source, $offset) use ($format, &$placeholders) {
44 | $before = $offset > 0 ? $source[$offset - 1] : '';
45 | $id = ('<' === $before || '/' === $before) ? $twig : mt_rand();
46 | $placeholder = sprintf($format, md5($id));
47 |
48 | if (!in_array($before, array(' ', '<', '>', '/'), true)) {
49 | $placeholder = ' ' . $placeholder;
50 | }
51 |
52 | $placeholders[$placeholder] = $twig;
53 |
54 | return $placeholder;
55 | });
56 |
57 | $this->placeholders = $placeholders;
58 |
59 | $event->setTemplate($source);
60 | }
61 |
62 | /**
63 | *
64 | * @param SourceEvent $event
65 | */
66 | public function removePlaceholder(SourceEvent $event)
67 | {
68 | $source = $event->getTemplate();
69 |
70 | $placeholders = $this->placeholders;
71 |
72 | $source = $this->processPlaceholder($source, function ($matches) use ($placeholders) {
73 | if (isset($placeholders[$matches[0]])) {
74 | return $placeholders[$matches[0]];
75 | } elseif (isset($placeholders[$matches[2]])) {
76 | return $matches[1] . $placeholders[$matches[2]];
77 | } else {
78 | return $matches[0];
79 | }
80 | });
81 |
82 | $event->setTemplate($source);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/EventSubscriber/IDNodeSubscriber.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class IDNodeSubscriber implements EventSubscriberInterface
15 | {
16 | public static function getSubscribedEvents()
17 | {
18 | return array(
19 | CompilerEvents::POST_LOAD => array(
20 | array(
21 | 'addAttribute'
22 | )
23 | ),
24 | CompilerEvents::PRE_DUMP => array(
25 | array(
26 | 'removeAttribute'
27 | )
28 | )
29 | );
30 | }
31 |
32 | public function addAttribute(TemplateEvent $event)
33 | {
34 | $doc = $event->getTemplate()->getDocument();
35 | $xp = new \DOMXPath($doc);
36 | /**
37 | * @var \DOMElement[] $nodes
38 | */
39 | $nodes = $xp->query("//*[@*[namespace-uri()='" . Twital::NS . "']]");
40 | foreach ($nodes as $node) {
41 | $node->setAttributeNS(Twital::NS, '__internal-id__', microtime(1) . mt_rand());
42 | }
43 | }
44 |
45 | public function removeAttribute(TemplateEvent $event)
46 | {
47 | $doc = $event->getTemplate()->getDocument();
48 | $xp = new \DOMXPath($doc);
49 | $xp->registerNamespace('twital', Twital::NS);
50 | $attributes = $xp->query("//@twital:__internal-id__");
51 | foreach ($attributes as $attribute) {
52 | $attribute->ownerElement->removeAttributeNode($attribute);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/EventSubscriber/ReplaceDoctypeAsTwigExpressionSubscriber.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class ReplaceDoctypeAsTwigExpressionSubscriber implements EventSubscriberInterface
15 | {
16 | public static function getSubscribedEvents()
17 | {
18 | return array(
19 | CompilerEvents::PRE_LOAD => array('replaceDoctype', 130),
20 | );
21 | }
22 |
23 | /**
24 | *
25 | * @param SourceEvent $event
26 | */
27 | public function replaceDoctype(SourceEvent $event)
28 | {
29 | $source = $event->getTemplate();
30 |
31 | $source = preg_replace_callback('/^/im', function ($mch) {
32 | return '{{ \'' . addslashes($mch[0]) . '\' }}';
33 | }, $source);
34 |
35 | $event->setTemplate($source);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Exception.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | */
9 | class Exception extends \Exception
10 | {
11 | }
12 |
--------------------------------------------------------------------------------
/src/Extension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | */
9 | interface Extension
10 | {
11 | /**
12 | * Array of objects implementing Node interface, responsible of DOM node handling.
13 | * The returned array must be a "two level array",
14 | * first level as namespace and second level as attribute name.
15 | * Example:
16 | *
17 | * array(
18 | * 'http://www.w3.org/1998/Math/MathML' => array(
19 | * 'math'=> new MathML\MathAttribute()
20 | * )
21 | * )
22 | *
23 | *
24 | * @return array
25 | */
26 | public function getAttributes();
27 |
28 | /**
29 | * Array of objects implementing Node interface, responsible of DOM node handling.
30 | * The returned array must be a "two level array",
31 | * first level as namespace and second level as attribute name.
32 | * Example:
33 | *
34 | * array(
35 | * 'http://www.w3.org/1998/Math/MathML' => array(
36 | * 'math'=> new MathML\MathNode()
37 | * )
38 | * )
39 | *
40 | *
41 | * @return array
42 | */
43 | public function getNodes();
44 |
45 | /**
46 | * Array of event subscribers
47 | * @return array
48 | */
49 | public function getSubscribers();
50 | }
51 |
--------------------------------------------------------------------------------
/src/Extension/AbstractExtension.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | abstract class AbstractExtension implements Extension
12 | {
13 | public function getAttributes()
14 | {
15 | return array();
16 | }
17 |
18 | public function getNodes()
19 | {
20 | return array();
21 | }
22 |
23 | public function getSubscribers()
24 | {
25 | return array();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Extension/CoreExtension.php:
--------------------------------------------------------------------------------
1 |
16 | *
17 | */
18 | class CoreExtension extends AbstractExtension
19 | {
20 | public function getSubscribers()
21 | {
22 | return array(
23 | new DOMMessSubscriber(),
24 | new CustomNamespaceRawSubscriber(array(
25 | 't' => Twital::NS
26 | )),
27 | new FixHtmlEntitiesInExpressionSubscriber(),
28 | new ContextAwareEscapingSubscriber(),
29 | new IDNodeSubscriber()
30 | );
31 | }
32 |
33 | public function getAttributes()
34 | {
35 | $attributes = array();
36 | $attributes[Twital::NS]['__base__'] = new Attribute\BaseAttribute();
37 | $attributes[Twital::NS]['__internal-id__'] = new Attribute\InternalIDAttribute();
38 |
39 | $attributes[Twital::NS]['if'] = new Attribute\IfAttribute();
40 | $attributes[Twital::NS]['elseif'] = new Attribute\ElseIfAttribute();
41 | $attributes[Twital::NS]['else'] = new Attribute\ElseAttribute();
42 |
43 | $attributes[Twital::NS]['omit'] = new Attribute\OmitAttribute();
44 | $attributes[Twital::NS]['set'] = new Attribute\SetAttribute();
45 |
46 | $attributes[Twital::NS]['content'] = new Attribute\ContentAttribute();
47 | $attributes[Twital::NS]['capture'] = new Attribute\CaptureAttribute();
48 | $attributes[Twital::NS]['replace'] = new Attribute\ReplaceAttribute();
49 |
50 | $attributes[Twital::NS]['attr'] = new Attribute\AttrAttribute();
51 | $attributes[Twital::NS]['attr-append'] = new Attribute\AttrAppendAttribute();
52 |
53 | $attributes[Twital::NS]['extends'] = new Attribute\ExtendsAttribute();
54 |
55 | $attributes[Twital::NS]['block'] = new Attribute\BlockInnerAttribute();
56 | $attributes[Twital::NS]['block-inner'] = new Attribute\BlockInnerAttribute();
57 | $attributes[Twital::NS]['block-outer'] = new Attribute\BlockOuterAttribute();
58 |
59 | return $attributes;
60 | }
61 |
62 | public function getNodes()
63 | {
64 | $nodes = array();
65 | $nodes[Twital::NS]['extends'] = new Node\ExtendsNode();
66 | $nodes[Twital::NS]['block'] = new Node\BlockNode();
67 | $nodes[Twital::NS]['macro'] = new Node\MacroNode();
68 | $nodes[Twital::NS]['import'] = new Node\ImportNode();
69 | $nodes[Twital::NS]['include'] = new Node\IncludeNode();
70 | $nodes[Twital::NS]['omit'] = new Node\OmitNode();
71 | $nodes[Twital::NS]['embed'] = new Node\EmbedNode();
72 | $nodes[Twital::NS]['use'] = new Node\UseNode();
73 |
74 | return $nodes;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Extension/FullCompatibilityTwigExtension.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class FullCompatibilityTwigExtension extends AbstractExtension
15 | {
16 | public function getSubscribers()
17 | {
18 | return array(
19 | new ReplaceDoctypeAsTwigExpressionSubscriber(),
20 | new FixTwigExpressionSubscriber(),
21 | new CustomNamespaceSubscriber(array(
22 | 't' => Twital::NS
23 | )),
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Helper/DOMHelper.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | */
9 | class DOMHelper
10 | {
11 | public static function removeChilds(\DOMNode $ref)
12 | {
13 | while ($ref->hasChildNodes()) {
14 | $ref->removeChild($ref->firstChild);
15 | }
16 | }
17 |
18 | public static function insertAfterSet(\DOMNode $node, array $newNodes)
19 | {
20 | $ref = $node;
21 | foreach ($newNodes as $newNode) {
22 | if ($newNode->parentNode) {
23 | $newNode->parentNode->removeChild($newNode);
24 | }
25 | $ref->parentNode->insertBefore($newNode, $ref->nextSibling);
26 | $ref = $newNode;
27 | }
28 | }
29 |
30 | public static function replaceWithSet(\DOMNode $node, array $newNodes)
31 | {
32 | self::insertAfterSet($node, $newNodes);
33 | $node->parentNode->removeChild($node);
34 | }
35 |
36 | public static function remove(\DOMNode $ref)
37 | {
38 | return $ref->parentNode->removeChild($ref);
39 | }
40 |
41 | public static function checkNamespaces(\DOMElement $element, array $namespaces = array())
42 | {
43 | if ($element->namespaceURI === null && preg_match('/^([a-z0-9\-]+):(.+)$/i', $element->nodeName, $mch) && isset($namespaces[$mch[1]])) {
44 | $oldElement = $element;
45 | $element = self::copyElementInNs($oldElement, $namespaces[$mch[1]]);
46 | }
47 | // fix attrs
48 | foreach (iterator_to_array($element->attributes) as $attr) {
49 | if ($attr->namespaceURI === null && preg_match('/^([a-z0-9\-]+):/i', $attr->name, $mch) && isset($namespaces[$mch[1]])) {
50 | $element->removeAttributeNode($attr);
51 | $element->setAttributeNS($namespaces[$mch[1]], $attr->name, $attr->value);
52 | }
53 | }
54 | foreach (iterator_to_array($element->childNodes) as $child) {
55 | if ($child instanceof \DOMElement) {
56 | self::checkNamespaces($child, $namespaces);
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * @param \DOMElement $oldElement
63 | * @param string $newNamespace
64 | * @return mixed
65 | */
66 | public static function copyElementInNs($oldElement, $newNamespace)
67 | {
68 | $element = $oldElement->ownerDocument->createElementNS($newNamespace, $oldElement->nodeName);
69 |
70 | // copy attributes
71 | foreach (iterator_to_array($oldElement->attributes) as $attr) {
72 | $oldElement->removeAttributeNode($attr);
73 | if ($attr->namespaceURI) {
74 | $element->setAttributeNodeNS($attr);
75 | } else {
76 | $element->setAttributeNode($attr);
77 | }
78 | }
79 | // copy children
80 | while ($child = $oldElement->firstChild) {
81 | $oldElement->removeChild($child);
82 | $element->appendChild($child);
83 | }
84 | $oldElement->parentNode->replaceChild($element, $oldElement);
85 |
86 | return $element;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Helper/ParserHelper.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | class ParserHelper
12 | {
13 | private static $closing = array(
14 | '}' => '{',
15 | ')' => '(',
16 | ']' => '['
17 | );
18 |
19 | public static function staticSplitExpression($str, $splitter, $limit = 0)
20 | {
21 | $in = array();
22 | $inApex = false;
23 | $parts = array();
24 | $prev = 0;
25 |
26 | for ($i = 0, $l = strlen($str); $i < $l; $i++) {
27 | $chr = $str[$i];
28 |
29 | if ($chr == "'" || $chr == '"') {
30 | $j = 1;
31 | while ($i >= $j && $str[$i - $j] === '\\') {
32 | $j++;
33 | }
34 |
35 | if ($j % 2 !== 0) {
36 | if (!$inApex) {
37 | $inApex = $chr;
38 | } elseif ($inApex === $chr) {
39 | $inApex = false;
40 | }
41 | }
42 | }
43 |
44 | if (!$inApex) {
45 | if (in_array($chr, self::$closing)) {
46 | array_push($in, $chr);
47 | } elseif (isset(self::$closing[$chr]) && self::$closing[$chr] === end($in)) {
48 | array_pop($in);
49 | } elseif (isset(self::$closing[$chr]) && !count($in)) {
50 | throw new Exception(sprintf('Unexpected "%s" next to "%s"', $chr, substr($str, 0, $i + 1)));
51 | }
52 |
53 | if (!count($in) && $chr === $splitter) {
54 | $parts[] = substr($str, $prev, $i - $prev);
55 | $prev = $i + 1;
56 | if ($limit > 1 && count($parts) == ($limit - 1)) {
57 | break;
58 | }
59 | }
60 | }
61 | }
62 | if ($inApex) {
63 | throw new Exception(sprintf('Can\'t find the closing "%s" in "%s" expression', $inApex, $str));
64 | } elseif (count($in)) {
65 | throw new Exception(sprintf('Can\'t find the closing braces for "%s" in "%s" expression', implode(',', $in), $str));
66 | }
67 |
68 | $parts[] = substr($str, $prev);
69 |
70 | return array_map('trim', $parts);
71 | }
72 |
73 | public static function implodeKeyedDouble($glue, array $array, $quoteKeys = false)
74 | {
75 | $a = array();
76 | foreach ($array as $key => $val) {
77 | $a[] = ($quoteKeys ? "'$key'" : $key) . ":[" . implode(",", $val) . "]";
78 | }
79 |
80 | return implode($glue, $a);
81 | }
82 |
83 | public static function implodeKeyed($glue, array $array, $quoteKeys = false)
84 | {
85 | $a = array();
86 | foreach ($array as $key => $val) {
87 | $a[] = ($quoteKeys ? "'$key'" : $key) . ":$val";
88 | }
89 |
90 | return implode($glue, $a);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Node.php:
--------------------------------------------------------------------------------
1 |
8 | *
9 | */
10 | interface Node
11 | {
12 | /**
13 | * Visit a node.
14 | *
15 | * @param \DOMElement $node
16 | * @param Compiler $context
17 | * @return void
18 | */
19 | public function visit(\DOMElement $node, Compiler $context);
20 | }
21 |
--------------------------------------------------------------------------------
/src/Node/BlockNode.php:
--------------------------------------------------------------------------------
1 |
13 | *
14 | */
15 | class BlockNode implements Node
16 | {
17 | public function visit(\DOMElement $node, Compiler $context)
18 | {
19 | if (!$node->hasAttribute("name")) {
20 | throw new Exception("Name attribute is required");
21 | }
22 |
23 | $sandbox = $node->ownerDocument->createElementNS(Twital::NS, "sandbox");
24 | $node->parentNode->insertBefore($sandbox, $node);
25 | $node->parentNode->removeChild($node);
26 | $sandbox->appendChild($node);
27 |
28 | $context->compileAttributes($node);
29 | $context->compileChilds($node);
30 |
31 | $start = $context->createControlNode("block " . $node->getAttribute("name"));
32 | $end = $context->createControlNode("endblock");
33 |
34 | $sandbox->insertBefore($start, $sandbox->firstChild);
35 | $sandbox->appendChild($end);
36 |
37 | DOMHelper::replaceWithSet($sandbox, iterator_to_array($sandbox->childNodes));
38 | DOMHelper::replaceWithSet($node, iterator_to_array($node->childNodes));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Node/EmbedNode.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class EmbedNode implements Node
15 | {
16 | public function visit(\DOMElement $node, Compiler $context)
17 | {
18 | if ($node->hasAttribute("from-exp")) {
19 | $filename = $node->getAttribute("from-exp");
20 | } elseif ($node->hasAttribute("from")) {
21 | $filename = '"' . $node->getAttribute("from") . '"';
22 | } else {
23 | throw new Exception("The 'from' or 'from-exp' attribute is required");
24 | }
25 |
26 | // remove any non-element node
27 | foreach (iterator_to_array($node->childNodes) as $child) {
28 | if (!($child instanceof \DOMElement)) {
29 | $child->parentNode->removeChild($child);
30 | }
31 | }
32 |
33 | $context->compileChilds($node);
34 |
35 | $code = "embed {$filename}";
36 |
37 | if ($node->hasAttribute("ignore-missing") && $node->hasAttribute("ignore-missing") !== false) {
38 | $code .= " ignore missing";
39 | }
40 | if ($node->hasAttribute("with")) {
41 | $code .= " with " . $node->getAttribute("with");
42 | }
43 | if ($node->hasAttribute("only") && $node->getAttribute("only") !== "false") {
44 | $code .= " only";
45 | }
46 |
47 | $ext = $context->createControlNode($code);
48 |
49 | $set = iterator_to_array($node->childNodes);
50 |
51 | $n = $node->ownerDocument->createTextNode("\n");
52 | array_unshift($set, $n);
53 | array_unshift($set, $ext);
54 |
55 | $set[] = $context->createControlNode("endembed");
56 |
57 | DOMHelper::replaceWithSet($node, $set);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Node/ExtendsNode.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class ExtendsNode implements Node
15 | {
16 | public function visit(\DOMElement $node, Compiler $context)
17 | {
18 | if ($node->hasAttribute("from-exp")) {
19 | $filename = $node->getAttribute("from-exp");
20 | } elseif ($node->hasAttribute("from")) {
21 | $filename = '"' . $node->getAttribute("from") . '"';
22 | } else {
23 | throw new Exception("The 'from' or 'from-exp' attribute is required");
24 | }
25 |
26 | $context->compileChilds($node);
27 |
28 | $ext = $context->createControlNode("extends {$filename}");
29 |
30 | $set = iterator_to_array($node->childNodes);
31 | if (count($set)) {
32 | $n = $node->ownerDocument->createTextNode("\n");
33 | array_unshift($set, $n);
34 | }
35 | array_unshift($set, $ext);
36 |
37 | DOMHelper::replaceWithSet($node, $set);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Node/ImportNode.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class ImportNode implements Node
14 | {
15 | public function visit(\DOMElement $node, Compiler $context)
16 | {
17 | if ($node->hasAttribute("from-exp")) {
18 | $filename = $node->getAttribute("from-exp");
19 | } elseif ($node->hasAttribute("from")) {
20 | $filename = '"' . $node->getAttribute("from") . '"';
21 | } else {
22 | throw new Exception("The 'from' or 'from-exp' attribute is required");
23 | }
24 |
25 | if ($node->hasAttribute("as")) {
26 | $code = "import $filename as " . $node->getAttribute("as");
27 | $context->createControlNode("import " . ($node->getAttribute("fro-exp") ? $node->getAttribute("name-exp") : ("'" . $node->getAttribute("name") . "'")) . " as " . $node->getAttribute("as"));
28 | } elseif ($node->hasAttribute("aliases")) {
29 | $code = "from $filename import " . $node->getAttribute("aliases");
30 | } else {
31 | throw new Exception("As or Alias attribute is required");
32 | }
33 |
34 | $pi = $context->createControlNode($code);
35 |
36 | $node->parentNode->replaceChild($pi, $node);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Node/IncludeNode.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class IncludeNode implements Node
14 | {
15 | public function visit(\DOMElement $node, Compiler $context)
16 | {
17 | $code = "include ";
18 |
19 | if ($node->hasAttribute("from-exp")) {
20 | $code .= $node->getAttribute("from-exp");
21 | } elseif ($node->hasAttribute("from")) {
22 | $code .= '"' . $node->getAttribute("from") . '"';
23 | } else {
24 | throw new Exception("The 'from' or 'from-exp' attribute is required");
25 | }
26 |
27 | if ($node->hasAttribute("ignore-missing") && $node->getAttribute("ignore-missing") !== "false") {
28 | $code .= " ignore missing";
29 | }
30 | if ($node->hasAttribute("with")) {
31 | $code .= " with " . $node->getAttribute("with");
32 | }
33 | if ($node->hasAttribute("only") && $node->getAttribute("only") !== "false") {
34 | $code .= " only";
35 | }
36 | if ($node->hasAttribute("sandboxed") && $node->getAttribute("sandboxed") !== "false") {
37 | $code .= " sandboxed = true";
38 | }
39 |
40 | $pi = $context->createControlNode($code);
41 | $node->parentNode->replaceChild($pi, $node);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Node/MacroNode.php:
--------------------------------------------------------------------------------
1 |
12 | *
13 | */
14 | class MacroNode implements Node
15 | {
16 | public function visit(\DOMElement $node, Compiler $context)
17 | {
18 | if (!$node->hasAttribute("name")) {
19 | throw new Exception("Name attribute is required");
20 | }
21 |
22 | $context->compileChilds($node);
23 |
24 | $set = iterator_to_array($node->childNodes);
25 |
26 | $start = $context->createControlNode("macro " . $node->getAttribute("name") . "(" . $node->getAttribute("args") . ")");
27 | array_unshift($set, $start);
28 |
29 | $set[] = $context->createControlNode("endmacro");
30 |
31 | DOMHelper::replaceWithSet($node, $set);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Node/OmitNode.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class OmitNode implements Node
14 | {
15 | public function visit(\DOMElement $node, Compiler $context)
16 | {
17 | $context->compileAttributes($node);
18 | $context->compileChilds($node);
19 | DOMHelper::replaceWithSet($node, iterator_to_array($node->childNodes));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Node/UseNode.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class UseNode implements Node
14 | {
15 | public function visit(\DOMElement $node, Compiler $context)
16 | {
17 | $code = "use ";
18 |
19 | if ($node->hasAttribute("from")) {
20 | $code .= '"' . $node->getAttribute("from") . '"';
21 | } else {
22 | throw new Exception("The 'from' attribute is required");
23 | }
24 |
25 | if ($node->hasAttribute("with")) {
26 | $code .= " with " . $node->getAttribute("with");
27 | }
28 |
29 | $pi = $context->createControlNode($code);
30 | $node->parentNode->replaceChild($pi, $node);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/SourceAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | */
9 | interface SourceAdapter
10 | {
11 | /**
12 | * Gets the raw template source code and return a {Goetas\Twital\Template} instance.
13 | *
14 | * @param string $string
15 | * @return Template
16 | */
17 | public function load($string);
18 |
19 | /**
20 | * Gets a {Template} instance and return the raw template source code.
21 | *
22 | * @param Template $dom
23 | * @return string
24 | */
25 | public function dump(Template $dom);
26 | }
27 |
--------------------------------------------------------------------------------
/src/SourceAdapter/HTML5Adapter.php:
--------------------------------------------------------------------------------
1 |
11 | *
12 | */
13 | class HTML5Adapter implements SourceAdapter
14 | {
15 | private $html5;
16 |
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function load($source)
21 | {
22 | $html5 = $this->getHtml5();
23 |
24 | if (stripos(rtrim($source), '') === 0) {
25 | $dom = $html5->loadHTML($source);
26 | } else {
27 | $f = $html5->loadHTMLFragment($source);
28 | $dom = new \DOMDocument('1.0', 'UTF-8');
29 | if ('' !== trim($source)) {
30 | $dom->appendChild($dom->importNode($f, true));
31 | }
32 | }
33 | return new Template($dom, $this->collectMetadata($dom, $source));
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function dump(Template $template)
40 | {
41 | $metadata = $template->getMetadata();
42 | $dom = $template->getDocument();
43 | $html5 = $this->getHtml5();
44 | return $html5->saveHTML($metadata['fragment'] ? $dom->childNodes : $dom);
45 | }
46 |
47 | /**
48 | * Collect some metadata about $dom and $content
49 | * @param \DOMDocument $dom
50 | * @param string $source
51 | * @return mixed
52 | */
53 | protected function collectMetadata(\DOMDocument $dom, $source)
54 | {
55 | $metadata = array();
56 |
57 | $metadata['doctype'] = !!$dom->doctype;
58 | $metadata['fragment'] = stripos(rtrim($source), '') !== 0;
59 |
60 | return $metadata;
61 | }
62 |
63 | private function getHtml5()
64 | {
65 | if (!$this->html5) {
66 | $this->html5 = new HTML5(array(
67 | "xmlNamespaces" => true
68 | ));
69 | }
70 | return $this->html5;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/SourceAdapter/XHTMLAdapter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | class XHTMLAdapter extends XMLAdapter
12 | {
13 | /**
14 | * {@inheritdoc}
15 | */
16 | public function dump(Template $template)
17 | {
18 | $metadata = $template->getMetadata();
19 | $dom = $template->getDocument();
20 | $dom->preserveWhiteSpace = true;
21 | $dom->formatOutput = false;
22 |
23 | if ($metadata['xmldeclaration']) {
24 | $xml = $dom->saveXML();
25 | } else {
26 | $xml = '';
27 | foreach ($dom->childNodes as $node) {
28 | $xml .= $dom->saveXML($node, LIBXML_NOEMPTYTAG);
29 | if ($node instanceof \DOMDocumentType) {
30 | $xml .= PHP_EOL;
31 | }
32 | }
33 | }
34 |
35 | return $this->replaceShortTags($xml);
36 | }
37 |
38 | protected function replaceShortTags($str)
39 | {
40 | $selfClosingTags = array(
41 | "area",
42 | "base",
43 | "br",
44 | "col",
45 | "embed",
46 | "hr",
47 | "img",
48 | "input",
49 | "keygen",
50 | "link",
51 | "menuitem",
52 | "meta",
53 | "param",
54 | "source",
55 | "track",
56 | "wbr"
57 | );
58 | $regex = implode("|", array_map(function ($tag) {
59 | return ">\s*($tag)\s*>";
60 | }, $selfClosingTags));
61 |
62 | return preg_replace("#$regex#i", "/>", $str);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/SourceAdapter/XMLAdapter.php:
--------------------------------------------------------------------------------
1 |
10 | *
11 | */
12 | class XMLAdapter implements SourceAdapter
13 | {
14 | /**
15 | * {@inheritdoc}
16 | */
17 | public function load($source)
18 | {
19 | $dom = $this->createDom($source);
20 |
21 | return new Template($dom, $this->collectMetadata($dom, $source));
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function dump(Template $template)
28 | {
29 | $metadata = $template->getMetadata();
30 | $dom = $template->getDocument();
31 | $dom->preserveWhiteSpace = true;
32 | $dom->formatOutput = false;
33 |
34 | if ($metadata['xmldeclaration']) {
35 | return $dom->saveXML();
36 | } else {
37 | $xml = '';
38 | foreach ($dom->childNodes as $node) {
39 | $xml .= $dom->saveXML($node);
40 | if ($node instanceof \DOMDocumentType) {
41 | $xml .= PHP_EOL;
42 | }
43 | }
44 |
45 | return $xml;
46 | }
47 | }
48 |
49 | /**
50 | * Collect some metadata about $dom and $source
51 | * @param \DOMDocument $dom
52 | * @param string $source
53 | * @return mixed
54 | */
55 | protected function collectMetadata(\DOMDocument $dom, $source)
56 | {
57 | $metadata = array();
58 |
59 | $metadata['xmldeclaration'] = strpos(rtrim($source), 'doctype;
61 |
62 | return $metadata;
63 | }
64 |
65 | protected function createDom($source)
66 | {
67 | $internalErrors = libxml_use_internal_errors(true);
68 | libxml_clear_errors();
69 |
70 | $dom = new \DOMDocument('1.0', 'UTF-8');
71 | if ('' !== trim($source) && !$dom->loadXML($source, LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
72 | throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($internalErrors)));
73 | }
74 |
75 | libxml_use_internal_errors($internalErrors);
76 |
77 | return $dom;
78 | }
79 |
80 | protected function getXmlErrors($internalErrors)
81 | {
82 | $errors = array();
83 | foreach (libxml_get_errors() as $error) {
84 | $errors[] = sprintf(
85 | '[%s %s] %s (in %s - line %d, column %d)',
86 | LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
87 | $error->code,
88 | trim($error->message),
89 | $error->file ? $error->file : 'n/a',
90 | $error->line,
91 | $error->column
92 | );
93 | }
94 |
95 | libxml_clear_errors();
96 | libxml_use_internal_errors($internalErrors);
97 |
98 | return $errors;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Template.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | */
11 | class Template
12 | {
13 | /**
14 | * The template {DOMDocument}
15 | *
16 | * @var \DOMDocument
17 | */
18 | private $document;
19 |
20 | /**
21 | * Template metadatas
22 | *
23 | * @var mixed
24 | */
25 | private $metadata;
26 |
27 | /**
28 | * @param \DOMDocument $document The template {DOMDocument}
29 | * @param mixed $metadata Template metadatas
30 | */
31 | public function __construct(\DOMDocument $document, $metadata = null)
32 | {
33 | $this->document = $document;
34 | $this->metadata = $metadata;
35 | }
36 |
37 | /**
38 | * Returns the {DOMDocument} of a template
39 | *
40 | * @return \DOMDocument
41 | */
42 | public function getDocument()
43 | {
44 | return $this->document;
45 | }
46 |
47 | /**
48 | * Return template metadatas.
49 | *
50 | * @return mixed
51 | */
52 | public function getMetadata()
53 | {
54 | return $this->metadata;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Twital.php:
--------------------------------------------------------------------------------
1 |
14 | *
15 | */
16 | class Twital
17 | {
18 | const NS = 'urn:goetas:twital';
19 |
20 | protected $extensionsInitialized = false;
21 |
22 | /**
23 | *
24 | * @var EventDispatcher
25 | */
26 | protected $dispatcher;
27 |
28 | /**
29 | *
30 | * @var array
31 | */
32 | private $attributes = array();
33 |
34 | /**
35 | *
36 | * @var array
37 | */
38 | private $nodes = array();
39 |
40 | /**
41 | *
42 | * @var array
43 | */
44 | private $extensions = array();
45 |
46 | /**
47 | *
48 | * @var array
49 | */
50 | private $options = array();
51 |
52 | public function __construct(array $options = array())
53 | {
54 | $this->options = $options;
55 | $this->dispatcher = new EventDispatcher();
56 |
57 | $this->addExtension(new CoreExtension());
58 | }
59 |
60 | /**
61 | *
62 | * @return \Symfony\Component\EventDispatcher\EventDispatcher
63 | */
64 | public function getEventDispatcher()
65 | {
66 | $this->initExtensions();
67 |
68 | return $this->dispatcher;
69 | }
70 |
71 | public function getNodes()
72 | {
73 | $this->initExtensions();
74 |
75 | return $this->nodes;
76 | }
77 |
78 | public function getAttributes()
79 | {
80 | $this->initExtensions();
81 |
82 | return $this->attributes;
83 | }
84 |
85 | /**
86 | *
87 | * @param SourceAdapter $adapter
88 | * @param string $source
89 | * @return string
90 | */
91 | public function compile(SourceAdapter $adapter, $source)
92 | {
93 | $this->initExtensions();
94 |
95 | $sourceEvent = new SourceEvent($this, $source);
96 | $this->dispatch($sourceEvent, CompilerEvents::PRE_LOAD);
97 | $template = $adapter->load($sourceEvent->getTemplate());
98 |
99 | $templateEvent = new TemplateEvent($this, $template);
100 | $this->dispatch($templateEvent, CompilerEvents::POST_LOAD);
101 |
102 | $compiler = new Compiler($this, isset($this->options['lexer']) ? $this->options['lexer'] : array());
103 | $compiler->compile($templateEvent->getTemplate()->getDocument());
104 |
105 | $templateEvent = new TemplateEvent($this, $templateEvent->getTemplate());
106 | $this->dispatch($templateEvent, CompilerEvents::PRE_DUMP);
107 | $source = $adapter->dump($templateEvent->getTemplate());
108 |
109 | $sourceEvent = new SourceEvent($this, $source);
110 | $this->dispatch($sourceEvent, CompilerEvents::POST_DUMP);
111 |
112 | return $sourceEvent->getTemplate();
113 | }
114 |
115 | public function addExtension(Extension $extension)
116 | {
117 | $this->extensionsInitialized = false;
118 |
119 | return $this->extensions[] = $extension;
120 | }
121 |
122 | public function setExtensions(array $extensions)
123 | {
124 | $this->extensionsInitialized = false;
125 |
126 | $this->extensions = $extensions;
127 | }
128 |
129 | /**
130 | * @return Extension[]
131 | */
132 | public function getExtensions()
133 | {
134 | return $this->extensions;
135 | }
136 |
137 | protected function initExtensions()
138 | {
139 | if (!$this->extensionsInitialized) {
140 | foreach ($this->getExtensions() as $extension) {
141 | $this->attributes = array_merge_recursive($this->attributes, $extension->getAttributes());
142 | $this->nodes = array_merge_recursive($this->nodes, $extension->getNodes());
143 |
144 | foreach ($extension->getSubscribers() as $subscriber) {
145 | $this->dispatcher->addSubscriber($subscriber);
146 | }
147 | }
148 | $this->extensionsInitialized = true;
149 | }
150 | }
151 |
152 | protected function dispatch($event, $name)
153 | {
154 | if ($this->dispatcher instanceof EventDispatcherInterface) {
155 | $this->dispatcher->dispatch($event, $name);
156 | } else {
157 | $this->dispatcher->dispatch($name, $event);
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/TwitalLoader.php:
--------------------------------------------------------------------------------
1 | getSourceContext($name)->getCode();
17 | }
18 | }
19 | } elseif (interface_exists(ExistsLoaderInterface::class)) { // Twig 2
20 | abstract class BaseTwitalLoader extends TwitalLoaderTwigLt3 implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface
21 | {
22 | }
23 | } else { // Twig 3
24 | abstract class BaseTwitalLoader extends TwitalLoaderTwigGte3
25 | {
26 | }
27 | }
28 |
29 | /**
30 | * This is a Twital Loader.
31 | * Compiles a Twital template into a Twig template.
32 | *
33 | * @author Asmir Mustafic
34 | */
35 | class TwitalLoader extends BaseTwitalLoader
36 | {
37 | }
38 |
--------------------------------------------------------------------------------
/src/TwitalLoaderTrait.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | trait TwitalLoaderTrait
19 | {
20 | /**
21 | * Array of patterns used to decide if a template is twital-compilable or not.
22 | * Items are strings or callbacks
23 | *
24 | * @var array
25 | */
26 | protected $sourceAdapters = array();
27 |
28 | /**
29 | * The internal Twital compiler
30 | *
31 | * @var Twital
32 | */
33 | protected $twital;
34 |
35 | /**
36 | * The wrapped Twig loader
37 | *
38 | * @var LoaderInterface|\Twig_LoaderInterface
39 | */
40 | protected $loader;
41 |
42 | /**
43 | * Creates a new Twital loader.
44 | *
45 | * @param LoaderInterface $loader
46 | * @param Twital $twital
47 | * @param bool $addDefaults If NULL, some standard rules will be used (`*.twital.*` and `*.twital`).
48 | */
49 | public function __construct(LoaderInterface $loader = null, Twital $twital = null, $addDefaults = true)
50 | {
51 | $this->loader = $loader;
52 | $this->twital = $twital;
53 |
54 | if ($addDefaults === true || (is_array($addDefaults) && in_array('html', $addDefaults))) {
55 | $this->addSourceAdapter('/\.twital\.html$/i', new HTML5Adapter());
56 | }
57 | if ($addDefaults === true || (is_array($addDefaults) && in_array('xml', $addDefaults))) {
58 | $this->addSourceAdapter('/\.twital\.xml$/i', new XMLAdapter());
59 | }
60 | if ($addDefaults === true || (is_array($addDefaults) && in_array('xhtml', $addDefaults))) {
61 | $this->addSourceAdapter('/\.twital\.xhtml$/i', new XHTMLAdapter());
62 | }
63 | }
64 |
65 | /**
66 | * Add a new pattern that can decide if a template is twital-compilable or not.
67 | * If $pattern is a string, then must be a valid regex that matches the template filename.
68 | * If $pattern is a callback, then must return true if the template is compilable, false otherwise.
69 | *
70 | * @param string|callback $pattern
71 | * @param SourceAdapter $adapter
72 | * @return TwitalLoader
73 | */
74 | public function addSourceAdapter($pattern, SourceAdapter $adapter)
75 | {
76 | $this->sourceAdapters[$pattern] = $adapter;
77 |
78 | return $this;
79 | }
80 |
81 | /**
82 | * Get all patterns used to choose if a template is twital-compilable or not
83 | *
84 | * @return array:
85 | */
86 | public function getSourceAdapters()
87 | {
88 | return $this->sourceAdapters;
89 | }
90 |
91 | /**
92 | * Decide if a template is twital-compilable or not.
93 | *
94 | * @param string $name
95 | * @return SourceAdapter
96 | */
97 | public function getSourceAdapter($name)
98 | {
99 | foreach (array_reverse($this->sourceAdapters) as $pattern => $adapter) {
100 | if (preg_match($pattern, $name)) {
101 | return $adapter;
102 | }
103 | }
104 |
105 | return null;
106 | }
107 |
108 | /**
109 | * Get the wrapped Twig loader
110 | *
111 | * @return LoaderInterface|\Twig_LoaderInterface
112 | */
113 | public function getLoader()
114 | {
115 | return $this->loader;
116 | }
117 |
118 | /**
119 | * Set the wrapped Twig loader
120 | *
121 | * @param LoaderInterface|\Twig_LoaderInterface $loader
122 | * @return TwitalLoader
123 | */
124 | public function setLoader($loader)
125 | {
126 | $this->loader = $loader;
127 |
128 | return $this;
129 | }
130 |
131 | /**
132 | * @return Twital
133 | */
134 | public function getTwital()
135 | {
136 | if ($this->twital === null) {
137 | $this->twital = new Twital();
138 | }
139 |
140 | return $this->twital;
141 | }
142 |
143 | private function doGetSourceContext($name)
144 | {
145 | if (Environment::MAJOR_VERSION >= 2 || $this->loader instanceof SourceContextLoaderInterface) {
146 | $originalContext = $this->loader->getSourceContext($name);
147 | $code = $originalContext->getCode();
148 | $path = $originalContext->getPath();
149 | } else {
150 | $code = $this->loader->getSource($name);
151 | $path = null;
152 | }
153 |
154 | if ($adapter = $this->getSourceAdapter($name)) {
155 | $code = $this->getTwital()->compile($adapter, $code);
156 | }
157 |
158 | return new Source($code, $name, $path);
159 | }
160 |
161 | private function doExists($name)
162 | {
163 | if (Environment::MAJOR_VERSION >= 2 || $this->loader instanceof ExistsLoaderInterface) {
164 | return $this->loader->exists($name);
165 | } else {
166 | try {
167 | $this->getSourceContext($name);
168 |
169 | return true;
170 | } catch (LoaderError $e) {
171 | return false;
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/TwitalLoaderTwigGte3.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | abstract class TwitalLoaderTwigGte3 implements LoaderInterface
12 | {
13 | use TwitalLoaderTrait;
14 |
15 | public function getSourceContext(string $name): Source
16 | {
17 | return $this->doGetSourceContext($name);
18 | }
19 |
20 | public function getCacheKey(string $name): string
21 | {
22 | return $this->loader->getCacheKey($name);
23 | }
24 |
25 | public function isFresh(string $name, int $time): bool
26 | {
27 | return $this->loader->isFresh($name, $time);
28 | }
29 |
30 | public function exists(string $name)
31 | {
32 | return $this->doExists($name);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/TwitalLoaderTwigLt3.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | abstract class TwitalLoaderTwigLt3
9 | {
10 | use TwitalLoaderTrait;
11 |
12 | public function getCacheKey($name)
13 | {
14 | return $this->loader->getCacheKey($name);
15 | }
16 |
17 | public function isFresh($name, $time)
18 | {
19 | return $this->loader->isFresh($name, $time);
20 | }
21 |
22 | public function getSourceContext($name)
23 | {
24 | return $this->doGetSourceContext($name);
25 | }
26 |
27 | public function exists($name)
28 | {
29 | return $this->doExists($name);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Tests/ContextAwareEscapingTest.php:
--------------------------------------------------------------------------------
1 | twital = new Twital();
20 | }
21 |
22 | /**
23 | * @dataProvider getData
24 | */
25 | public function testHTML5SourceAdapter($source, $expected)
26 | {
27 | $sourceAdapter = new HTML5Adapter();
28 |
29 | $expectedDom = $sourceAdapter->load($expected);
30 | $expectedStr = $sourceAdapter->dump($expectedDom);
31 |
32 | $compiled = $this->twital->compile($sourceAdapter, $this->wrap($source, false));
33 | $this->assertEquals($this->wrap($expectedStr, false), $compiled);
34 | }
35 |
36 | /**
37 | * @dataProvider getData
38 | */
39 | public function testHTML5SourceAdapterNotWrapped($source, $expected)
40 | {
41 | $sourceAdapter = new HTML5Adapter();
42 |
43 | $expectedDom = $sourceAdapter->load($expected);
44 | $expectedStr = $sourceAdapter->dump($expectedDom);
45 |
46 | $compiled = $this->twital->compile($sourceAdapter, $source);
47 | $this->assertEquals($expectedStr, $compiled);
48 | }
49 |
50 | /**
51 | * @dataProvider getData
52 | */
53 | public function testXHTMLSourceAdapter($source, $expected)
54 | {
55 | $sourceAdapter = new XHTMLAdapter();
56 |
57 | $compiled = $this->twital->compile($sourceAdapter, $this->wrap($source));
58 | $this->assertEquals($this->wrap($expected), $compiled);
59 | }
60 |
61 | /**
62 | * @dataProvider getData
63 | */
64 | public function testXMLSourceAdapter($source, $expected)
65 | {
66 | $sourceAdapter = new XMLAdapter();
67 |
68 | $compiled = $this->twital->compile($sourceAdapter, $this->wrap($source));
69 | $this->assertEquals($this->wrap($expected), $compiled);
70 | }
71 |
72 | public function getData()
73 | {
74 | return array(
75 | // script escaping
76 | array(
77 | '',
78 | '',
79 | ),
80 | array(
81 | '',
82 | '',
83 | ),
84 | array(
85 | '',
86 | '',
87 | ),
88 | array(
89 | '',
90 | '',
91 | ),
92 |
93 | // CSS escaping
94 | array(
95 | '',
96 | '',
97 | ),
98 | array(
99 | '',
100 | '',
101 | ),
102 | array(
103 | '',
104 | '',
105 | ),
106 | array(
107 | '',
108 | '',
109 | ),
110 |
111 | // inline script escaping
112 | array(
113 | 'bar ',
114 | 'bar ',
115 | ),
116 |
117 | // URL escaping
118 | array(
119 | ' ',
120 | ' '
121 | ),
122 | array(
123 | 'bar ',
124 | 'bar ',
125 | ),
126 |
127 | array(
128 | 'bar ',
129 | 'bar ',
130 | ),
131 |
132 | array(
133 | 'bar ',
134 | 'bar ',
135 | ),
136 |
137 | array(
138 | ' ',
139 | ' '
140 | ),
141 | );
142 | }
143 |
144 | protected function wrap($html, $addNs = true)
145 | {
146 | return "$html";
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/tests/Tests/CoreAttributeTest.php:
--------------------------------------------------------------------------------
1 | twital = new Twital();
17 | $this->sourceAdapter = new XMLAdapter();
18 | }
19 |
20 | /**
21 | * @dataProvider getData
22 | */
23 | public function testVisitAttribute($source, $expected)
24 | {
25 | $compiled = $this->twital->compile($this->sourceAdapter, $source);
26 | $this->assertEquals($expected, $compiled);
27 | }
28 |
29 | public function getData()
30 | {
31 | return array(
32 | // if
33 | array('content
', '{% if test %}content
{% endif %}'),
34 | array('', '{% if test1 %}
content1
{% elseif test2 %}
content2
{% endif %}
'),
35 | array('', '{% if test1 %}
content1
{% elseif test2 %}
content2
{% endif %}
'),
36 | array('content1
content2
content3
', '{% if test1 %}
content1
{% elseif test2 %}
content2
{% elseif test3 %}
content3
{% endif %}
'),
37 | array('', '{% if test1 %}
content1
{% else %}
content2
{% endif %}
'),
38 | array('', '{% if test1 %}
content1
{% elseif test2 %}
content2
{% else %}
content3
{% endif %}
'),
39 | // for
40 | array('content
', '{% for foo %}content
{% endfor %}'),
41 | // set
42 | array('content
', '{% set foo = 1 %}content
'),
43 | array('content
', '{% set foo %}content
{% endset %}'),
44 | // content
45 | array('content
', '{{ foo }}
'),
46 | // mixed
47 | array('content
', '{% if cond %}{% for foo %}content
{% endfor %}{% endif %}'),
48 | array('content
', '{% for foo %}{% if cond %}content
{% endif %}{% endfor %}'),
49 | // omit
50 | array('content
', '{% set __tmp_omit = cond %}{% if not __tmp_omit %}{% endif %}content{% if not __tmp_omit %}
{% endif %}'),
51 | array('content
', '{% set __tmp_omit = cond %}{% if not __tmp_omit %}{% endif %}content {% if not __tmp_omit %}
{% endif %}'),
52 |
53 | array('content
', '{% if cond %}{% set __tmp_omit = true %}{% if not __tmp_omit %}{% endif %}content{% if not __tmp_omit %}
{% endif %}{% endif %}'),
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Tests/DynamicAttrAttributeTest.php:
--------------------------------------------------------------------------------
1 | loader = new ArrayLoader();
30 | $twitalLoader = new TwitalLoader($this->loader, null, false);
31 | $twitalLoader->addSourceAdapter("/.*/", new XMLAdapter());
32 |
33 | $this->twig = new Environment($twitalLoader);
34 | }
35 |
36 | /**
37 | * @dataProvider getData
38 | */
39 | public function testVisitAttribute($source, $expected, $vars = null)
40 | {
41 | $this->loader->setTemplate('template', $source);
42 | $rendered = $this->twig->render('template', $vars ?: array());
43 | $this->assertEquals($expected, $rendered);
44 | }
45 |
46 | public function getData()
47 | {
48 | return array(
49 | array('content
', 'content
'),
50 | array('', ''),
51 | array('', ''),
52 | array('', ''),
53 | array('content
', 'content
'),
54 | array('content
', 'content
'),
55 | array('content
', 'content
', array('condition' => 1)),
56 | array('content
', 'content
', array('condition' => 0)),
57 | array('content
', 'content
', array('condition' => 1)),
58 |
59 | array('a ', 'a '),
60 | array('a ', 'a '),
61 | array('a ', 'a '),
62 |
63 | array('content
', 'content
', array('condition' => 0)),
64 |
65 | array('content
', 'content
', array('condition' => 1)),
66 | array('content
', 'content
', array('condition' => 1)),
67 | array('content
', 'content
', array('condition' => 1)),
68 |
69 | array('content
', 'content', array('condition' => 1)),
70 | array('content
', 'content
', array('condition' => 0)),
71 | array('content
', 'content'),
72 |
73 | array(' ', ' ', array('a' => '/a/x.jpg')),
74 | array(' ', ' ',),
75 | array(' ', ' '),
76 | array(' ', ' ', array('a' => '/d/x.jpg', 'b' => '/b/x.jpg')),
77 | );
78 | }
79 |
80 | public function testAttributeHash()
81 | {
82 | $source = <<
84 |
85 |
86 |
87 |
88 | yyy
89 |
90 |
91 |
92 |
93 |
94 | EOT;
95 |
96 | $this->loader->setTemplate('template', $source);
97 | $rendered = $this->twig->render('template');
98 | $expected = <<
100 |
101 |
102 |
103 |
104 | yyy
105 |
106 |
107 |
108 |
109 |
110 | EOT;
111 | $this->assertEquals($expected, $rendered);
112 | }
113 |
114 | public function getInvalidData()
115 | {
116 | return array(
117 | array('content
'),
118 | array('content
'),
119 | );
120 | }
121 |
122 | /**
123 | * @dataProvider getInvalidData
124 | */
125 | public function testInvalidVisitAttribute($source)
126 | {
127 | $this->expectException(\Exception::class);
128 | $this->loader->setTemplate('template', $source);
129 | $this->twig->render('template');
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/Tests/Event/SourceEventTest.php:
--------------------------------------------------------------------------------
1 | twital = new Twital();
19 | }
20 |
21 | public function testBase()
22 | {
23 | $template = md5(microtime());
24 | $ist = new SourceEvent($this->twital, $template);
25 |
26 | $this->assertSame($this->twital, $ist->getTwital());
27 | $this->assertSame($template, $ist->getTemplate());
28 | }
29 |
30 | public function testBaseSetter()
31 | {
32 | $template = md5(microtime());
33 | $ist = new SourceEvent($this->twital, $template);
34 |
35 | $this->assertSame($template, $ist->getTemplate());
36 |
37 | $templateNew = md5(microtime());
38 | $ist->setTemplate($templateNew);
39 |
40 | $this->assertSame($templateNew, $ist->getTemplate());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Tests/Event/TemplateEventTest.php:
--------------------------------------------------------------------------------
1 | twital = new Twital();
21 | $this->template = $this->getTemplateMock();
22 | }
23 |
24 | public function testBase()
25 | {
26 | $ist = new TemplateEvent($this->twital, $this->template);
27 |
28 | $this->assertSame($this->twital, $ist->getTwital());
29 | $this->assertSame($this->template, $ist->getTemplate());
30 | }
31 |
32 | public function testBaseSetter()
33 | {
34 | $ist = new TemplateEvent($this->twital, $this->template);
35 |
36 | $template = $this->getTemplateMock();
37 | $ist->setTemplate($template);
38 |
39 | $this->assertSame($template, $ist->getTemplate());
40 | }
41 |
42 | protected function getTemplateMock()
43 | {
44 | return $this->getMockBuilder(Template::class)->setConstructorArgs(array(new \DOMDocument('1.0', 'UTF-8')))->getMock();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Tests/ExpressionParserTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $splitted);
16 | }
17 |
18 | /**
19 | * @dataProvider getDataWithLimit
20 | */
21 | public function testExpressionsWithLimit($expression, $splitter, $limit, $expected)
22 | {
23 | $splitted = ParserHelper::staticSplitExpression($expression, $splitter, $limit);
24 | $this->assertEquals($expected, $splitted);
25 | }
26 |
27 | /**
28 | * @dataProvider getWrongData
29 | */
30 | public function testWrongExpressions($expression)
31 | {
32 | $this->expectException(\Exception::class);
33 | ParserHelper::staticSplitExpression($expression, "x");
34 | }
35 |
36 | public function getWrongData()
37 | {
38 | return array(
39 |
40 | array('a? "b'),
41 | array('a? \'b'),
42 | array('a? b)'),
43 |
44 | );
45 | }
46 |
47 | public function getDataWithLimit()
48 | {
49 | return array(
50 | array('a', '?', 2, array('a')),
51 | array('?', '?', 2, array('', '')),
52 | array('a?b?c', '?', 2, array('a', 'b?c')),
53 | array('a?(b?c)', '?', 2, array('a', '(b?c)')),
54 | array('a?(b?c)?d?e', '?', 3, array('a', '(b?c)', 'd?e')),
55 | array('[a?(b?c)?d]?e', '?', 3, array('[a?(b?c)?d]', 'e')),
56 |
57 | array('[aa?(bb?cc)?dd]?ee', '?', 3, array('[aa?(bb?cc)?dd]', 'ee')),
58 | array('[aa?"bb?cc"?dd]?ee', '?', 3, array('[aa?"bb?cc"?dd]', 'ee')),
59 |
60 | array('[aa?"bb\"?cc"?dd]?ee', '?', 3, array('[aa?"bb\"?cc"?dd]', 'ee')),
61 | );
62 | }
63 |
64 | public function getData()
65 | {
66 | return array(
67 | array('?a b', '?', array('', 'a b')),
68 |
69 | array('a b?', '?', array('a b', '')),
70 |
71 | array('a b', '?', array('a b')),
72 |
73 | array('a? b', '?', array('a', 'b')),
74 | array('a? "b?"', '?', array('a', '"b?"')),
75 | array('"a?"? "b?"', '?', array('"a?"', '"b?"')),
76 | array('"a?" ? "b?"', '?', array('"a?"', '"b?"')),
77 | array('"a?" ? "b?"', '?', array('"a?"', '"b?"')),
78 |
79 | array('\'a?\' ? "b?"', '?', array('\'a?\'', '"b?"')),
80 |
81 | //non quoted group
82 | array('(a?) ? "b?"', '?', array('(a?)', '"b?"')),
83 | array('"a?" ? (b?)', '?', array('"a?"', '(b?)')),
84 |
85 | array("aaa:{'aaa':'xxx', 'bbb':'cc'},title", ',', array("aaa:{'aaa':'xxx', 'bbb':'cc'}", "title")),
86 | array("'aaa':'xxx', 'bbb':'cc'", ',', array("'aaa':'xxx'", "'bbb':'cc'")),
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/Tests/FullCompatibilityTwigTest.php:
--------------------------------------------------------------------------------
1 | templateSubscriber = new DebugTemplateSubscriber();
25 |
26 | $this->twital = new Twital();
27 | $this->twital->addExtension(new FullCompatibilityTwigExtension());
28 | $this->twital->getEventDispatcher()->addSubscriber($this->templateSubscriber);
29 | }
30 |
31 | public function testListenerPriority()
32 | {
33 | $eventDispatcher = $this->twital->getEventDispatcher();
34 |
35 | $preLoad = $eventDispatcher->getListeners(CompilerEvents::PRE_LOAD);
36 |
37 | $this->assertEquals(array(new ReplaceDoctypeAsTwigExpressionSubscriber(), 'replaceDoctype'), $preLoad[0]);
38 | $this->assertEquals(array(new FixTwigExpressionSubscriber(), 'addPlaceholder'), $preLoad[1]);
39 |
40 | $postDump = $eventDispatcher->getListeners(CompilerEvents::POST_DUMP);
41 | $this->assertEquals(array(new FixTwigExpressionSubscriber(), 'removePlaceholder'), end($postDump));
42 | }
43 |
44 | /**
45 | * @dataProvider getData
46 | */
47 | public function testHTML5SourceAdapter($source, $expected = null)
48 | {
49 | $sourceAdapter = new HTML5Adapter();
50 |
51 | $compiled = $this->twital->compile($sourceAdapter, $source);
52 | $this->assertEquals($expected !== null ? $expected : $source, $compiled, 'PRE: '.$this->templateSubscriber->preLoadTemplate."\n\nPOST: ".$this->templateSubscriber->postDumpTemplate);
53 | }
54 |
55 | public function getData()
56 | {
57 | return array(
58 | array('{% if foo > 5 and bar < 8 and bar & 4 %}foo{% endif %}
'),
59 | array('{{ foo > 5 and bar < 8 and bar & 4 ? "foo" }}
'),
60 | array('{# foo > 5 and bar < 8 and bar & 4 ? "foo" #}
'),
61 |
62 | array("{% '{%' and '%}' and '{{' and '}}' and '{#' and '#}' %}
"),
63 | array("{{ '{%' and '%}' and '{{' and '}}' and '{#' and '#}' }}
"),
64 | array("{# '{%' and '%}' and '{{' and '}}' and '{#' and '#}' #}
"),
65 |
66 | array("{{ '}}\\'}}' > 5 & 4 }}
"),
67 | array("{{ '{%\\'%}\\\\\\'}}' > 5 & 4 }}
"),
68 |
69 | array('{% block title "title" %} '),
70 | array('foo '),
71 |
72 | array('foo
'),
73 | array('<{{ tagname }}>foo{{ tagname }}>'),
74 | array('foo
'),
75 | array('foo
'),
76 | array(' 5 or foo < 8 }} class="class {{ "foo" < "bar" and 5 > 3 }}">foo
'),
77 | array('foo
'),
78 | array('foo
'),
79 | array('foo '),
80 | array('foo
'),
81 |
82 | array(file_get_contents(__DIR__.'/templates/web_profiler_js.html.twig')),
83 | array(file_get_contents(__DIR__.'/templates/logger.html.twig')),
84 | array("{% set a = 1 %}\n\nfoo", "{% set a = 1 %}\n{{ '' }}\nfoo"),
85 | array("{% set a = 1 %}\n\nfoo", "{% set a = 1 %}\n{{ '' }}\nfoo"),
86 | array('test {{ foo }}
test {{ bar }}
test', 'test {% if foo %}{{ foo }}
{% endif %} test {% if bar %}{{ bar }}
{% endif %} test'),
87 | );
88 | }
89 | }
90 |
91 | class DebugTemplateSubscriber implements EventSubscriberInterface
92 | {
93 | public $preLoadTemplate;
94 | public $postDumpTemplate;
95 |
96 | public static function getSubscribedEvents()
97 | {
98 | return array(
99 | CompilerEvents::PRE_LOAD => array('onPreLoad'),
100 | CompilerEvents::POST_DUMP => array('onPostDump'),
101 | );
102 | }
103 |
104 | public function onPreLoad(SourceEvent $event)
105 | {
106 | $this->preLoadTemplate = $event->getTemplate();
107 | }
108 |
109 | public function onPostDump(SourceEvent $event)
110 | {
111 | $this->postDumpTemplate = $event->getTemplate();
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/tests/Tests/Html5CoreNodesTest.php:
--------------------------------------------------------------------------------
1 | ', '', $str);
39 | return parent::cleanup($str);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Tests/Html5DynamicAttrAttributeTest.php:
--------------------------------------------------------------------------------
1 | loader = new ArrayLoader();
30 | $twitalLoader = new TwitalLoader($this->loader, null, false);
31 | $twitalLoader->addSourceAdapter("/.*/", new HTML5Adapter());
32 |
33 | $this->twig = new Environment($twitalLoader, array(
34 | 'strict_variables' => true
35 | ));
36 | }
37 |
38 | /**
39 | * @dataProvider getData
40 | */
41 | public function testVisitAttribute($source, $expected, $vars = null)
42 | {
43 | $this->loader->setTemplate('template', $source);
44 | $rendered = $this->twig->render('template', $vars ?: array());
45 | $this->assertEquals($expected, $rendered);
46 | }
47 |
48 | public function getData()
49 | {
50 | return array(
51 | array('content ', 'content ', array('value' => true)),
52 |
53 | array('content ', 'content ', array('value' => false)),
54 | array('content ', 'content ', array('value' => 1)),
55 | array('content ', 'content ', array('value' => 'foo')),
56 |
57 | array('content ', 'content ', array('value' => true)),
58 | array('content ', 'content ', array('value' => true)),
59 |
60 | array('content ', 'content ', array('value' => false)),
61 | array('content ', 'content ', array('value' => false)),
62 | array('content ', 'content ', array('value' => false)),
63 |
64 | array('content ', 'content ', array('value' => 'foo')),
65 | array('content ', 'content ', array('value' => 'foo')),
66 | array('content ', 'content ', array('value' => 'foo')),
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Tests/TwitalLoaderTest.php:
--------------------------------------------------------------------------------
1 | assertSame($loader, $twitalLoader->getLoader());
45 |
46 | $newLoader = new ArrayLoader();
47 | $twitalLoader->setLoader($newLoader);
48 | $this->assertSame($newLoader, $twitalLoader->getLoader());
49 | $this->assertNotSame($loader, $twitalLoader->getLoader());
50 | }
51 |
52 | public function testDefaultAdapters()
53 | {
54 | $twitalLoader = new TwitalLoader();
55 |
56 | $adapters = $twitalLoader->getSourceAdapters();
57 |
58 | $this->assertContainsOnlyInstancesOf('Goetas\Twital\SourceAdapter', $adapters);
59 |
60 | foreach ($this->getRequiredAdapters() as $class) {
61 | $filteredAdapters = array_filter($adapters, function ($adapter) use ($class) {
62 | return is_a($adapter, $class);
63 | });
64 | $this->assertGreaterThanOrEqual(1, count($filteredAdapters), "Cant find any $class adapter");
65 | }
66 | }
67 |
68 | /**
69 | *
70 | * @dataProvider getMatchedFilenames
71 | */
72 | public function testRegexAdapters($filename, $managed)
73 | {
74 | $twitalLoader = new TwitalLoader();
75 | $this->assertEquals($managed, !!$twitalLoader->getSourceAdapter($filename));
76 | }
77 |
78 | public function testTwitalFile()
79 | {
80 | $loader = new ArrayLoader();
81 | $loader->setTemplate('aaa.xml', '');
82 |
83 | $twital = $this->createMock(Twital::class);
84 | $twitalLoader = new TwitalLoader($loader, $twital, false);
85 | $twitalLoader->addSourceAdapter('/.*\.xml$/', new XMLAdapter());
86 |
87 | $twital->expects($this->once())->method('compile')->willReturn('');
88 | $twitalLoader->getSourceContext('aaa.xml');
89 | }
90 |
91 | public function testNonTwitalFile()
92 | {
93 | $loader = new ArrayLoader();
94 | $loader->setTemplate('aaa.txt', '');
95 |
96 | $twital = $this->createMock(Twital::class);
97 | $twitalLoader = new TwitalLoader($loader, $twital, false);
98 | $twitalLoader->addSourceAdapter('/.*\.xml$/', new XMLAdapter());
99 |
100 | $twital->expects($this->never())->method('compile');
101 | $twitalLoader->getSourceContext('aaa.txt');
102 | }
103 |
104 | public function testExistsWithBaseLoaderTwig1()
105 | {
106 | if (Environment::MAJOR_VERSION >= 2) {
107 | $this->markTestSkipped("Twig > 1 has the LoaderInterface::exists method");
108 | }
109 |
110 | $mockLoader = $this->createMock(LoaderInterface::class);
111 | $mockLoader->expects($this->once())->method('getSource')->with($this->equalTo('foo'));
112 |
113 | $twitalLoader = new TwitalLoader($mockLoader, null, false);
114 | $this->assertTrue($twitalLoader->exists('foo'));
115 | }
116 |
117 | public function testNonExistsWithBaseLoaderTwig1()
118 | {
119 | if (Environment::MAJOR_VERSION >= 2) {
120 | $this->markTestSkipped("Twig > 1 has the LoaderInterface::exists method");
121 | }
122 |
123 | $mockLoader = $this->createMock(LoaderInterface::class);
124 |
125 | $mockLoader->expects($this->once())
126 | ->method('getSource')
127 | ->with($this->equalTo('foo'))
128 | ->will($this->throwException(new LoaderError("File not found")));
129 |
130 | $twitalLoader = new TwitalLoader($mockLoader, null, false);
131 | $this->assertFalse($twitalLoader->exists('foo'));
132 | }
133 |
134 | public function testExistsWithBaseLoaderTwigGte2()
135 | {
136 | if (Environment::MAJOR_VERSION < 2) {
137 | $this->markTestSkipped("Twig >= 2 only");
138 | }
139 |
140 | $mockLoader = $this->createMock(LoaderInterface::class);
141 |
142 | $mockLoader->expects($this->once())->method('exists')->will($this->returnValue(true));
143 |
144 | $twitalLoader = new TwitalLoader($mockLoader, null, false);
145 | $this->assertTrue($twitalLoader->exists('foo'));
146 | }
147 |
148 | public function testNonExistsWithBaseLoaderTwigGte2()
149 | {
150 | if (Environment::MAJOR_VERSION < 2) {
151 | $this->markTestSkipped("Twig >= 2 only");
152 | }
153 |
154 | $mockLoader = $this->createMock(LoaderInterface::class);
155 |
156 | $mockLoader->expects($this->once())->method('exists')->will($this->returnValue(false));
157 |
158 | $twitalLoader = new TwitalLoader($mockLoader, null, false);
159 | $this->assertFalse($twitalLoader->exists('foo'));
160 | }
161 |
162 | protected function getRequiredAdapters()
163 | {
164 | return array(
165 | 'Goetas\Twital\SourceAdapter\HTML5Adapter',
166 | 'Goetas\Twital\SourceAdapter\XMLAdapter',
167 | 'Goetas\Twital\SourceAdapter\XHTMLAdapter'
168 | );
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/tests/Tests/XhtmlCoreNodesTest.php:
--------------------------------------------------------------------------------
1 | {% block block01 %}
2 | Content {{ parent() }}
3 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/base-01.xml:
--------------------------------------------------------------------------------
1 |
2 | Content {{ parent() }}
3 |
--------------------------------------------------------------------------------
/tests/Tests/templates/doctype-01.html.twig:
--------------------------------------------------------------------------------
1 |
2 | foo
3 |
4 |
--------------------------------------------------------------------------------
/tests/Tests/templates/doctype-01.twig:
--------------------------------------------------------------------------------
1 |
2 | foo
--------------------------------------------------------------------------------
/tests/Tests/templates/doctype-01.xml:
--------------------------------------------------------------------------------
1 |
2 | foo
3 |
--------------------------------------------------------------------------------
/tests/Tests/templates/doctype-02.htm:
--------------------------------------------------------------------------------
1 |
2 | foo
3 |
--------------------------------------------------------------------------------
/tests/Tests/templates/doctype-02.twig:
--------------------------------------------------------------------------------
1 |
2 | foo
3 |
4 |
--------------------------------------------------------------------------------
/tests/Tests/templates/embed-01.twig:
--------------------------------------------------------------------------------
1 | {% extends "layout_skeleton.twig" %}
2 | {% block content %}
3 | {% embed "vertical_boxes_skeleton.twig" %}
4 | {% block top %}
5 | Some content for the top box
6 | {% endblock %}{% block bottom %}
7 | Some content for the bottom box
8 | {% endblock %}{% endembed %}
9 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/embed-01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Some content for the top box
6 |
7 |
8 | Some content for the bottom box
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/Tests/templates/embed-02.twig:
--------------------------------------------------------------------------------
1 | {% extends "layout_skeleton.twig" %}
2 | {% block content %}
3 | {% embed "vertical_boxes_skeleton.twig" ignore missing with {'foo': 'bar'} only %}
4 | {% block top %}
5 | Some content for the top box
6 | {% endblock %}{% block bottom %}
7 | Some content for the bottom box
8 | {% endblock %}{% endembed %}
9 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/embed-02.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Some content for the top box
6 |
7 |
8 | Some content for the bottom box
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/Tests/templates/empty.twig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goetas/twital/20dc10457d24257342e4a447961b5ab9e6376b5f/tests/Tests/templates/empty.twig
--------------------------------------------------------------------------------
/tests/Tests/templates/empty.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-00.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-00.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-01.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 | {% block block01 %}
3 | Test content
4 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test content
4 |
5 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-02.twig:
--------------------------------------------------------------------------------
1 | {% extends var %}
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-02.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-03.twig:
--------------------------------------------------------------------------------
1 | {% extends "layout_skeleton.twig" %}
2 | {% block a %}
3 | a
4 | {% endblock %}
5 |
6 | {% block b %}
7 | b
8 | {% endblock %}
9 | {% block c %}
10 | c
11 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-03.xml:
--------------------------------------------------------------------------------
1 |
2 | {% block a %}
3 | a
4 | {% endblock %}
5 |
6 | {% block b %}
7 | b
8 | {% endblock %}
9 |
10 | c
11 |
12 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes-inner.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 |
3 | {% block block01 %}
4 | 01
5 |
6 | {% block block02 %}
7 |
8 | 02
9 |
10 | {% block block03 %}
11 | 03
12 | {% endblock %}
13 |
14 |
15 | {% endblock %}
16 |
17 | some
18 |
19 | {% block block04 %}
20 | 04
21 | {% endblock %}
22 |
23 | {% endblock %}
24 |
25 | {% block block05 %}
26 | 05
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes-inner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 01
5 |
6 |
7 |
8 | 02
9 |
10 |
11 | 03
12 |
13 |
14 |
15 | some
16 |
17 | 04
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes-use.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 | {% use "blocks.html" %}
3 | {% block block01 %}
4 |
5 | 01
6 |
7 | {% endblock %}
8 |
9 | {% block block05 %}
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes-use.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 01
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 |
3 | {% block block01 %}
4 |
5 | 01
6 | {% block block02 %}
7 |
8 | 02
9 | {% block block03 %}
10 | 03
11 | {% endblock %}
12 |
13 | {% endblock %}
14 |
15 | some
16 |
17 | {% block block04 %}
18 | 04
19 | {% endblock %}
20 |
21 | {% endblock %}
22 |
23 | {% block block05 %}
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/tests/Tests/templates/extends-as-attributes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 01
5 |
6 |
7 |
8 | 02
9 |
10 |
11 | 03
12 |
13 |
14 |
15 | some
16 |
17 | 04
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Tests/templates/macro-01.html.twig:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type, size) %}
2 |
3 | {% endmacro %}
--------------------------------------------------------------------------------
/tests/Tests/templates/macro-01.twig:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type, size) %}
2 |
3 | {% endmacro %}
--------------------------------------------------------------------------------
/tests/Tests/templates/macro-01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tests/Tests/templates/use-01.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 | {% use "blocks.html" %}
3 | {% block block01 %}
4 | Test content
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/tests/Tests/templates/use-01.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test content
5 |
6 |
--------------------------------------------------------------------------------
/tests/Tests/templates/use-02.twig:
--------------------------------------------------------------------------------
1 | {% extends "foo.twig" %}
2 | {% use "blocks.html" with sidebar as base_sidebar %}{% block block01 %}
3 | Test content
4 | {% endblock %}
--------------------------------------------------------------------------------
/tests/Tests/templates/use-02.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test content
5 |
6 |
--------------------------------------------------------------------------------
/tests/Tests/templates/widget-header.twig:
--------------------------------------------------------------------------------
1 | {% extends '@widget/widget.html.twig' %}
2 |
3 | {% from '@widget/semantic-ui/macros.html.twig' import size %}
4 |
5 | {% block widget %}
6 | {%- set align = (centered|default or center|default) ? 'center' : align|default -%}
7 | {%- set align = left|default ? 'left' : (right|default ? 'right' : align) -%}
8 |
9 | {% set tag = 'h'~level %}
10 | <{{ tag }} class="ui header
11 | {%- if 'icon' is block name %} icon{% endif -%}
12 | {%- if disabled|default %} disabled{% endif -%}
13 | {%- if dividing|default %} dividing{% endif -%}
14 | {%- if block|default %} block{% endif -%}
15 | {%- if attached|default %} {{ attached }} attached{% endif -%}
16 | {%- if floated|default %} {{ floated }} floated{% endif -%}
17 | {%- if align|default %} {{ align }} aligned{% endif -%}
18 | {%- if color|default %} {{ color }}{% endif -%}
19 | {%- if inverted|default %} inverted{% endif -%}
20 | {%- if size|default %} {{ size(size) }}{% endif -%}
21 | ">
22 | {{ block('icon') }}
23 | {% if 'icon' is block name or 'subheader' is block name %}
24 |
25 | {{ block('header') }}
26 | {% if 'subheader' is block name %}{% endif %}
27 |
28 | {% else %}
29 | {{ block('header') }}
30 | {% endif %}
31 | {{ tag }}>
32 | {% endblock %}
33 |
34 | {% block header %}
35 | {{ 'text' is block name ? block('text') : block('__content__') }}
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/tests/Tests/templates/xmldeclaration-01.html.twig:
--------------------------------------------------------------------------------
1 |
2 | foo
--------------------------------------------------------------------------------
/tests/Tests/templates/xmldeclaration-01.twig:
--------------------------------------------------------------------------------
1 |
2 | foo
3 |
--------------------------------------------------------------------------------
/tests/Tests/templates/xmldeclaration-01.xml:
--------------------------------------------------------------------------------
1 |
2 | foo
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |