├── demos ├── test.html ├── node_express │ ├── .gitignore │ ├── public │ │ ├── views │ │ │ ├── inline.twig │ │ │ ├── pages │ │ │ │ ├── note_404.twig │ │ │ │ ├── index.twig │ │ │ │ ├── note.twig │ │ │ │ ├── notes.twig │ │ │ │ └── note_form.twig │ │ │ └── layout.twig │ │ ├── less │ │ │ ├── css3.less │ │ │ └── styles.less │ │ ├── vendor │ │ │ ├── signals.min.js │ │ │ └── crossroads.min.js │ │ └── js │ │ │ └── app.js │ ├── package.json │ └── app.js └── twitter_backbone │ ├── templates │ ├── test.twig │ ├── tweet.twig │ └── app.twig │ ├── images │ ├── icons.ai │ ├── user.png │ └── reload.png │ ├── js │ ├── model │ │ ├── account.js │ │ ├── tweet.js │ │ ├── settings.js │ │ └── feed.js │ ├── app.js │ └── view │ │ ├── feedView.js │ │ ├── tweetView.js │ │ └── appView.js │ ├── README.md │ ├── index.html │ ├── app.js │ └── less │ ├── css3.less │ └── styles.less ├── src ├── .gitignore ├── twig.parser.source.js ├── twig.parser.twig.js ├── twig.js ├── twig.factory.js ├── twig.compiler.js ├── twig.loader.ajax.js ├── twig.tests.js ├── twig.lib.js ├── twig.loader.fs.js ├── twig.path.js └── twig.exports.js ├── test ├── namespaces.twig ├── templates │ ├── simple.twig │ ├── extendee-array.twig │ ├── inc.twig │ ├── namespaces │ │ ├── namespaces.twig │ │ ├── one │ │ │ └── namespaces.twig │ │ └── two │ │ │ └── namespaces.twig │ ├── block-parent.twig │ ├── test-async.twig │ ├── include.twig │ ├── use.twig │ ├── include │ │ └── relative.twig │ ├── extender-array.twig │ ├── namespaces_@.twig │ ├── namespaces_coloncolon.twig │ ├── import.twig │ ├── include-array.twig │ ├── embed-include.twig │ ├── error │ │ ├── compile │ │ │ └── entry.twig │ │ └── parse │ │ │ ├── in-partial │ │ │ └── entry.twig │ │ │ └── in-entry │ │ │ └── entry.twig │ ├── escape-date-format.twig │ ├── test.twig │ ├── include-ignore-missing-missing.twig │ ├── blocks-extended-syntax.twig │ ├── include-array-second-exists.twig │ ├── blocks-nested.twig │ ├── extender-array-second-exists.twig │ ├── include-ignore-missing.twig │ ├── embed-ignore-missing.twig │ ├── template.twig │ ├── b │ │ └── template.twig │ ├── block-function-parent.twig │ ├── block-function.twig │ ├── extender-array-none-exist.twig │ ├── namespaces_multiple.twig │ ├── extender.twig │ ├── include-only.twig │ ├── include-with.twig │ ├── a │ │ └── child.twig │ ├── child-blocks-nested.twig │ ├── child.twig │ ├── namespaces_without_namespace.twig │ ├── use-override-block.twig │ ├── source.twig │ ├── macro.twig │ ├── blocks-nested-multiple.twig │ ├── embed-base.twig │ ├── child-parent.twig │ ├── extendee.twig │ ├── macro-context.twig │ ├── macro-defaults.twig │ ├── blocks.twig │ ├── from.twig │ ├── block-outer-context.twig │ ├── use-override-nested-block.twig │ ├── macro-self.twig │ ├── macro-blocks.twig │ ├── macro-self-twice.twig │ ├── macro-wrapped.twig │ └── embed-simple.twig ├── compiler │ ├── src │ │ ├── sub │ │ │ └── sub.twig │ │ └── dir_test.twig │ ├── test.twig │ ├── templates │ │ ├── sub │ │ │ └── sub.twig.js │ │ ├── test.twig.js │ │ └── dir_test.twig.js │ └── test.html ├── test.inheritance.js ├── test.allowed-tags.js ├── test.logic.js ├── test.performance.js ├── test.parsers.js ├── test.factory.js ├── index.html ├── test.exports.js ├── test.regression.js ├── test.tags.js ├── test.rethrow.js ├── test.loaders.js ├── test.options.js ├── browser │ ├── test.macro.js │ ├── test.namespace.js │ ├── test.block.js │ └── test.browser.js ├── test.path.js ├── test.namespaces.js ├── test.macro.js ├── test.extends.js ├── test.embed.js ├── test.expressions.operators.js ├── test.async.js ├── test.fs.js └── test.tests.js ├── .gitignore ├── .gitmodules ├── docs ├── public │ └── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff └── release checklist.md ├── .editorconfig ├── .npmignore ├── .travis.yml ├── test-ext └── checkout.sh ├── LICENSE ├── webpack.config.js ├── package.json ├── lib ├── paths.js └── compile.js ├── bin └── twigjs ├── ASYNC.md └── README.md /demos/test.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | !twig.js 2 | -------------------------------------------------------------------------------- /test/namespaces.twig: -------------------------------------------------------------------------------- 1 | namespaces 2 | -------------------------------------------------------------------------------- /test/templates/simple.twig: -------------------------------------------------------------------------------- 1 | Twig.js! -------------------------------------------------------------------------------- /demos/node_express/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json -------------------------------------------------------------------------------- /test/templates/extendee-array.twig: -------------------------------------------------------------------------------- 1 | Hello, world! -------------------------------------------------------------------------------- /test/compiler/src/sub/sub.twig: -------------------------------------------------------------------------------- 1 | I'm a {{ type }} 2 | -------------------------------------------------------------------------------- /test/templates/inc.twig: -------------------------------------------------------------------------------- 1 | template: {{ a }},{{ test }} -------------------------------------------------------------------------------- /test/templates/namespaces/namespaces.twig: -------------------------------------------------------------------------------- 1 | namespaces -------------------------------------------------------------------------------- /test/templates/block-parent.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | -------------------------------------------------------------------------------- /test/templates/namespaces/one/namespaces.twig: -------------------------------------------------------------------------------- 1 | namespace one -------------------------------------------------------------------------------- /test/templates/namespaces/two/namespaces.twig: -------------------------------------------------------------------------------- 1 | namespace two -------------------------------------------------------------------------------- /test/templates/test-async.twig: -------------------------------------------------------------------------------- 1 | {{ hello_world() }} 2 | -------------------------------------------------------------------------------- /test/templates/include.twig: -------------------------------------------------------------------------------- 1 | Before{% include "test.twig" %}After -------------------------------------------------------------------------------- /test/templates/use.twig: -------------------------------------------------------------------------------- 1 | {% use "blocks.twig" %}{{ block("msg") }} -------------------------------------------------------------------------------- /demos/node_express/public/views/inline.twig: -------------------------------------------------------------------------------- 1 | {% block body %}{% endblock %} -------------------------------------------------------------------------------- /test/templates/include/relative.twig: -------------------------------------------------------------------------------- 1 | {% include "../simple.twig" %} 2 | -------------------------------------------------------------------------------- /demos/twitter_backbone/templates/test.twig: -------------------------------------------------------------------------------- 1 | The {{ baked_good }} is a lie! 2 | -------------------------------------------------------------------------------- /test/templates/extender-array.twig: -------------------------------------------------------------------------------- 1 | {% extends ['extendee-array.twig'] %} 2 | -------------------------------------------------------------------------------- /test/templates/namespaces_@.twig: -------------------------------------------------------------------------------- 1 | {% include "@test/namespaces.twig" %} 2 | -------------------------------------------------------------------------------- /test/templates/namespaces_coloncolon.twig: -------------------------------------------------------------------------------- 1 | {% include "test::namespaces.twig" %} -------------------------------------------------------------------------------- /test/templates/import.twig: -------------------------------------------------------------------------------- 1 | {% import "macro.twig" as say %}{{ say.echo('World') }} 2 | -------------------------------------------------------------------------------- /test/templates/include-array.twig: -------------------------------------------------------------------------------- 1 | Before{% include ["test.twig", "nothing.twig"] %}After -------------------------------------------------------------------------------- /test/templates/embed-include.twig: -------------------------------------------------------------------------------- 1 | {% block header %} 2 | Cool header 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /test/templates/error/compile/entry.twig: -------------------------------------------------------------------------------- 1 | Should throw a compile error 2 | {{ "foo" bar }} 3 | -------------------------------------------------------------------------------- /test/templates/error/parse/in-partial/entry.twig: -------------------------------------------------------------------------------- 1 | {% include "../in-entry/entry.twig" %} 2 | -------------------------------------------------------------------------------- /test/templates/escape-date-format.twig: -------------------------------------------------------------------------------- 1 | {{ "1970-01-01 00:00:00"|date("F jS \\a\\t g:ia") }} -------------------------------------------------------------------------------- /test/templates/test.twig: -------------------------------------------------------------------------------- 1 | Test template = {{ test }} 2 | 3 | {% if flag %}Flag set!{% endif %} -------------------------------------------------------------------------------- /test/compiler/test.twig: -------------------------------------------------------------------------------- 1 | {% if test %} 2 | Yep 3 | {% else %} 4 | Nope 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /test/templates/error/parse/in-entry/entry.twig: -------------------------------------------------------------------------------- 1 | Should throw a parse error 2 | {{ "foo"|bar }} 3 | -------------------------------------------------------------------------------- /test/templates/include-ignore-missing-missing.twig: -------------------------------------------------------------------------------- 1 | ignore-{% include "missing-file.twig" %}missing -------------------------------------------------------------------------------- /test/templates/blocks-extended-syntax.twig: -------------------------------------------------------------------------------- 1 | {% block msg %}This is the only thing.{% endblock msg %} 2 | -------------------------------------------------------------------------------- /test/templates/include-array-second-exists.twig: -------------------------------------------------------------------------------- 1 | Before{% include ["nothing.twig", "test.twig"] %}After -------------------------------------------------------------------------------- /test/templates/blocks-nested.twig: -------------------------------------------------------------------------------- 1 | {% block body %}parent{% block child %}:child{% endblock %}{% endblock %} -------------------------------------------------------------------------------- /test/templates/extender-array-second-exists.twig: -------------------------------------------------------------------------------- 1 | {% extends ['notfound.twig', 'extendee-array.twig'] %} 2 | -------------------------------------------------------------------------------- /test/templates/include-ignore-missing.twig: -------------------------------------------------------------------------------- 1 | ignore-{% include "missing-file.twig" ignore missing %}missing -------------------------------------------------------------------------------- /test/compiler/src/dir_test.twig: -------------------------------------------------------------------------------- 1 | {% if test %} 2 | Yep :) 3 | {% else %} 4 | Nope :( 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | twig.js 4 | twig.min.js 5 | twig.min.js.map 6 | twig.min.js.LICENSE.txt 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/wiki"] 2 | path = docs/wiki 3 | url = git://github.com/justjohn/twig.js.wiki.git 4 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /test/templates/embed-ignore-missing.twig: -------------------------------------------------------------------------------- 1 | ignore-{% embed "embed-not-there.twig" ignore missing %}{% endembed %}missing -------------------------------------------------------------------------------- /test/templates/template.twig: -------------------------------------------------------------------------------- 1 | {% block title %}Default Title{% endblock %} - {% block body %}body{{inner}}{% endblock %} -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /test/templates/b/template.twig: -------------------------------------------------------------------------------- 1 | {% block title %}Default Title{% endblock %} - {% block body %}body{{inner}}{% endblock %} -------------------------------------------------------------------------------- /test/templates/block-function-parent.twig: -------------------------------------------------------------------------------- 1 | {% block content %}parent block{% endblock %} / Result: {{ block("content") }} -------------------------------------------------------------------------------- /test/templates/block-function.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | 3 | {% block content %}Child content = {{ val }}{% endblock %} -------------------------------------------------------------------------------- /demos/twitter_backbone/images/icons.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/demos/twitter_backbone/images/icons.ai -------------------------------------------------------------------------------- /demos/twitter_backbone/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/demos/twitter_backbone/images/user.png -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /test/templates/extender-array-none-exist.twig: -------------------------------------------------------------------------------- 1 | {% extends ['notfound.twig', 'definitelynotfound.twig'] %} 2 | Nothing to see here -------------------------------------------------------------------------------- /test/templates/namespaces_multiple.twig: -------------------------------------------------------------------------------- 1 | {% include "one::namespaces.twig" %} 2 | 3 | {% include "two::namespaces.twig" %} 4 | -------------------------------------------------------------------------------- /demos/twitter_backbone/images/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/twig.js/master/demos/twitter_backbone/images/reload.png -------------------------------------------------------------------------------- /test/templates/extender.twig: -------------------------------------------------------------------------------- 1 | {% extends 'extendee.twig' %} 2 | 3 | {% block content %} 4 | {{ my.macro({}) }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /test/templates/include-only.twig: -------------------------------------------------------------------------------- 1 | {% include "inc.twig" with {'a': 'before'} only %}-mid-{% include "inc.twig" with {'a': 'after'} only %} -------------------------------------------------------------------------------- /test/templates/include-with.twig: -------------------------------------------------------------------------------- 1 | {% include "inc.twig" with {'a': 'before'} %}-mid-{% include "inc.twig" with { 2 | 'a': 'after' 3 | } %} -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | trim_trailing_whitespace = true 4 | 5 | [*.js] 6 | indent_style = space 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /test/templates/a/child.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | 3 | {% block title %}Other Title{% endblock %} 4 | {% block body %}child{% endblock %} 5 | -------------------------------------------------------------------------------- /test/templates/child-blocks-nested.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | 3 | {% block body %}parent{% block child %}:child{% endblock %}{% endblock %} -------------------------------------------------------------------------------- /test/templates/child.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | 3 | {% block title %}Other Title{% endblock %} 4 | {% block body %}child{% endblock %} 5 | -------------------------------------------------------------------------------- /test/templates/namespaces_without_namespace.twig: -------------------------------------------------------------------------------- 1 | {% include "namespaces/namespaces.twig" %} 2 | 3 | {% include "test::namespaces.twig" %} 4 | -------------------------------------------------------------------------------- /test/templates/use-override-block.twig: -------------------------------------------------------------------------------- 1 | {% use "blocks.twig" %} 2 | {% block msg %}Sorry, can't come to a {{ place }} today.{% endblock %} 3 | -------------------------------------------------------------------------------- /test/templates/source.twig: -------------------------------------------------------------------------------- 1 | {% if isUserNew == true %} 2 | Hello {{ name }} 3 | {% else %} 4 | Welcome back {{ name }} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /test/templates/macro.twig: -------------------------------------------------------------------------------- 1 | {% macro echo(name) %}Hello {{ name }}{% endmacro %} 2 | {% macro whitespace_echo( name ) %}Hello {{ name }}{% endmacro %} 3 | -------------------------------------------------------------------------------- /test/templates/blocks-nested-multiple.twig: -------------------------------------------------------------------------------- 1 | {% block body %}parent{% block child1 %}:child1{% endblock %}{% block child2 %}:child2{% endblock %}{% endblock %} -------------------------------------------------------------------------------- /test/templates/embed-base.twig: -------------------------------------------------------------------------------- 1 | A 2 | {% block header %} 3 | base header 4 | {% endblock %} 5 | {% block footer %} 6 | base footer 7 | {% endblock %} 8 | B 9 | -------------------------------------------------------------------------------- /test/templates/child-parent.twig: -------------------------------------------------------------------------------- 1 | {% extends base %} 2 | 3 | {% block title %}Other Title{% endblock %} 4 | {% block body %}{{ parent() }}:child{% endblock %} 5 | -------------------------------------------------------------------------------- /test/templates/extendee.twig: -------------------------------------------------------------------------------- 1 | {% macro macro() %}ok!{% endmacro %} 2 | {% import _self as my %} 3 | 4 | {% block content %} 5 |

Sub: content

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /test/templates/macro-context.twig: -------------------------------------------------------------------------------- 1 | {% macro make_me_smile(name, slang) %}{{ slang }} {{ name }}{% endmacro %} 2 | {% import _self as greet %}{{ greet.make_me_smile('Twigjs', greetings) }} 3 | -------------------------------------------------------------------------------- /test/templates/macro-defaults.twig: -------------------------------------------------------------------------------- 1 | {% macro make_me_smile(name, slang = 'Howdy') %}{{ slang }} {{ name }}{% endmacro %} 2 | {% import _self as greet %}{{ greet.make_me_smile('Twigjs') }} 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demos 2 | tools 3 | test 4 | test-ext 5 | docs 6 | qtest 7 | .hg 8 | .git 9 | .gitignore 10 | .gitmodules 11 | .travis.yml 12 | .idea 13 | webpack.config.js 14 | bower.json 15 | -------------------------------------------------------------------------------- /test/templates/blocks.twig: -------------------------------------------------------------------------------- 1 | {% block list %}{% for key, item in list %}{{ key }}: {{ item }} / {% endfor %}{% endblock %} 2 | 3 | {% block msg %}Coming soon to a {{ place}} near you!{% endblock %} 4 | -------------------------------------------------------------------------------- /test/templates/from.twig: -------------------------------------------------------------------------------- 1 | {% from "macro.twig" import echo %}{{ echo("Twig.js") }}{% from "macro-wrapped.twig" import wrapped_input as input, red_input %}{{ input('text') }}{{ red_input('password') }} 2 | -------------------------------------------------------------------------------- /src/twig.parser.source.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Twig) { 2 | 'use strict'; 3 | 4 | Twig.Templates.registerParser('source', params => { 5 | return params.data || ''; 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/twig.parser.twig.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Twig) { 2 | 'use strict'; 3 | 4 | Twig.Templates.registerParser('twig', params => { 5 | return new Twig.Template(params); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/templates/block-outer-context.twig: -------------------------------------------------------------------------------- 1 | {% for item in items %} 2 | {% block main %}Hello {{ item }}!{% endblock %} 3 | {% endfor %} 4 | {% block loopy %}{% for name in items %}{{ name }}{% endfor %}{% endblock %} 5 | -------------------------------------------------------------------------------- /test/templates/use-override-nested-block.twig: -------------------------------------------------------------------------------- 1 | {% use "blocks-nested-multiple.twig" %} 2 | {% block body %}parent{% block child1 %}:new-child1{% endblock %}{% block child2 %}:new-child2{% endblock %}{% endblock %} 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 11 5 | - 12 6 | - 13 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | - $HOME/.npm 12 | 13 | notifications: 14 | on_success: false 15 | email: false 16 | -------------------------------------------------------------------------------- /test-ext/checkout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | if [ ! -d $DIR/twig.php ]; then 6 | git clone git://github.com/fabpot/Twig.git $DIR/twig.php 7 | else 8 | cd $DIR/twig.php 9 | git pull 10 | fi 11 | -------------------------------------------------------------------------------- /test/templates/macro-self.twig: -------------------------------------------------------------------------------- 1 | {% macro input(name, value, type, size) %}{% endmacro %} 2 | {% import _self as forms %}

{{ forms.input('username') }}

3 | -------------------------------------------------------------------------------- /src/twig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Twig.js 3 | * 4 | * @copyright 2011-2020 John Roepke and the Twig.js Contributors 5 | * @license Available under the BSD 2-Clause License 6 | * @link https://github.com/twigjs/twig.js 7 | */ 8 | 9 | module.exports = require('./twig.factory')(); 10 | -------------------------------------------------------------------------------- /test/templates/macro-blocks.twig: -------------------------------------------------------------------------------- 1 | {% extends "blocks.twig" %} 2 | {% block list %}{% endblock %} 3 | {% block msg %}{% macro fullname(first, last) %}
{{ first }} {{ last }}
{% endmacro %} 4 | {% import _self as name %}Welcome {{ name.fullname('Twig', 'Js') }}{% endblock %} 5 | -------------------------------------------------------------------------------- /test/templates/macro-self-twice.twig: -------------------------------------------------------------------------------- 1 | {% macro input(name, value, type, size) %}{% endmacro %} 2 | {% import _self as forms %}

{{ forms.input('username') }}

{{ forms.input('password') }}

3 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/model/account.js: -------------------------------------------------------------------------------- 1 | module.declare( 2 | [ 3 | {backbone: 'vendor/backbone'} 4 | ] 5 | , (require, exports, module) => { 6 | const Backbone = require('backbone'); 7 | const Account = Backbone.Model.extend({ }); 8 | 9 | return Account; 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/model/tweet.js: -------------------------------------------------------------------------------- 1 | // Tweet Model 2 | module.declare( 3 | [ 4 | {backbone: 'vendor/backbone'} 5 | ] 6 | , (require, exports, module) => { 7 | const Backbone = require('backbone'); 8 | const Tweet = Backbone.Model.extend({ }); 9 | 10 | exports.Tweet = Tweet; 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /demos/node_express/public/views/pages/note_404.twig: -------------------------------------------------------------------------------- 1 | {% extends json is defined?"../inline.twig":"../layout.twig" %} 2 | 3 | {% block title %}:( Twig.Notebook{% endblock %} 4 | 5 | {% block body %} 6 |

Unable to find the requested note!

7 |
8 | Why not go back to the list and select a note from there? 9 | {% endblock %} -------------------------------------------------------------------------------- /demos/twitter_backbone/README.md: -------------------------------------------------------------------------------- 1 | # Twig.Twitter 2 | 3 | This is a demonstration of using Twig to create a application for viewing a user's Twitter stream. It's fairly simple as such things go, but it demonstrates how to use twig with Backbone.js Views. 4 | 5 | To get started, you should look through the views in js/views to get an idea of how the templates are setup. 6 | 7 | -------------------------------------------------------------------------------- /test/compiler/templates/sub/sub.twig.js: -------------------------------------------------------------------------------- 1 | module.declare([{twig: 'vendor/twig'}], (require, exports, module) => { 2 | const {twig} = require('twig'); 3 | exports.template = twig({id: 'templates/sub/sub.twig', data: [{type: 'raw', value: 'I\'m a '}, {type: 'output', stack: [{type: 'Twig.expression.type.variable', value: 'type', match: ['type']}]}, {type: 'raw', value: '\n'}], precompiled: true}); 4 | }); 5 | -------------------------------------------------------------------------------- /test/compiler/templates/test.twig.js: -------------------------------------------------------------------------------- 1 | twig({id: 'templates/test.twig', data: [{type: 'logic', token: {type: 'Twig.logic.type.if', stack: [{type: 'Twig.expression.type.variable', value: 'test', match: ['test']}], output: [{type: 'raw', value: '\n Yep\n'}]}}, {type: 'logic', token: {type: 'Twig.logic.type.else', match: ['else'], output: [{type: 'raw', value: '\n Nope\n'}]}}, {type: 'raw', value: '\n'}], precompiled: true}); 2 | -------------------------------------------------------------------------------- /test/compiler/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/templates/macro-wrapped.twig: -------------------------------------------------------------------------------- 1 | {% macro wrapped_input(name, value, type, size) %}{% import "macro-self.twig" as forms %}
{{ forms.input(name, value, type, size) }}
{% endmacro %} 2 | {% macro red_input(name, value, type, size) %}{% import "macro-self.twig" as forms %}
{{ forms.input(name, value, type, size) }}
{% endmacro %} 3 | {% import _self as forms %}

{{ forms.wrapped_input('username') }}

4 | -------------------------------------------------------------------------------- /demos/node_express/public/views/pages/index.twig: -------------------------------------------------------------------------------- 1 | {% extends json is defined?"../inline.twig":"../layout.twig" %} 2 | 3 | {% block body %} 4 |

Twig Notebook

5 | 6 |
7 | Welcome to the Twig Notebook demo. 8 | This is a demonstration of how to use Twig.js on both client and server side using Express with progressive enhancement. 9 | 10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /demos/node_express/public/views/pages/note.twig: -------------------------------------------------------------------------------- 1 | {% extends json is defined?"../inline.twig":"../layout.twig" %} 2 | 3 | {% block title %}{{ title }} :: Twig.Notebook{% endblock %} 4 | 5 | {% block body %} 6 | 10 |

{{ title }}

11 |
12 |
13 | {{ markdown }} 14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /demos/twitter_backbone/js/model/settings.js: -------------------------------------------------------------------------------- 1 | // Settings model and collection 2 | module.declare( 3 | [ 4 | {backbone: 'vendor/backbone'} 5 | ] 6 | , (require, exports, module) => { 7 | const Backbone = require('backbone'); 8 | const Setting = Backbone.Model.extend({ }); 9 | const Settings = Backbone.Collection.extend({ 10 | model: Setting, 11 | localStorage: new Backbone.Store('settings') 12 | }); 13 | 14 | exports.Setting = Setting; 15 | exports.Settings = Settings; 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /test/compiler/templates/dir_test.twig.js: -------------------------------------------------------------------------------- 1 | module.declare([{twig: 'vendor/twig'}], (require, exports, module) => { 2 | const {twig} = require('twig'); 3 | exports.template = twig({id: 'templates/dir_test.twig', data: [{type: 'logic', token: {type: 'Twig.logic.type.if', stack: [{type: 'Twig.expression.type.variable', value: 'test', match: ['test']}], output: [{type: 'raw', value: '\n Yep :)\n'}]}}, {type: 'logic', token: {type: 'Twig.logic.type.else', match: ['else'], output: [{type: 'raw', value: '\n Nope :(\n'}]}}, {type: 'raw', value: '\n'}], precompiled: true}); 4 | }); 5 | -------------------------------------------------------------------------------- /docs/release checklist.md: -------------------------------------------------------------------------------- 1 | Steps to release twig.js 2 | 3 | ## repository 4 | 5 | 1. Compile list of changes in CHANGELOG.md 6 | 2. Update version in package.json 7 | 3. Update version in bower.json 8 | 4. Update version in src/twig.header.js 9 | 5. `make`, `make docs`, `make test` and commit the changes. 10 | 6. `git tag` new version 11 | 12 | ## bower 13 | 14 | Bower will pick up the new version in bower.json and use the associated git tag. 15 | 16 | ## npm 17 | 18 | To publish the latest version to npmjs.org run this command from the twig.js directory: 19 | 20 | npm publish 21 | -------------------------------------------------------------------------------- /test/templates/embed-simple.twig: -------------------------------------------------------------------------------- 1 | START 2 | {% embed "embed-base.twig" %} 3 | {% block header %} 4 | new header 5 | {% endblock %} 6 | {% endembed %} 7 | 8 | {% embed "embed-base.twig" %} 9 | {% block footer %} 10 | {{ parent() }}extended 11 | {% endblock %} 12 | {% endembed %} 13 | 14 | {% embed "embed-base.twig" %} 15 | {% block header %} 16 | {{ parent() }}extended 17 | {% endblock %} 18 | 19 | {% block footer %} 20 | {{ parent() }}extended 21 | {% endblock %} 22 | {% endembed %} 23 | 24 | {% embed "embed-base.twig" %} 25 | {% block header %} 26 | {% embed "embed-include.twig" %} 27 | {% block header %} 28 | Super cool new header 29 | {% endblock %} 30 | {% endembed %} 31 | {% endblock %} 32 | {% block footer%} 33 | Cool footer 34 | {% endblock %} 35 | {% endembed %} 36 | END 37 | -------------------------------------------------------------------------------- /demos/node_express/public/views/pages/notes.twig: -------------------------------------------------------------------------------- 1 | {% extends json is defined?"../inline.twig":"../layout.twig" %} 2 | 3 | {% block title %}Twig.Notebook{% endblock %} 4 | 5 | {% block body %} 6 | 10 |

Twig.js Notes

11 | {% if messages is not empty %} 12 |
{{ message }}
13 | {% endif %} 14 |
15 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /demos/twitter_backbone/templates/tweet.twig: -------------------------------------------------------------------------------- 1 | {% if retweeted_status is defined %} 2 | {% set rt_user=retweeted_status.user %} 3 | {% else %} 4 | {% set rt_user=user %} 5 | {% endif %} 6 |
7 |
8 |
{{ created_at|date("M j, Y @ g:i a") }}
9 |
10 | {{ rt_user.name }} 11 | {{ rt_user.screen_name }} 12 |
13 | {% if retweeted_status is defined %} 14 |
{{ retweeted_status.text }}
15 | {% else %} 16 |
{{ text }}
17 | {% endif %} 18 |
19 | -------------------------------------------------------------------------------- /src/twig.factory.js: -------------------------------------------------------------------------------- 1 | // ## twig.factory.js 2 | // 3 | // This file handles creating the Twig library 4 | module.exports = function factory() { 5 | const Twig = { 6 | VERSION: '1.14.0' 7 | }; 8 | 9 | require('./twig.core')(Twig); 10 | require('./twig.compiler')(Twig); 11 | require('./twig.expression')(Twig); 12 | require('./twig.filters')(Twig); 13 | require('./twig.functions')(Twig); 14 | require('./twig.lib')(Twig); 15 | require('./twig.loader.ajax')(Twig); 16 | require('./twig.loader.fs')(Twig); 17 | require('./twig.logic')(Twig); 18 | require('./twig.parser.source')(Twig); 19 | require('./twig.parser.twig')(Twig); 20 | require('./twig.path')(Twig); 21 | require('./twig.tests')(Twig); 22 | require('./twig.async')(Twig); 23 | require('./twig.exports')(Twig); 24 | 25 | Twig.exports.factory = factory; 26 | 27 | return Twig.exports; 28 | }; 29 | -------------------------------------------------------------------------------- /demos/twitter_backbone/templates/app.twig: -------------------------------------------------------------------------------- 1 | {# 2 | # This template displays a twitter feed or error message based on the 3 | # value of the username in the model. 4 | #} 5 | 6 |
7 | Change User 8 | {% if username is not empty %} 9 | Reload Tweets 10 |
11 | Twitter feed for {{ username }} 12 |
13 | {% else %} 14 | Error 15 | {% endif %} 16 |
17 | 18 | {% if username is not empty %} 19 | {# The container for the feed (a FeedView) will be inserted here with JQuery. #} 20 |
21 | {% else %} 22 |
23 | No username provided. Can't load Twitter feed. 24 |
25 | {% endif %} 26 | -------------------------------------------------------------------------------- /demos/twitter_backbone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Twig.Twitter 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demos/node_express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "John Roepke (http://johnroepke.com/)", 3 | "name": "twig-demo-node", 4 | "private": true, 5 | "description": "Demo using node and twig.", 6 | "version": "0.3.0", 7 | "homepage": "https://github.com/twigjs/twig.js", 8 | "licenses": [ 9 | { 10 | "type": "BSD-2-Clause", 11 | "url": "https://raw.github.com/twigjs/twig.js/master/LICENSE" 12 | } 13 | ], 14 | "scripts": { 15 | "install": "cd ../.. && npm it", 16 | "start": "node app.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/twigjs/twig.js.git" 21 | }, 22 | "main": "app.js", 23 | "engines": { 24 | "node": "*" 25 | }, 26 | "dependencies": { 27 | "body-parser": "^1.19.0", 28 | "express": "4.17.x", 29 | "markdown": "0.5.x", 30 | "underscore": "1.9.x", 31 | "twig": "file:../.." 32 | }, 33 | "devDependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /test/test.inheritance.js: -------------------------------------------------------------------------------- 1 | const {factory} = require('../twig'); 2 | 3 | let twig; 4 | 5 | describe('Twig.js Blocks ->', function () { 6 | beforeEach(function () { 7 | twig = factory().twig; 8 | }); 9 | 10 | describe('"extends" tag and inheritance', function () { 11 | it('"extends" applies recursively to grand-parents', function () { 12 | twig({ 13 | id: 'grand-parent.twig', 14 | data: '{% block content %}grand-parent.twig{% endblock%}' 15 | }); 16 | twig({ 17 | id: 'parent.twig', 18 | data: '{% extends "grand-parent.twig" %}' 19 | }); 20 | 21 | twig({ 22 | allowInlineIncludes: true, 23 | data: '{% extends "parent.twig" %}{% block content %}main.twig > {{ parent() }}{% endblock %}' 24 | }).render().should.equal('main.twig > grand-parent.twig'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/test.allowed-tags.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js striptags filter arguments', function () { 6 | it('should apply allowed_tags argument', function () { 7 | const content = 'linktest boldtest

paragraphtest

'; 8 | 9 | const template = twig({ 10 | data: 'template with {{content|striptags(",,

")}}' 11 | }); 12 | const output = template.render({content}); 13 | 14 | output.should.equal('template with linktest boldtest

paragraphtest

'); 15 | }); 16 | 17 | it('should remove tags if no argument passed', function () { 18 | const content = 'linktest boldtest

paragraphtest

'; 19 | 20 | const template = twig({ 21 | data: 'template with {{content|striptags}}' 22 | }); 23 | const output = template.render({content}); 24 | 25 | output.should.equal('template with linktest boldtest paragraphtest'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /demos/node_express/public/views/pages/note_form.twig: -------------------------------------------------------------------------------- 1 | {% extends json is defined?"../inline.twig":"../layout.twig" %} 2 | 3 | {% block title %}{{ id is empty ? "Add Note":"Update " ~ title }} :: Twig.Notebook{% endblock %} 4 | 5 | {% block body %} 6 |
7 | 11 |

{{ id is empty ? "Add":"Update" }} Note

12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | Use of Markdown is encouraged! 24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /test/test.logic.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Logic ->', function () { 6 | describe('set ->', function () { 7 | it('should define variable', function () { 8 | twig({ 9 | data: '{% set list = _context %}{{ list|json_encode }}' 10 | }).render({a: 10, b: 4, c: 2}).should.equal(JSON.stringify({a: 10, b: 4, c: 2})); 11 | }); 12 | }); 13 | 14 | describe('if ->', function () { 15 | it('should ignore spaces', function () { 16 | twig({data: '{% if (1 == 1) %}true{% endif %}'}).render().should.equal('true'); 17 | twig({data: '{% if(1 == 1) %}true{% endif %}'}).render().should.equal('true'); 18 | }); 19 | }); 20 | 21 | describe('elseif ->', function () { 22 | it('should ignore spaces', function () { 23 | twig({data: '{% if (1 == 2) %}false{% elseif (1 == 1) %}true{% endif %}'}).render().should.equal('true'); 24 | twig({data: '{% if (1 == 2) %}false{% elseif(1 == 1) %}true{% endif %}'}).render().should.equal('true'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/app.js: -------------------------------------------------------------------------------- 1 | // # Application JS file 2 | // 3 | // Launch the application. 4 | // 5 | 6 | module.declare( 7 | [ 8 | {settings: 'js/model/settings'}, 9 | {appView: 'js/view/appView'} 10 | ] 11 | , (require, exports, module) => { 12 | const {Settings} = require('settings'); 13 | const {AppView} = require('appView') 14 | 15 | // Models 16 | ; const settingId = 0; 17 | const settings = new Settings(); 18 | 19 | // Load from local storage 20 | settings.fetch(); 21 | 22 | let setting = settings.get(settingId); 23 | let username = null; 24 | 25 | // Initialize the settings model with a username 26 | if (!setting || !setting.get('username')) { 27 | username = prompt('Please enter a twitter username:'); 28 | setting = settings.create({ 29 | id: settingId, 30 | username 31 | }); 32 | setting.save(); 33 | } 34 | 35 | // Create the view and kick off the application 36 | const appView = new AppView({ 37 | model: setting 38 | }); 39 | 40 | $('body').append(appView.el); 41 | } 42 | ); 43 | 44 | -------------------------------------------------------------------------------- /test/test.performance.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | describe('Twig.js Performance Regressions ->', function () { 4 | const template = '{{ echoTest }}\n' + 5 | '{% for item in items %}\n' + 6 | '

{{ item.title }}

\n' + 7 | '{% if item.isActive %}\n' + 8 | '

Active

\n' + 9 | '{% else %}\n' + 10 | '

Inactive

\n' + 11 | '{% endif %}\n' + 12 | '{% endfor %}'; 13 | 14 | it('Should not start running slower', function () { 15 | this.timeout(500); 16 | this.retries(3); 17 | this.slow(300); 18 | 19 | console.time('Should not start running slower '); 20 | for (let i = 0; i < 1000; i++) { 21 | Twig.twig({ 22 | data: template 23 | }).render({ 24 | echoTest: 'Data for echo', 25 | items: [ 26 | { 27 | title: 'This is a title', 28 | isActive: true 29 | }, 30 | { 31 | title: 'This is a title', 32 | isActive: false 33 | } 34 | ] 35 | }); 36 | } 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2015, John Roepke and the Twig.js Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | 11 | -------------------------------------------------------------------------------- /test/test.parsers.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | describe('Twig.js Parsers ->', function () { 4 | describe('custom parser ->', function () { 5 | it('should define a custom parser', function () { 6 | Twig.extend(Twig => { 7 | const parser = function (params) { 8 | return '[CUSTOM PARSER] ' + params.data; 9 | }; 10 | 11 | Twig.Templates.registerParser('custom', parser); 12 | Twig.Templates.parsers.should.have.property('custom'); 13 | }); 14 | }); 15 | 16 | it('should run the data through the custom parser', function () { 17 | Twig.extend(Twig => { 18 | const params = { 19 | data: 'This is a test template.' 20 | }; 21 | const template = Twig.Templates.parsers.custom(params); 22 | 23 | template.should.equal('[CUSTOM PARSER] This is a test template.'); 24 | }); 25 | }); 26 | 27 | it('should remove a registered parser', function () { 28 | Twig.extend(Twig => { 29 | Twig.Templates.unRegisterParser('custom'); 30 | Twig.Templates.parsers.should.not.have.property('custom'); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /demos/node_express/public/views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% block title %}Twig.Notebook{% endblock %} 24 | 25 | 26 |
27 | {% block body %}{% endblock %} 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /demos/twitter_backbone/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | port = process.argv[2] || process.env.PORT || 8888, 6 | host = process.argv[3] || process.env.IP || '0.0.0.0'; 7 | 8 | http.createServer((request, response) => { 9 | const uri = url.parse(request.url).pathname; 10 | let filename = path.join(__dirname, uri); 11 | 12 | path.exists(filename, exists => { 13 | if (!exists) { 14 | response.writeHead(404, {'Content-Type': 'text/plain'}); 15 | response.write('404 Not Found\n'); 16 | response.end(); 17 | return; 18 | } 19 | 20 | if (fs.statSync(filename).isDirectory()) { 21 | filename += '/index.html'; 22 | } 23 | 24 | fs.readFile(filename, 'binary', (err, file) => { 25 | if (err) { 26 | response.writeHead(500, {'Content-Type': 'text/plain'}); 27 | response.write(err + '\n'); 28 | response.end(); 29 | return; 30 | } 31 | 32 | response.writeHead(200); 33 | response.write(file, 'binary'); 34 | response.end(); 35 | }); 36 | }); 37 | }).listen(parseInt(port, 10), host); 38 | 39 | console.log('Twig.Twitter demo running at\n => ' + host + ':' + port + '/\nCTRL + C to shutdown'); 40 | 41 | -------------------------------------------------------------------------------- /test/test.factory.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig'); 2 | 3 | const FreshTwig = Twig.factory(); 4 | 5 | describe('Twig.js Factory ->', function () { 6 | Twig.extendFunction('foo', () => { 7 | return 'foo'; 8 | }); 9 | 10 | FreshTwig.extendFunction('bar', () => { 11 | return 'bar'; 12 | }); 13 | 14 | it('should not have access to extensions on the main Twig object', function () { 15 | const fixtOptions = { 16 | rethrow: true, 17 | data: '{{ foo() }}' 18 | }; 19 | 20 | Twig.twig(fixtOptions).render(); 21 | 22 | try { 23 | FreshTwig.twig(fixtOptions).render(); 24 | throw new Error('should have thrown an error'); 25 | } catch (error) { 26 | error.message.should.equal('foo function does not exist and is not defined in the context'); 27 | } 28 | }); 29 | 30 | it('should not leak extensions to the main Twig object', function () { 31 | const fixtOptions = { 32 | rethrow: true, 33 | data: '{{ bar() }}' 34 | }; 35 | 36 | FreshTwig.twig(fixtOptions).render(); 37 | 38 | try { 39 | Twig.twig(fixtOptions).render(); 40 | throw new Error('should have thrown an error'); 41 | } catch (error) { 42 | error.message.should.equal('bar function does not exist and is not defined in the context'); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const TerserJsPlugin = require('terser-webpack-plugin'); 2 | 3 | const commonModule = { 4 | exclude: /(node_modules)/, 5 | use: { 6 | loader: "babel-loader", 7 | options: { 8 | presets: ["@babel/preset-env"], 9 | plugins: [ 10 | "@babel/plugin-transform-modules-commonjs", 11 | "@babel/plugin-transform-runtime" 12 | ] 13 | } 14 | } 15 | }; 16 | 17 | const serverBuild = { 18 | mode: 'production', 19 | entry: './src/twig.js', 20 | target: 'node', 21 | node: false, 22 | output: { 23 | path: __dirname, 24 | filename: 'twig.js', 25 | library: 'Twig', 26 | libraryTarget: 'umd' 27 | }, 28 | module: { 29 | rules: [commonModule], 30 | }, 31 | optimization: { 32 | minimize: false 33 | } 34 | }; 35 | 36 | const clientBuild = { 37 | mode: 'production', 38 | entry: './src/twig.js', 39 | target: 'web', 40 | devtool: '#source-map', 41 | node: { 42 | __dirname: false, 43 | __filename: false 44 | }, 45 | module: { 46 | rules: [commonModule] 47 | }, 48 | output: { 49 | path: __dirname, 50 | filename: 'twig.min.js', 51 | library: 'Twig', 52 | libraryTarget: 'umd' 53 | }, 54 | optimization: { 55 | minimize: true, 56 | minimizer: [new TerserJsPlugin({ 57 | sourceMap: true, 58 | include: /\.min\.js$/ 59 | })] 60 | } 61 | }; 62 | 63 | module.exports = [serverBuild, clientBuild]; 64 | -------------------------------------------------------------------------------- /test/test.exports.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | describe('Twig.js Exports __express ->', function () { 4 | /* Otherwise express will return it as JSON, see: https://github.com/twigjs/twig.js/pull/348 for more information */ 5 | it('should return a string (and not a String)', function (done) { 6 | Twig.__express('test/templates/test.twig', { 7 | settings: { 8 | 'twig options': { 9 | autoescape: 'html' 10 | } 11 | } 12 | }, (err, response) => { 13 | (err === null).should.be.true(); 14 | 15 | const responseType = (typeof response); 16 | responseType.should.equal('string'); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should allow async rendering', function (done) { 22 | Twig.__express('test/templates/test-async.twig', { 23 | settings: { 24 | 'twig options': { 25 | allowAsync: true 26 | } 27 | }, 28 | /* eslint-disable-next-line camelcase */ 29 | hello_world() { 30 | return Promise.resolve('hello world'); 31 | } 32 | }, (err, response) => { 33 | if (err) { 34 | return done(err); 35 | } 36 | 37 | try { 38 | const responseType = (typeof response); 39 | responseType.should.equal('string'); 40 | response.should.equal('hello world\n'); 41 | done(); 42 | } catch (error) { 43 | done(error); 44 | } 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/model/feed.js: -------------------------------------------------------------------------------- 1 | module.declare( 2 | [ 3 | {backbone: 'vendor/backbone'}, 4 | {underscore: 'vendor/underscore'}, 5 | {tweet: 'js/model/tweet'} 6 | ], 7 | (require, exports, module) => { 8 | const Backbone = require('backbone'); 9 | const {_} = require('underscore'); 10 | const {Tweet} = require('tweet'); 11 | const Feed = Backbone.Collection.extend({ 12 | localStorage: new Backbone.Store('tweets'), 13 | model: Tweet, 14 | 15 | loadUser(username) { 16 | const that = this; 17 | let request; 18 | while (this.length > 0) { 19 | this.each(tweet => { 20 | tweet.destroy(); 21 | }); 22 | } 23 | 24 | request = $.ajax({ 25 | url: 'https://api.twitter.com/1/statuses/user_timeline.json?callback=?', 26 | dataType: 'json', 27 | data: { 28 | include_entities: 'true', 29 | include_rts: 'true', 30 | screen_name: username 31 | } 32 | }); 33 | 34 | request.done(data => { 35 | _.each(data, tweet => { 36 | const newTweet = that.create(tweet); 37 | }); 38 | }); 39 | 40 | request.error((jqXHR, status) => { 41 | alert('Unable to load tweets, error:\n' + status); 42 | }); 43 | } 44 | }); 45 | exports.feed = new Feed(); 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /test/test.regression.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Regression Tests ->', function () { 6 | it('#47 should not match variables starting with not', function () { 7 | // Define and save a template 8 | twig({data: '{% for note in notes %}{{note}}{% endfor %}'}).render({notes: ['a', 'b', 'c']}).should.equal('abc'); 9 | }); 10 | 11 | it('#56 functions work inside parentheses', function () { 12 | // Define and save a template 13 | Twig.extendFunction('custom', _ => { 14 | return true; 15 | }); 16 | 17 | twig({data: '{% if (custom("val") and custom("val")) %}out{% endif %}'}).render({}).should.equal('out'); 18 | }); 19 | 20 | it('#83 Support for trailing commas in arrays', function () { 21 | twig({data: '{{ [1,2,3,4,] }}'}).render().should.equal('1,2,3,4'); 22 | }); 23 | 24 | it('#83 Support for trailing commas in objects', function () { 25 | twig({data: '{{ {a:1, b:2, c:3, } }}'}).render(); 26 | }); 27 | 28 | it('#283 should support quotes between raw tags', function () { 29 | twig({data: '{% raw %}\n"\n{% endraw %}'}).render().should.equal('"'); 30 | twig({data: '{% raw %}\n\'\n{% endraw %}'}).render().should.equal('\''); 31 | }); 32 | 33 | it('#737 ternary expression should not override context', function () { 34 | const str = `{% set classes = ['a', 'b'] %}{% set classes = classes ? classes|merge(['c']) : '' %}{{ dump(classes) }}`; 35 | const expected = Twig.functions.dump(['a', 'b', 'c']); 36 | const testTemplate = twig({data: str}); 37 | testTemplate.render().should.equal(expected); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/twig.compiler.js: -------------------------------------------------------------------------------- 1 | // ## twig.compiler.js 2 | // 3 | // This file handles compiling templates into JS 4 | module.exports = function (Twig) { 5 | /** 6 | * Namespace for compilation. 7 | */ 8 | Twig.compiler = { 9 | module: {} 10 | }; 11 | 12 | // Compile a Twig Template to output. 13 | Twig.compiler.compile = function (template, options) { 14 | // Get tokens 15 | const tokens = JSON.stringify(template.tokens); 16 | const {id} = template; 17 | let output = null; 18 | 19 | if (options.module) { 20 | if (Twig.compiler.module[options.module] === undefined) { 21 | throw new Twig.Error('Unable to find module type ' + options.module); 22 | } 23 | 24 | output = Twig.compiler.module[options.module](id, tokens, options.twig); 25 | } else { 26 | output = Twig.compiler.wrap(id, tokens); 27 | } 28 | 29 | return output; 30 | }; 31 | 32 | Twig.compiler.module = { 33 | amd(id, tokens, pathToTwig) { 34 | return 'define(["' + pathToTwig + '"], function (Twig) {\n\tvar twig, templates;\ntwig = Twig.twig;\ntemplates = ' + Twig.compiler.wrap(id, tokens) + '\n\treturn templates;\n});'; 35 | }, 36 | node(id, tokens) { 37 | return 'var twig = require("twig").twig;\nexports.template = ' + Twig.compiler.wrap(id, tokens); 38 | }, 39 | cjs2(id, tokens, pathToTwig) { 40 | return 'module.declare([{ twig: "' + pathToTwig + '" }], function (require, exports, module) {\n\tvar twig = require("twig").twig;\n\texports.template = ' + Twig.compiler.wrap(id, tokens) + '\n});'; 41 | } 42 | }; 43 | 44 | Twig.compiler.wrap = function (id, tokens) { 45 | return 'twig({id:"' + id.replace('"', '\\"') + '", data:' + tokens + ', precompiled: true});\n'; 46 | }; 47 | 48 | return Twig; 49 | }; 50 | -------------------------------------------------------------------------------- /src/twig.loader.ajax.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Twig) { 2 | 'use strict'; 3 | 4 | Twig.Templates.registerLoader('ajax', function (location, params, callback, errorCallback) { 5 | let template; 6 | const {precompiled} = params; 7 | const parser = this.parsers[params.parser] || this.parser.twig; 8 | 9 | if (typeof XMLHttpRequest === 'undefined') { 10 | throw new Twig.Error('Unsupported platform: Unable to do ajax requests ' + 11 | 'because there is no "XMLHTTPRequest" implementation'); 12 | } 13 | 14 | const xmlhttp = new XMLHttpRequest(); 15 | xmlhttp.onreadystatechange = function () { 16 | let data = null; 17 | 18 | if (xmlhttp.readyState === 4) { 19 | if (xmlhttp.status === 200 || (window.cordova && xmlhttp.status === 0)) { 20 | Twig.log.debug('Got template ', xmlhttp.responseText); 21 | 22 | if (precompiled === true) { 23 | data = JSON.parse(xmlhttp.responseText); 24 | } else { 25 | data = xmlhttp.responseText; 26 | } 27 | 28 | params.url = location; 29 | params.data = data; 30 | 31 | template = parser.call(this, params); 32 | 33 | if (typeof callback === 'function') { 34 | callback(template); 35 | } 36 | } else if (typeof errorCallback === 'function') { 37 | errorCallback(xmlhttp); 38 | } 39 | } 40 | }; 41 | 42 | xmlhttp.open('GET', location, Boolean(params.async)); 43 | xmlhttp.overrideMimeType('text/plain'); 44 | xmlhttp.send(); 45 | 46 | if (params.async) { 47 | // TODO: return deferred promise 48 | return true; 49 | } 50 | 51 | return template; 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "John Roepke (http://john.sh/)", 3 | "name": "twig", 4 | "description": "JS port of the Twig templating language.", 5 | "version": "1.15.4", 6 | "homepage": "https://github.com/twigjs/twig.js", 7 | "license": "BSD-2-Clause", 8 | "licenses": [ 9 | { 10 | "type": "BSD-2-Clause", 11 | "url": "https://raw.github.com/twigjs/twig.js/master/LICENSE" 12 | } 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/twigjs/twig.js.git" 17 | }, 18 | "main": "twig.js", 19 | "engines": { 20 | "node": ">=8.16" 21 | }, 22 | "bin": { 23 | "twigjs": "./bin/twigjs" 24 | }, 25 | "scripts": { 26 | "preversion": "npm test && git diff --exit-code --quiet", 27 | "postversion": "git push origin master && git push origin master --tags", 28 | "pretest": "npm run build", 29 | "test": "mocha -r should", 30 | "build": "webpack", 31 | "posttest": "xo src lib bin" 32 | }, 33 | "dependencies": { 34 | "@babel/runtime": "^7.8.4", 35 | "locutus": "^2.0.11", 36 | "minimatch": "3.0.x", 37 | "walk": "2.3.x" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.8.4", 41 | "@babel/plugin-transform-runtime": "^7.8.3", 42 | "@babel/preset-env": "^7.8.4", 43 | "babel-loader": "^8.0.6", 44 | "eslint-plugin-mocha": "^6.3.0", 45 | "mocha": "^7.0.1", 46 | "should": "^13.2.3", 47 | "should-sinon": "0.0.6", 48 | "sinon": "^9.0.0", 49 | "terser-webpack-plugin": "^2.3.5", 50 | "tokenizer": "1.1.x", 51 | "webpack": "^4.41.6", 52 | "webpack-cli": "^3.3.11", 53 | "xo": "^0.26.1" 54 | }, 55 | "browser": { 56 | "fs": false 57 | }, 58 | "xo": { 59 | "space": 4, 60 | "envs": [ 61 | "browser", 62 | "node", 63 | "mocha" 64 | ], 65 | "plugins": [ 66 | "mocha" 67 | ], 68 | "rules": { 69 | "promise/prefer-await-to-then": 0, 70 | "prefer-arrow-callback": 0, 71 | "mocha/prefer-arrow-callback": 2 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/view/feedView.js: -------------------------------------------------------------------------------- 1 | // # Feed View 2 | // 3 | // This is the view module for a Twitter feed. 4 | // 5 | 6 | module.declare( 7 | [ 8 | {backbone: 'vendor/backbone'}, 9 | {feed: 'js/model/feed'}, 10 | {tweetView: 'js/view/tweetView'} 11 | ] 12 | , (require, exports, module) => { 13 | const {feed} = require('feed'); 14 | const Backbone = require('backbone'); 15 | const {TweetView} = require('tweetView') 16 | 17 | // The FeedView is a simple container of TweetViews and therefore 18 | // doesn't need a template. The ul element provided by the Backbone 19 | // View is sufficient. 20 | ; const FeedView = Backbone.View.extend({ 21 | tagName: 'ul', 22 | className: 'feed', 23 | 24 | initialize() { 25 | // Bind to model changes 26 | feed.bind('add', this.addTweet, this); 27 | feed.bind('reset', this.addAll, this); 28 | 29 | // Load stored tweets from local storage 30 | feed.fetch(); 31 | }, 32 | 33 | // Add a tweet to the view 34 | // Creates a new TweetView for the tweet model 35 | // and adds it to the FeedView 36 | addTweet(tweet) { 37 | const tweetView = new TweetView({ 38 | model: tweet 39 | }); 40 | const {el} = tweetView.render(); 41 | $(this.el).append(el); 42 | 43 | return this; 44 | }, 45 | 46 | // Handle resets to the model by adding all new elements to the view 47 | // Existing tweet views will have been removed when their models are 48 | // destroyed. 49 | addAll() { 50 | const that = this; 51 | feed.each(tweet => { 52 | that.addTweet(tweet); 53 | }); 54 | } 55 | }); 56 | 57 | exports.FeedView = FeedView; 58 | } 59 | ); 60 | -------------------------------------------------------------------------------- /lib/paths.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | 3 | const sepChr = '/'; 4 | 5 | exports.relativePath = function (base, file) { 6 | let basePath = exports.normalize(base.split(sepChr)); 7 | const newPath = []; 8 | let val; 9 | 10 | // Remove file from url 11 | basePath.pop(); 12 | basePath = basePath.concat(file.split(sepChr)); 13 | 14 | while (basePath.length > 0) { 15 | val = basePath.shift(); 16 | if (val === '.') { 17 | // Ignore 18 | } else if (val === '..' && newPath.length > 0 && newPath[newPath.length - 1] !== '..') { 19 | newPath.pop(); 20 | } else { 21 | newPath.push(val); 22 | } 23 | } 24 | 25 | return newPath.join(sepChr); 26 | }; 27 | 28 | exports.findBase = function (file) { 29 | const paths = exports.normalize(file.split(sepChr)); 30 | // We want everything before the file 31 | if (paths.length > 1) { 32 | // Get rid of the filename 33 | paths.pop(); 34 | return paths.join(sepChr) + sepChr; 35 | } 36 | 37 | // We're in the file directory 38 | return ''; 39 | }; 40 | 41 | exports.removePath = function (path, file) { 42 | if (!path) { 43 | return ''; 44 | } 45 | 46 | const filePath = exports.normalize(file.split(sepChr)); 47 | 48 | return filePath.join(sepChr); 49 | }; 50 | 51 | exports.normalize = function (fileArr) { 52 | const newArr = []; 53 | let val; 54 | while (fileArr.length > 0) { 55 | val = fileArr.shift(); 56 | if (val !== '') { 57 | newArr.push(val); 58 | } 59 | } 60 | 61 | return newArr; 62 | }; 63 | 64 | exports.stripSlash = function (path) { 65 | if (path.slice(-1) === '/') { 66 | path = path.slice(0, Math.max(0, path.length - 1)); 67 | } 68 | 69 | return path; 70 | }; 71 | 72 | exports.mkdir = function (dir) { 73 | try { 74 | FS.mkdirSync(dir); 75 | } catch (error) { 76 | if (error.code === 'EEXIST') { 77 | // ignore if it's a "EEXIST" exeption 78 | } else { 79 | console.log(error); 80 | throw error; 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /demos/twitter_backbone/less/css3.less: -------------------------------------------------------------------------------- 1 | .box-shadow (@x: 0, @y: 0, @blurx: 1px, @blury: 1px, @color: #000) { 2 | -moz-box-shadow: @arguments; 3 | -webkit-box-shadow: @arguments; 4 | box-shadow: @arguments; 5 | } 6 | 7 | .inset-box-shadow (@x: 0, @y: 0, @blur: 1px, @size: 1px, @color: #000) { 8 | -moz-box-shadow: inset @arguments; 9 | -webkit-box-shadow: inset @arguments; 10 | box-shadow: inset @arguments; 11 | } 12 | 13 | .background-gradient (@start, @end) { 14 | background-image: -webkit-gradient(linear, left top, left bottom, from(@start), to(@end)); 15 | background-image: -webkit-linear-gradient(top, @start, @end); 16 | background-image: -moz-linear-gradient(top, @start, @end); 17 | background-image: -ms-linear-gradient(top, @start, @end); 18 | background-image: -o-linear-gradient(top, @start, @end); 19 | background-image: linear-gradient(top, @start, @end); 20 | } 21 | 22 | .border-radius (@radius) { 23 | -moz-border-radius: @radius; 24 | -webkit-border-radius: @radius; 25 | border-radius: @radius; 26 | } 27 | 28 | .border-radius-top (@radius) { 29 | -webkit-border-top-right-radius: @radius; 30 | -webkit-border-top-left-radius: @radius; 31 | -moz-border-radius-topright: @radius; 32 | -moz-border-radius-topleft: @radius; 33 | border-top-right-radius: @radius; 34 | border-top-left-radius: @radius; 35 | } 36 | 37 | .border-radius-bottom (@radius) { 38 | -webkit-border-bottom-right-radius: @radius; 39 | -webkit-border-bottom-left-radius: @radius; 40 | -moz-border-radius-bottomright: @radius; 41 | -moz-border-radius-bottomleft: @radius; 42 | border-bottom-right-radius: @radius; 43 | border-bottom-left-radius: @radius; 44 | } 45 | 46 | .transition (@property, @duration, @timing) { 47 | -moz-transition: @arguments; 48 | -o-transition: @arguments; 49 | -ms-transition: @arguments; 50 | -webkit-transition: @arguments; 51 | transition: @arguments; 52 | } 53 | 54 | .transform-y (@distance) { 55 | -webkit-transform: translate3D(0, @distance, 0); 56 | -moz-transform: translateY(@distance); 57 | -ms-transform: translate3D(0, @distance, 0); 58 | -o-transform: translate3D(0, @distance, 0); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /demos/node_express/public/less/css3.less: -------------------------------------------------------------------------------- 1 | .box-shadow (@x: 0, @y: 0, @blurx: 1px, @blury: 1px, @color: #000) { 2 | -moz-box-shadow: @arguments; 3 | -webkit-box-shadow: @arguments; 4 | box-shadow: @arguments; 5 | } 6 | 7 | .inset-box-shadow (@x: 0, @y: 0, @blur: 1px, @size: 1px, @color: #000) { 8 | -moz-box-shadow: inset @arguments; 9 | -webkit-box-shadow: inset @arguments; 10 | box-shadow: inset @arguments; 11 | } 12 | 13 | .background-gradient (@start, @end) { 14 | background-image: -webkit-gradient(linear, left top, left bottom, from(@start), to(@end)); 15 | background-image: -webkit-linear-gradient(top, @start, @end); 16 | background-image: -moz-linear-gradient(top, @start, @end); 17 | background-image: -ms-linear-gradient(top, @start, @end); 18 | background-image: -o-linear-gradient(top, @start, @end); 19 | background-image: linear-gradient(top, @start, @end); 20 | } 21 | 22 | .border-radius (@radius) { 23 | -moz-border-radius: @radius; 24 | -webkit-border-radius: @radius; 25 | border-radius: @radius; 26 | } 27 | 28 | .border-radius-top (@radius) { 29 | -webkit-border-top-right-radius: @radius; 30 | -webkit-border-top-left-radius: @radius; 31 | -moz-border-radius-topright: @radius; 32 | -moz-border-radius-topleft: @radius; 33 | border-top-right-radius: @radius; 34 | border-top-left-radius: @radius; 35 | } 36 | 37 | .border-radius-bottom (@radius) { 38 | -webkit-border-bottom-right-radius: @radius; 39 | -webkit-border-bottom-left-radius: @radius; 40 | -moz-border-radius-bottomright: @radius; 41 | -moz-border-radius-bottomleft: @radius; 42 | border-bottom-right-radius: @radius; 43 | border-bottom-left-radius: @radius; 44 | } 45 | 46 | .transition (@property, @duration, @timing) { 47 | -moz-transition: @arguments; 48 | -o-transition: @arguments; 49 | -ms-transition: @arguments; 50 | -webkit-transition: @arguments; 51 | transition: @arguments; 52 | } 53 | 54 | .transform-y (@distance) { 55 | -webkit-transform: translate3D(0, @distance, 0); 56 | -moz-transform: translateY(@distance); 57 | -ms-transform: translate3D(0, @distance, 0); 58 | -o-transform: translate3D(0, @distance, 0); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/twig.tests.js: -------------------------------------------------------------------------------- 1 | // ## twig.tests.js 2 | // 3 | // This file handles expression tests. (is empty, is not defined, etc...) 4 | module.exports = function (Twig) { 5 | 'use strict'; 6 | Twig.tests = { 7 | empty(value) { 8 | if (value === null || value === undefined) { 9 | return true; 10 | } 11 | 12 | // Handler numbers 13 | if (typeof value === 'number') { 14 | return false; 15 | } // Numbers are never "empty" 16 | 17 | // Handle strings and arrays 18 | if (value.length > 0) { 19 | return false; 20 | } 21 | 22 | // Handle objects 23 | for (const key in value) { 24 | if (Object.hasOwnProperty.call(value, key)) { 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | }, 31 | odd(value) { 32 | return value % 2 === 1; 33 | }, 34 | even(value) { 35 | return value % 2 === 0; 36 | }, 37 | divisibleby(value, params) { 38 | return value % params[0] === 0; 39 | }, 40 | defined(value) { 41 | return value !== undefined; 42 | }, 43 | none(value) { 44 | return value === null; 45 | }, 46 | null(value) { 47 | return this.none(value); // Alias of none 48 | }, 49 | 'same as'(value, params) { 50 | return value === params[0]; 51 | }, 52 | sameas(value, params) { 53 | console.warn('`sameas` is deprecated use `same as`'); 54 | return Twig.tests['same as'](value, params); 55 | }, 56 | iterable(value) { 57 | return value && (Twig.lib.is('Array', value) || Twig.lib.is('Object', value)); 58 | } 59 | /* 60 | Constant ? 61 | */ 62 | }; 63 | 64 | Twig.test = function (test, value, params) { 65 | if (!Twig.tests[test]) { 66 | throw Twig.Error('Test ' + test + ' is not defined.'); 67 | } 68 | 69 | return Twig.tests[test](value, params); 70 | }; 71 | 72 | Twig.test.extend = function (test, definition) { 73 | Twig.tests[test] = definition; 74 | }; 75 | 76 | return Twig; 77 | }; 78 | -------------------------------------------------------------------------------- /src/twig.lib.js: -------------------------------------------------------------------------------- 1 | // ## twig.lib.js 2 | // 3 | // This file contains 3rd party libraries used within twig. 4 | // 5 | // Copies of the licenses for the code included here can be found in the 6 | // LICENSES.md file. 7 | // 8 | 9 | module.exports = function (Twig) { 10 | // Namespace for libraries 11 | Twig.lib = { }; 12 | 13 | Twig.lib.sprintf = require('locutus/php/strings/sprintf'); 14 | Twig.lib.vsprintf = require('locutus/php/strings/vsprintf'); 15 | Twig.lib.round = require('locutus/php/math/round'); 16 | Twig.lib.max = require('locutus/php/math/max'); 17 | Twig.lib.min = require('locutus/php/math/min'); 18 | Twig.lib.stripTags = require('locutus/php/strings/strip_tags'); 19 | Twig.lib.strtotime = require('locutus/php/datetime/strtotime'); 20 | Twig.lib.date = require('locutus/php/datetime/date'); 21 | Twig.lib.boolval = require('locutus/php/var/boolval'); 22 | 23 | Twig.lib.is = function (type, obj) { 24 | if (typeof obj === 'undefined' || obj === null) { 25 | return false; 26 | } 27 | 28 | switch (type) { 29 | case 'Array': 30 | return Array.isArray(obj); 31 | case 'Date': 32 | return obj instanceof Date; 33 | case 'String': 34 | return (typeof obj === 'string' || obj instanceof String); 35 | case 'Number': 36 | return (typeof obj === 'number' || obj instanceof Number); 37 | case 'Function': 38 | return (typeof obj === 'function'); 39 | case 'Object': 40 | return obj instanceof Object; 41 | default: 42 | return false; 43 | } 44 | }; 45 | 46 | Twig.lib.replaceAll = function (string, search, replace) { 47 | // Escape possible regular expression syntax 48 | const searchEscaped = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 49 | 50 | return string.replace(new RegExp(searchEscaped, 'g'), replace); 51 | }; 52 | 53 | // Chunk an array (arr) into arrays of (size) items, returns an array of arrays, or an empty array on invalid input 54 | Twig.lib.chunkArray = function (arr, size) { 55 | const returnVal = []; 56 | let x = 0; 57 | const len = arr.length; 58 | 59 | if (size < 1 || !Array.isArray(arr)) { 60 | return []; 61 | } 62 | 63 | while (x < len) { 64 | returnVal.push(arr.slice(x, x += size)); 65 | } 66 | 67 | return returnVal; 68 | }; 69 | 70 | return Twig; 71 | }; 72 | -------------------------------------------------------------------------------- /test/test.tags.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | const sinon = require('sinon'); 3 | 4 | const {twig} = Twig; 5 | 6 | describe('Twig.js Tags ->', function () { 7 | it('should support spaceless', function () { 8 | twig({ 9 | data: '{% spaceless %}
\n b i\n
{% endspaceless %}' 10 | }).render().should.equal( 11 | '
bi
' 12 | ); 13 | }); 14 | 15 | it('should not escape static values when using spaceless', function () { 16 | twig({ 17 | autoescape: true, 18 | data: '{% spaceless %}
{% endspaceless %}' 19 | }).render().should.equal( 20 | '
' 21 | ); 22 | }); 23 | 24 | it('should support with', function () { 25 | twig({ 26 | autoescape: true, 27 | data: '{% set prefix = "Hello" %}{% with { name: "world" } %}{{prefix}} {{name}}{% endwith %}' 28 | }).render().should.equal( 29 | 'Hello world' 30 | ); 31 | }); 32 | 33 | it('should limit scope of with only', function () { 34 | twig({ 35 | autoescape: true, 36 | data: '{% set prefix = "Hello" %}{% with { name: "world" } only %}{{prefix}} {{name}}{% endwith %}' 37 | }).render().should.equal( 38 | ' world' 39 | ); 40 | }); 41 | 42 | it('should support apply upper', function () { 43 | twig({ 44 | data: '{% apply upper %}twigjs{% endapply %}' 45 | }).render().should.equal( 46 | 'TWIGJS' 47 | ); 48 | }); 49 | 50 | it('should support apply lower|escape', function () { 51 | twig({ 52 | data: '{% apply lower|escape %}Twig.js{% endapply %}' 53 | }).render().should.equal( 54 | '<strong>twig.js</strong>' 55 | ); 56 | }); 57 | 58 | it('should support deprecated tag and show a console warn message', function () { 59 | const consoleSpy = sinon.spy(console, 'warn'); 60 | 61 | twig({ 62 | data: '{% deprecated \'`foo` is deprecated use `bar`\' %}' 63 | }).render(); 64 | 65 | consoleSpy.should.be.calledWith('Deprecation notice: \'`foo` is deprecated use `bar`\''); 66 | }); 67 | 68 | it('should support do', function () { 69 | twig({data: '{% do 1 + 2 %}'}).render().should.equal(''); 70 | twig({data: '{% do arr %}'}).render({arr:[1]}).should.equal(''); 71 | twig({data: `{% do arr.foo(" 72 | multiline", argument) %}`}).render().should.equal(''); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /demos/node_express/public/less/styles.less: -------------------------------------------------------------------------------- 1 | @import "css3.less"; 2 | body { 3 | padding:0; 4 | margin:0; 5 | overflow: hidden; 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | h1, h2 { 10 | font-size: 28pt; 11 | text-align: center; 12 | border-bottom: 1px solid #777; 13 | padding-bottom: 10px; 14 | margin: 5px 10px 10px; 15 | } 16 | h2 { 17 | font-size: 20pt; 18 | } 19 | 20 | .note_container { 21 | width: 400px; 22 | margin: 0 auto; 23 | padding: 10px 10px 20px; 24 | background: #F7F6F0; 25 | } 26 | 27 | .button, a.button { 28 | cursor: default; 29 | font-size: 100%; 30 | vertical-align: baseline; 31 | display:inline-block; 32 | outline:none; 33 | margin:5px auto; 34 | padding: 3px 7px; 35 | text-decoration: none; 36 | color: black; 37 | background: #F5F3EB; 38 | .background-gradient(#F5F3EB, #E8E7DF); 39 | border: 1px solid #D1D0C7; 40 | .border-radius(5px); 41 | line-height:130%; 42 | float: left; 43 | } 44 | .button.right { 45 | float:right; 46 | } 47 | 48 | .welcome { 49 | margin: 20px; 50 | 51 | nav { 52 | margin-top: 20px; 53 | text-align:center; 54 | 55 | .button { 56 | float: none; 57 | } 58 | } 59 | } 60 | 61 | nav { 62 | padding: 0 10px; 63 | .button { 64 | margin:10px 0 0; 65 | } 66 | } 67 | 68 | .notes { 69 | ul { 70 | list-style-type: none; 71 | padding:0; 72 | margin: 15px 5px; 73 | li { 74 | padding:0; 75 | margin:5px 0; 76 | 77 | a { 78 | color: black; 79 | } 80 | .count { 81 | color: #777; 82 | font-size: 70%; 83 | } 84 | } 85 | } 86 | } 87 | 88 | .note { 89 | margin: 10px; 90 | 91 | .text { 92 | padding: 0 10px; 93 | font-family: 'Handlee', cursive; 94 | font-size: 110%; 95 | } 96 | } 97 | 98 | .edit_note { 99 | margin: 0 10px; 100 | 101 | .field { 102 | display: block; 103 | margin: 10px 0; 104 | 105 | label { 106 | display: block; 107 | } 108 | 109 | input, textarea { 110 | margin: 0; 111 | width: 98%; 112 | } 113 | textarea { 114 | height: 200px; 115 | font-family: 'Handlee', cursive; 116 | font-size: 110%; 117 | } 118 | } 119 | } 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /test/test.rethrow.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Twig = require('../twig').factory(); 3 | 4 | const {twig} = Twig; 5 | 6 | describe('Twig.js Rethrow ->', function () { 7 | it('should throw a "Unable to parse \'missing\'" exception', function () { 8 | /* eslint-disable-next-line no-use-extend-native/no-use-extend-native */ 9 | (function () { 10 | twig({ 11 | rethrow: true, 12 | data: 'include missing template {% missing %}' 13 | }).render(); 14 | }).should.throw('Unable to parse \'missing\''); 15 | }); 16 | 17 | it('should throw a "Unable to find closing bracket \'%}" exception', function () { 18 | /* eslint-disable-next-line no-use-extend-native/no-use-extend-native */ 19 | (function () { 20 | twig({ 21 | rethrow: true, 22 | data: 'missing closing bracket {% }' 23 | }).render(); 24 | }).should.throw('Unable to find closing bracket \'%}\' opened near template position 26'); 25 | }); 26 | 27 | it('should throw a compile error having its file property set to the file', function (done) { 28 | try { 29 | const template = twig({ 30 | path: 'test/templates/error/compile/entry.twig', 31 | async: false, 32 | rethrow: true 33 | }); 34 | 35 | done(template); 36 | } catch (error) { 37 | error.should.have.property('file', 'test/templates/error/compile/entry.twig'); 38 | 39 | done(); 40 | } 41 | }); 42 | 43 | it('should throw a parse error having its file property set to the entry file', function (done) { 44 | try { 45 | const output = twig({ 46 | path: 'test/templates/error/parse/in-entry/entry.twig', 47 | async: false, 48 | rethrow: true 49 | }).render(); 50 | 51 | done(output); 52 | } catch (error) { 53 | error.should.have.property('file', 'test/templates/error/parse/in-entry/entry.twig'); 54 | 55 | done(); 56 | } 57 | }); 58 | 59 | it('should throw a parse error having its file property set to the partial file', function (done) { 60 | try { 61 | const output = twig({ 62 | path: 'test/templates/error/parse/in-partial/entry.twig', 63 | async: false, 64 | rethrow: true 65 | }).render(); 66 | 67 | done(output); 68 | } catch (error) { 69 | error.should.have.property('file', path.join('test/templates/error/parse/in-entry/entry.twig')); 70 | 71 | done(); 72 | } 73 | }); 74 | }); 75 | 76 | -------------------------------------------------------------------------------- /demos/twitter_backbone/less/styles.less: -------------------------------------------------------------------------------- 1 | @import "css3.less"; 2 | body { 3 | padding:0; 4 | margin:0; 5 | overflow: hidden; 6 | } 7 | .app { 8 | width: 500px; 9 | margin: 10px auto; 10 | 11 | .userInfo { 12 | top: 10px; 13 | height: 40px; 14 | line-height: 40px; 15 | 16 | position: absolute; 17 | text-align: center; 18 | color: #eee; 19 | .background-gradient(#777, #222); 20 | .border-radius(5px); 21 | .box-shadow(0, 2px, 3px, 2px, #eee); 22 | 23 | margin: auto; 24 | width: 500px; 25 | 26 | .reloadTweets { 27 | float: right; 28 | margin-right: 10px; 29 | margin-top: 3px; 30 | } 31 | .changeUser { 32 | float: left; 33 | margin-left: 10px; 34 | margin-top: 3px; 35 | } 36 | } 37 | 38 | .feedContainer, .errorContainer { 39 | position: absolute; 40 | top: 60px; 41 | bottom: 10px; 42 | overflow:auto; 43 | margin: auto; 44 | width: 500px; 45 | } 46 | 47 | .errorContainer { 48 | color: #ff0000; 49 | font-weight: bold; 50 | text-align:center; 51 | padding-top: 10px; 52 | } 53 | 54 | .feed { 55 | margin:0; 56 | padding:0; 57 | 58 | .tweet { 59 | padding:0; 60 | margin: 10px 0; 61 | list-style-type: none; 62 | 63 | a { 64 | color: #779; 65 | } 66 | 67 | .userPicture { 68 | float:left; 69 | margin: 0 10px; 70 | } 71 | 72 | .content { 73 | margin-left: 70px; 74 | padding-bottom: 5px; 75 | border-bottom: 1px solid #eee; 76 | margin-right: 10px; 77 | } 78 | 79 | .user { 80 | margin-bottom: 3px; 81 | 82 | .name a { 83 | color: #111; 84 | font-weight: bold; 85 | text-decoration: none; 86 | } 87 | .handle a { 88 | font-size: 90%; 89 | color: #555; 90 | text-decoration: none; 91 | } 92 | } 93 | 94 | .date { 95 | float: right; 96 | color: #555; 97 | font-size: 80%; 98 | margin-right: 5px; 99 | } 100 | 101 | &:after { 102 | clear:both; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/twig.loader.fs.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Twig) { 2 | 'use strict'; 3 | 4 | let fs; 5 | let path; 6 | 7 | try { 8 | // Require lib dependencies at runtime 9 | fs = require('fs'); 10 | path = require('path'); 11 | } catch (error) { 12 | // NOTE: this is in a try/catch to avoid errors cross platform 13 | console.warn('Missing fs and path modules. ' + error); 14 | } 15 | 16 | Twig.Templates.registerLoader('fs', function (location, params, callback, errorCallback) { 17 | let template; 18 | let data = null; 19 | const {precompiled} = params; 20 | const parser = this.parsers[params.parser] || this.parser.twig; 21 | 22 | if (!fs || !path) { 23 | throw new Twig.Error('Unsupported platform: Unable to load from file ' + 24 | 'because there is no "fs" or "path" implementation'); 25 | } 26 | 27 | const loadTemplateFn = function (err, data) { 28 | if (err) { 29 | if (typeof errorCallback === 'function') { 30 | errorCallback(err); 31 | } 32 | 33 | return; 34 | } 35 | 36 | if (precompiled === true) { 37 | data = JSON.parse(data); 38 | } 39 | 40 | params.data = data; 41 | params.path = params.path || location; 42 | 43 | // Template is in data 44 | template = parser.call(this, params); 45 | 46 | if (typeof callback === 'function') { 47 | callback(template); 48 | } 49 | }; 50 | 51 | params.path = params.path || location; 52 | 53 | if (params.async) { 54 | fs.stat(params.path, (err, stats) => { 55 | if (err || !stats.isFile()) { 56 | if (typeof errorCallback === 'function') { 57 | errorCallback(new Twig.Error('Unable to find template file ' + params.path)); 58 | } 59 | 60 | return; 61 | } 62 | 63 | fs.readFile(params.path, 'utf8', loadTemplateFn); 64 | }); 65 | // TODO: return deferred promise 66 | return true; 67 | } 68 | 69 | try { 70 | if (!fs.statSync(params.path).isFile()) { 71 | throw new Twig.Error('Unable to find template file ' + params.path); 72 | } 73 | } catch (error) { 74 | throw new Twig.Error('Unable to find template file ' + params.path + '. ' + error); 75 | } 76 | 77 | data = fs.readFileSync(params.path, 'utf8'); 78 | loadTemplateFn(undefined, data); 79 | return template; 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /demos/twitter_backbone/js/view/tweetView.js: -------------------------------------------------------------------------------- 1 | // # Tweet View 2 | // 3 | // The view for a single Tweet 4 | // 5 | 6 | module.declare( 7 | [ 8 | {backbone: 'vendor/backbone'}, 9 | {twig: 'vendor/twig'}, 10 | {tweet: 'js/model/tweet'} 11 | ] 12 | , (require, exports, module) => { 13 | const Backbone = require('backbone'); 14 | const {twig} = require('twig'); 15 | 16 | // Load the template for a "Tweet" 17 | // This template only needs to be loaded once. It will be compiled at 18 | // load time and can be rendered separately for each Tweet. 19 | const template = twig({ 20 | href: 'templates/tweet.twig', 21 | async: false 22 | }); 23 | 24 | const TweetView = Backbone.View.extend({ 25 | tagName: 'li', 26 | className: 'tweet', 27 | 28 | // Create the Tweet view 29 | initialize() { 30 | // Re-render the tweet if the backing model changes 31 | this.model.bind('change', this.render, this); 32 | 33 | // Remove the Tweet if the backing model is removed. 34 | this.model.bind('destroy', this.remove, this); 35 | }, 36 | 37 | // Render the tweet Twig template with the contents of the model 38 | render() { 39 | // Pass in an object representing the Tweet to serve as the 40 | // render context for the template and inject it into the View. 41 | $(this.el).html(template.render( 42 | this.enhanceModel(this.model.toJSON()) 43 | )); 44 | return this; 45 | }, 46 | 47 | // Regex's for matching twitter usernames and web links 48 | userRegEx: /\@([a-zA-Z0-9_\-\.]+)/g, 49 | hashRegex: /#([a-zA-Z0-9_\-\.]+)/g, 50 | linkRegEx: /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/g, 51 | 52 | // Enhance the model passed to the template with links 53 | enhanceModel(model) { 54 | model.text = model.text.replace(this.linkRegEx, '$1'); 55 | model.text = model.text.replace(this.hashRegex, '#$1'); 56 | model.text = model.text.replace(this.userRegEx, ''); 57 | return model; 58 | }, 59 | 60 | // Remove the tweet view from it's container (a FeedView) 61 | remove() { 62 | $(this.el).remove(); 63 | } 64 | }); 65 | 66 | exports.TweetView = TweetView; 67 | } 68 | ); 69 | 70 | -------------------------------------------------------------------------------- /test/test.loaders.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Loaders ->', function () { 6 | // Encodings 7 | describe('custom loader ->', function () { 8 | it('should define a custom loader', function () { 9 | Twig.extend(Twig => { 10 | const obj = { 11 | templates: { 12 | customLoaderBlock: '{% block main %}This lets you {% block data %}use blocks{% endblock data %}{% endblock main %}', 13 | customLoaderSimple: 'the value is: {{ value }}', 14 | customLoaderInclude: 'include others from the same loader method - {% include "customLoaderSimple" %}', 15 | customLoaderComplex: '{% extends "customLoaderBlock" %} {% block data %}extend other templates and {% include "customLoaderInclude" %}{% endblock data %}' 16 | }, 17 | loader(location, params, callback, _) { 18 | params.data = this.templates[location]; 19 | params.allowInlineIncludes = true; 20 | const template = new Twig.Template(params); 21 | if (typeof callback === 'function') { 22 | callback(template); 23 | } 24 | 25 | return template; 26 | } 27 | }; 28 | Twig.Templates.registerLoader('custom', obj.loader, obj); 29 | Twig.Templates.loaders.should.have.property('custom'); 30 | }); 31 | }); 32 | it('should load a simple template from a custom loader', function () { 33 | twig({ 34 | method: 'custom', 35 | name: 'customLoaderSimple' 36 | }).render({value: 'test succeeded'}).should.equal('the value is: test succeeded'); 37 | }); 38 | it('should load a template that includes another from a custom loader', function () { 39 | twig({ 40 | method: 'custom', 41 | name: 'customLoaderInclude' 42 | }).render({value: 'test succeeded'}).should.equal('include others from the same loader method - the value is: test succeeded'); 43 | }); 44 | it('should load a template that extends another from a custom loader', function () { 45 | twig({ 46 | method: 'custom', 47 | name: 'customLoaderComplex' 48 | }).render({value: 'test succeeded'}).should.equal('This lets you extend other templates and include others from the same loader method - the value is: test succeeded'); 49 | }); 50 | it('should remove a registered loader', function () { 51 | Twig.extend(Twig => { 52 | Twig.Templates.unRegisterLoader('custom'); 53 | Twig.Templates.loaders.should.not.have.property('custom'); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /bin/twigjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var PATHS = require("../lib/paths") 4 | , COMPILE = require("../lib/compile") 5 | , options = COMPILE.defaults 6 | 7 | , args = process.argv 8 | , node = args.shift() 9 | , thisPath = args.shift().split("/") 10 | , thisFile = thisPath[thisPath.length-1] 11 | , files = [] 12 | , arg; 13 | 14 | if (args.length == 0) { 15 | process.stderr.write("ERR: No input files provided\n\n"); 16 | printUsage(process.stderr); 17 | } 18 | 19 | while (args.length > 0) { 20 | arg = args.shift(); 21 | switch (arg) { 22 | case "--help": 23 | printUsage(process.stdout); 24 | return; 25 | case "--output": 26 | case "-o": 27 | options.output = PATHS.strip_slash(args.shift()); 28 | break; 29 | case "--pattern": 30 | case "-p": 31 | options.pattern = args.shift(); 32 | break; 33 | case "--module": 34 | case "-m": 35 | options.module = args.shift(); 36 | break; 37 | case "--twig": 38 | case "-t": 39 | options.twig = args.shift(); 40 | break; 41 | default: 42 | files.push(arg); 43 | } 44 | } 45 | 46 | COMPILE.compile(options, files); 47 | 48 | function printUsage(stream) { 49 | stream.write("Usage:\n\t"); 50 | stream.write(thisFile + " [options] input.twig | directory ...\n"); 51 | stream.write("\t_______________________________________________________________________________\n\n"); 52 | stream.write("\t" + thisFile + " can take a list of files and/or a directories as input. If a file is\n"); 53 | stream.write("\tprovided, it is compiled, if a directory is provided, all files matching *.twig\n"); 54 | stream.write("\tin the directory are compiled. The pattern can be overridden with --pattern\n\n") 55 | stream.write("\t--help Print this help message.\n\n"); 56 | stream.write("\t--output ... What directory should twigjs output to. By default twigjs will\n"); 57 | stream.write("\t write to the same directory as the input file.\n\n"); 58 | stream.write("\t--module ... Should the output be written in module format. Supported formats:\n"); 59 | stream.write("\t node: Node.js / CommonJS 1.1 modules\n"); 60 | stream.write("\t amd: RequireJS / Asynchronous modules (requires --twig)\n"); 61 | stream.write("\t cjs2: CommonJS 2.0 draft8 modules (requires --twig)\n\n"); 62 | stream.write("\t--twig ... Used with --module. The location relative to the output directory\n"); 63 | stream.write("\t of twig.js. (used for module dependency resolution).\n\n"); 64 | stream.write("\t--pattern ... If parsing a directory of files, what files should be compiled.\n"); 65 | stream.write("\t Defaults to *.twig.\n\n"); 66 | stream.write("NOTE: This is currently very rough, incomplete and under development.\n\n"); 67 | } 68 | -------------------------------------------------------------------------------- /test/test.options.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Optional Functionality ->', function () { 6 | it('should support inline includes by ID', function () { 7 | twig({ 8 | id: 'other', 9 | data: 'another template' 10 | }); 11 | 12 | const template = twig({ 13 | allowInlineIncludes: true, 14 | data: 'template with {% include "other" %}' 15 | }); 16 | const output = template.render(); 17 | 18 | output.should.equal('template with another template'); 19 | }); 20 | 21 | describe('should throw an error when `strict_variables` set to `true`', function () { 22 | const variable = twig({ 23 | rethrow: true, 24 | strict_variables: true, 25 | data: '{{ test }}' 26 | }); 27 | 28 | const object = twig({ 29 | rethrow: true, 30 | strict_variables: true, 31 | data: '{{ test.10 }}' 32 | }); 33 | 34 | const array = twig({ 35 | rethrow: true, 36 | strict_variables: true, 37 | data: '{{ test[10] }}' 38 | }); 39 | 40 | it('For undefined variables', function () { 41 | try { 42 | variable.render(); 43 | throw new Error('should have thrown an error.'); 44 | } catch (error) { 45 | error.message.should.equal('Variable "test" does not exist.'); 46 | } 47 | }); 48 | 49 | it('For empty objects', function () { 50 | try { 51 | object.render({test: {}}); 52 | throw new Error('should have thrown an error.'); 53 | } catch (error) { 54 | error.message.should.equal('Key "10" does not exist as the object is empty.'); 55 | } 56 | }); 57 | 58 | it('For undefined object keys', function () { 59 | try { 60 | object.render({test: {1: 'value', 2: 'value', 3: 'value'}}); 61 | throw new Error('should have thrown an error.'); 62 | } catch (error) { 63 | error.message.should.equal('Key "10" for object with keys "1, 2, 3" does not exist.'); 64 | } 65 | }); 66 | 67 | it('For empty arrays', function () { 68 | try { 69 | array.render({test: []}); 70 | throw new Error('should have thrown an error.'); 71 | } catch (error) { 72 | error.message.should.equal('Key "10" does not exist as the array is empty.'); 73 | } 74 | }); 75 | 76 | it('For undefined array keys', function () { 77 | try { 78 | array.render({test: [1, 2, 3]}); 79 | throw new Error('should have thrown an error.'); 80 | } catch (error) { 81 | error.message.should.equal('Key "10" for array with keys "0, 1, 2" does not exist.'); 82 | } 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/browser/test.macro.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Macro ->', function () { 6 | // Test loading a template from a remote endpoint 7 | it('it should load macro', function () { 8 | twig({ 9 | id: 'macro', 10 | href: 'templates/macro.twig', 11 | async: false 12 | }); 13 | // Load the template 14 | twig({ref: 'macro'}).render({ }).should.equal(''); 15 | }); 16 | 17 | it('it should import macro', function () { 18 | twig({ 19 | id: 'import-macro', 20 | href: 'templates/import.twig', 21 | async: false 22 | }); 23 | // Load the template 24 | twig({ref: 'import-macro'}).render({ }).trim().should.equal('Hello World'); 25 | }); 26 | 27 | it('it should run macro with self reference', function () { 28 | twig({ 29 | id: 'import-macro-self', 30 | href: 'templates/macro-self.twig', 31 | async: false 32 | }); 33 | // Load the template 34 | twig({ref: 'import-macro-self'}).render({ }).trim().should.equal('

'); 35 | }); 36 | 37 | it('it should run wrapped macro with self reference', function () { 38 | twig({ 39 | id: 'import-wrapped-macro-self', 40 | href: 'templates/macro-wrapped.twig', 41 | async: false 42 | }); 43 | // Load the template 44 | twig({ref: 'import-wrapped-macro-self'}).render({ }).trim().should.equal('

'); 45 | }); 46 | 47 | it('it should run wrapped macro with context and self reference', function () { 48 | twig({ 49 | id: 'import-macro-context-self', 50 | href: 'templates/macro-context.twig', 51 | async: false 52 | }); 53 | // Load the template 54 | twig({ref: 'import-macro-context-self'}).render({greetings: 'Howdy'}).trim().should.equal('Howdy Twigjs'); 55 | }); 56 | 57 | it('it should run wrapped macro inside blocks', function () { 58 | twig({ 59 | id: 'import-macro-inside-block', 60 | href: 'templates/macro-blocks.twig', 61 | async: false 62 | }); 63 | // Load the template 64 | twig({ref: 'import-macro-inside-block'}).render({ }).trim().should.equal('Welcome
Twig Js
'); 65 | }); 66 | 67 | it('it should import selected macros from template', function () { 68 | twig({ 69 | id: 'from-macro-import', 70 | href: 'templates/from.twig', 71 | async: false 72 | }); 73 | // Load the template 74 | twig({ref: 'from-macro-import'}).render({ }).trim().should.equal('Twig.js
'); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /demos/node_express/public/vendor/signals.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | JS Signals 4 | Released under the MIT license 5 | Author: Miller Medeiros 6 | Version: 0.7.2 - Build: 248 (2012/01/12 10:39 PM) 7 | */ 8 | (function(g){function f(a,b,c,h,d){this._listener=b;this._isOnce=c;this.context=h;this._signal=a;this._priority=d||0}function e(a,b){if(typeof a!=="function")throw Error("listener is a required param of {fn}() and should be a Function.".replace("{fn}",b));}var d={VERSION:"0.7.2"};f.prototype={active:!0,params:null,execute:function(a){var b;this.active&&this._listener&&(a=this.params?this.params.concat(a):a,b=this._listener.apply(this.context,a),this._isOnce&&this.detach());return b},detach:function(){return this.isBound()? 9 | this._signal.remove(this._listener):null},isBound:function(){return!!this._signal&&!!this._listener},getListener:function(){return this._listener},_destroy:function(){delete this._signal;delete this._listener;delete this.context},isOnce:function(){return this._isOnce},toString:function(){return"[SignalBinding isOnce:"+this._isOnce+", isBound:"+this.isBound()+", active:"+this.active+"]"}};d.Signal=function(){this._bindings=[];this._prevParams=null};d.Signal.prototype={memorize:!1,_shouldPropagate:!0, 10 | active:!0,_registerListener:function(a,b,c,d){var e=this._indexOfListener(a);if(e!==-1&&this._bindings[e].context===c){if(a=this._bindings[e],a.isOnce()!==b)throw Error("You cannot add"+(b?"":"Once")+"() then add"+(!b?"":"Once")+"() the same listener without removing the relationship first.");}else a=new f(this,a,b,c,d),this._addBinding(a);this.memorize&&this._prevParams&&a.execute(this._prevParams);return a},_addBinding:function(a){var b=this._bindings.length;do--b;while(this._bindings[b]&&a._priority<= 11 | this._bindings[b]._priority);this._bindings.splice(b+1,0,a)},_indexOfListener:function(a){for(var b=this._bindings.length;b--;)if(this._bindings[b]._listener===a)return b;return-1},has:function(a){return this._indexOfListener(a)!==-1},add:function(a,b,c){e(a,"add");return this._registerListener(a,!1,b,c)},addOnce:function(a,b,c){e(a,"addOnce");return this._registerListener(a,!0,b,c)},remove:function(a){e(a,"remove");var b=this._indexOfListener(a);b!==-1&&(this._bindings[b]._destroy(),this._bindings.splice(b, 12 | 1));return a},removeAll:function(){for(var a=this._bindings.length;a--;)this._bindings[a]._destroy();this._bindings.length=0},getNumListeners:function(){return this._bindings.length},halt:function(){this._shouldPropagate=!1},dispatch:function(a){if(this.active){var b=Array.prototype.slice.call(arguments),c=this._bindings.length,d;if(this.memorize)this._prevParams=b;if(c){d=this._bindings.slice();this._shouldPropagate=!0;do c--;while(d[c]&&this._shouldPropagate&&d[c].execute(b)!==!1)}}},forget:function(){this._prevParams= 13 | null},dispose:function(){this.removeAll();delete this._bindings;delete this._prevParams},toString:function(){return"[Signal active:"+this.active+" numListeners:"+this.getNumListeners()+"]"}};typeof define==="function"&&define.amd?define("signals",[],d):typeof module!=="undefined"&&module.exports?module.exports=d:g.signals=d})(this); -------------------------------------------------------------------------------- /demos/twitter_backbone/js/view/appView.js: -------------------------------------------------------------------------------- 1 | // # Application View 2 | // 3 | // This module contains the view component for the main app. 4 | // It also serves double duty as the application controller. 5 | // 6 | // The template is loaded from templates/app.twig 7 | // 8 | 9 | module.declare( 10 | [ 11 | {backbone: 'vendor/backbone'}, 12 | {twig: 'vendor/twig'}, 13 | {feed: 'js/model/feed'}, 14 | {feedView: 'js/view/feedView'} 15 | ] 16 | , (require, exports, module) => { 17 | const {twig} = require('twig'); 18 | const Backbone = require('backbone'); 19 | const {feed} = require('feed') 20 | 21 | // The application template 22 | ; const template = twig({ 23 | href: 'templates/app.twig', 24 | async: false 25 | }); 26 | const {FeedView} = require('feedView'); 27 | const feedView = new FeedView(); 28 | const AppView = Backbone.View.extend({ 29 | tagName: 'div', 30 | className: 'app', 31 | 32 | // Bind to the buttons in the template 33 | events: { 34 | 'click .reloadTweets': 'reload', 35 | 'click .changeUser': 'changeUser', 36 | 'click .twitter_user': 'twitterLink' 37 | }, 38 | 39 | // Initialize the Application 40 | initialize() { 41 | this.model.bind('change', this.changeSettings, this); 42 | this.feedView = feedView; 43 | this.changeSettings(); 44 | }, 45 | 46 | // Render the template with the contents of the Setting model 47 | render() { 48 | $(this.el).html(template.render(this.model.toJSON())); 49 | 50 | this.$('.feedContainer').html(this.feedView.el); 51 | }, 52 | 53 | // Trigger the feed Collection to refresh the twitter feed 54 | reload() { 55 | const username = this.model.get('username'); 56 | feed.loadUser(username); 57 | }, 58 | 59 | // Update the Setting model associated with this AppView 60 | // The change event will trigger a redraw 61 | changeUser() { 62 | const username = prompt('Please enter a twitter username:'); 63 | this.model.set({ 64 | username 65 | }); 66 | this.model.save(); 67 | }, 68 | 69 | twitterLink(e) { 70 | const username = $(e.target).attr('user'); 71 | if (username) { 72 | this.model.set({ 73 | username 74 | }); 75 | this.model.save(); 76 | } 77 | 78 | e.preventDefault(); 79 | e.stopPropagation(); 80 | }, 81 | 82 | // Handle change events from the Setting model 83 | // Renders the view and triggers a reload of the feed 84 | changeSettings() { 85 | this.render(); 86 | this.reload(); 87 | } 88 | }); 89 | 90 | exports.AppView = AppView; 91 | } 92 | ); 93 | -------------------------------------------------------------------------------- /test/test.path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const Twig = require('../twig').factory(); 4 | 5 | describe('Twig.js Path ->', function () { 6 | const sinon = require('sinon'); 7 | /* eslint-disable-next-line import/no-unassigned-import */ 8 | require('should-sinon'); 9 | 10 | describe('relativePath ->', function () { 11 | let relativePath; 12 | 13 | before(function () { 14 | relativePath = Twig.path.relativePath; 15 | }); 16 | 17 | it('should throw an error if trying to get a relative path in an inline template', function () { 18 | /* eslint-disable-next-line no-use-extend-native/no-use-extend-native */ 19 | (function () { 20 | relativePath({}); 21 | }).should.throw('Cannot extend an inline template.'); 22 | }); 23 | 24 | it('should give the full path to a file when file is passed', function () { 25 | relativePath({url: 'http://www.test.com/test.twig'}, 'templates/myFile.twig').should.equal('http://www.test.com/templates/myFile.twig'); 26 | relativePath({path: 'test/test.twig'}, 'templates/myFile.twig').should.equal(path.join('test/templates/myFile.twig')); 27 | }); 28 | 29 | it('should ascend directories', function () { 30 | relativePath({url: 'http://www.test.com/templates/../test.twig'}, 'myFile.twig').should.equal('http://www.test.com/myFile.twig'); 31 | relativePath({path: 'test/templates/../test.twig'}, 'myFile.twig').should.equal(path.join('test/myFile.twig')); 32 | }); 33 | 34 | it('should respect relative directories', function () { 35 | relativePath({url: 'http://www.test.com/templates/./test.twig'}, 'myFile.twig').should.equal('http://www.test.com/templates/myFile.twig'); 36 | relativePath({path: 'test/templates/./test.twig'}, 'myFile.twig').should.equal(path.join('test/templates/myFile.twig')); 37 | }); 38 | 39 | describe('url ->', function () { 40 | it('should use the url if no base is specified', function () { 41 | relativePath({url: 'http://www.test.com/test.twig'}).should.equal('http://www.test.com/'); 42 | }); 43 | 44 | it('should use the base if base is specified', function () { 45 | relativePath({url: 'http://www.test.com/test.twig', base: 'myTest'}).should.equal('myTest/'); 46 | }); 47 | }); 48 | 49 | describe('path ->', function () { 50 | it('should use the path if no base is specified', function () { 51 | relativePath({path: 'test/test.twig'}).should.equal(path.join('test/')); 52 | }); 53 | 54 | it('should use the base if base is specified', function () { 55 | relativePath({path: 'test/test.twig', base: 'myTest'}).should.equal(path.join('myTest/')); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('parsePath ->', function () { 61 | let parsePath; 62 | 63 | before(function () { 64 | parsePath = Twig.path.parsePath; 65 | }); 66 | 67 | it('should fall back to relativePath if the template has no namespaces defined', function () { 68 | const relativePathStub = sinon.stub(Twig.path, 'relativePath'); 69 | 70 | parsePath({options: {}}); 71 | 72 | relativePathStub.should.be.called(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/browser/test.namespace.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Namespaces ->', function () { 6 | it('should support namespaces defined with ::', function (done) { 7 | twig({ 8 | namespaces: {test: 'templates/namespaces/'}, 9 | path: 'templates/namespaces_coloncolon.twig', 10 | load(template) { 11 | // Render the template 12 | template.render({ 13 | test: 'yes', 14 | flag: true 15 | }).should.equal('namespaces'); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | 22 | it('should support namespaces defined with :: and without slash at the end of the path', function (done) { 23 | twig({ 24 | namespaces: {test: 'templates/namespaces'}, 25 | path: 'templates/namespaces_coloncolon.twig', 26 | load(template) { 27 | // Render the template 28 | template.render({ 29 | test: 'yes', 30 | flag: true 31 | }).should.equal('namespaces'); 32 | 33 | done(); 34 | } 35 | }); 36 | }); 37 | 38 | it('should support namespaces defined with @', function (done) { 39 | twig({ 40 | namespaces: {test: 'templates/namespaces/'}, 41 | path: 'templates/namespaces_@.twig', 42 | load(template) { 43 | // Render the template 44 | template.render({ 45 | test: 'yes', 46 | flag: true 47 | }).should.equal('namespaces'); 48 | 49 | done(); 50 | } 51 | }); 52 | }); 53 | 54 | it('should support namespaces defined with @ and without slash at the end of the path', function (done) { 55 | twig({ 56 | namespaces: {test: 'templates/namespaces'}, 57 | path: 'templates/namespaces_@.twig', 58 | load(template) { 59 | // Render the template 60 | template.render({ 61 | test: 'yes', 62 | flag: true 63 | }).should.equal('namespaces'); 64 | 65 | done(); 66 | } 67 | }); 68 | }); 69 | 70 | it('should support non-namespaced includes with namespaces configured', function (done) { 71 | twig({ 72 | namespaces: {test: 'templates/namespaces/'}, 73 | path: 'templates/namespaces_without_namespace.twig', 74 | load(template) { 75 | // Render the template 76 | template.render({ 77 | test: 'yes', 78 | flag: true 79 | }).should.equal('namespaces\nnamespaces'); 80 | 81 | done(); 82 | } 83 | }); 84 | }); 85 | 86 | it('should support multiple namespaces', function (done) { 87 | twig({ 88 | namespaces: { 89 | one: 'templates/namespaces/one/', 90 | two: 'templates/namespaces/two/' 91 | }, 92 | path: 'templates/namespaces_multiple.twig', 93 | load(template) { 94 | // Render the template 95 | template.render({ 96 | test: 'yes', 97 | flag: true 98 | }).should.equal('namespace one\nnamespace two'); 99 | 100 | done(); 101 | } 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /lib/compile.js: -------------------------------------------------------------------------------- 1 | const FS = require('fs'); 2 | const minimatch = require('minimatch'); 3 | const WALK = require('walk'); 4 | const Twig = require('../twig'); 5 | const PATHS = require('./paths'); 6 | 7 | const {twig} = Twig; 8 | 9 | exports.defaults = { 10 | compress: false, 11 | pattern: '*\\.twig', 12 | recursive: false 13 | }; 14 | 15 | exports.compile = function (options, files) { 16 | // Create output template directory if necessary 17 | if (options.output) { 18 | PATHS.mkdir(options.output); 19 | } 20 | 21 | files.forEach(file => { 22 | FS.stat(file, (err, stats) => { 23 | if (err) { 24 | console.error('ERROR ' + file + ': Unable to stat file'); 25 | return; 26 | } 27 | 28 | if (stats.isDirectory()) { 29 | parseTemplateFolder(file, options.pattern); 30 | } else if (stats.isFile()) { 31 | parseTemplateFile(file); 32 | } else { 33 | console.log('ERROR ' + file + ': Unknown file information'); 34 | } 35 | }); 36 | }); 37 | 38 | function parseTemplateFolder(directory, pattern) { 39 | directory = PATHS.stripSlash(directory); 40 | 41 | // Get the files in the directory 42 | // Walker options 43 | const walker = WALK.walk(directory, {followLinks: false}); 44 | const files = []; 45 | 46 | walker.on('file', (root, stat, next) => { 47 | // Normalize (remove / from end if present) 48 | root = PATHS.stripSlash(root); 49 | 50 | // Match against file pattern 51 | const {name} = stat; 52 | const file = root + '/' + name; 53 | if (minimatch(name, pattern)) { 54 | parseTemplateFile(file, directory); 55 | files.push(file); 56 | } 57 | 58 | next(); 59 | }); 60 | 61 | walker.on('end', () => { 62 | // Console.log(files); 63 | }); 64 | } 65 | 66 | function parseTemplateFile(file, base) { 67 | if (base) { 68 | base = PATHS.stripSlash(base); 69 | } 70 | 71 | const splitFile = file.split('/'); 72 | const outputFileName = splitFile.pop(); 73 | const outputFileBase = PATHS.findBase(file); 74 | const outputDirectory = options.output; 75 | let outputBase = PATHS.removePath(base, outputFileBase); 76 | let outputId; 77 | let outputFile; 78 | 79 | if (outputDirectory) { 80 | // Create template directory 81 | if (outputBase !== '') { 82 | PATHS.mkdir(outputDirectory + '/' + outputBase); 83 | outputBase += '/'; 84 | } 85 | 86 | outputId = outputDirectory + '/' + outputBase + outputFileName; 87 | outputFile = outputId + '.js'; 88 | } else { 89 | outputId = file; 90 | outputFile = outputId + '.js'; 91 | } 92 | 93 | twig({ 94 | id: outputId, 95 | path: file, 96 | load(template) { 97 | // Compile! 98 | const output = template.compile(options); 99 | 100 | FS.writeFile(outputFile, output, 'utf8', err => { 101 | if (err) { 102 | console.log('Unable to compile ' + file + ', error ' + err); 103 | } else { 104 | console.log('Compiled ' + file + '\t-> ' + outputFile); 105 | } 106 | }); 107 | } 108 | }); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /test/test.namespaces.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Namespaces ->', function () { 6 | it('should support namespaces defined with ::', function (done) { 7 | twig({ 8 | namespaces: {test: 'test/templates/namespaces/'}, 9 | path: 'test/templates/namespaces_coloncolon.twig', 10 | load(template) { 11 | // Render the template 12 | template.render({ 13 | test: 'yes', 14 | flag: true 15 | }).should.equal('namespaces'); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | 22 | it('should support namespaces defined with :: and without slash at the end of path', function (done) { 23 | twig({ 24 | namespaces: {test: 'test/templates/namespaces'}, 25 | path: 'test/templates/namespaces_coloncolon.twig', 26 | load(template) { 27 | // Render the template 28 | template.render({ 29 | test: 'yes', 30 | flag: true 31 | }).should.equal('namespaces'); 32 | 33 | done(); 34 | } 35 | }); 36 | }); 37 | 38 | it('should support namespaces defined with @', function (done) { 39 | twig({ 40 | namespaces: {test: 'test/templates/namespaces/'}, 41 | path: 'test/templates/namespaces_@.twig', 42 | load(template) { 43 | // Render the template 44 | template.render({ 45 | test: 'yes', 46 | flag: true 47 | }).should.equal('namespaces'); 48 | 49 | done(); 50 | } 51 | }); 52 | }); 53 | 54 | it('should support namespaces defined with @ and without slash at the end of path', function (done) { 55 | twig({ 56 | namespaces: {test: 'test/templates/namespaces'}, 57 | path: 'test/templates/namespaces_@.twig', 58 | load(template) { 59 | // Render the template 60 | template.render({ 61 | test: 'yes', 62 | flag: true 63 | }).should.equal('namespaces'); 64 | 65 | done(); 66 | } 67 | }); 68 | }); 69 | 70 | it('should support non-namespaced includes with namespaces configured', function (done) { 71 | twig({ 72 | namespaces: {test: 'test/templates/namespaces/'}, 73 | path: 'test/templates/namespaces_without_namespace.twig', 74 | load(template) { 75 | // Render the template 76 | template.render({ 77 | test: 'yes', 78 | flag: true 79 | }).should.equal('namespaces\nnamespaces'); 80 | 81 | done(); 82 | } 83 | }); 84 | }); 85 | 86 | it('should support multiple namespaces', function (done) { 87 | twig({ 88 | namespaces: { 89 | one: 'test/templates/namespaces/one/', 90 | two: 'test/templates/namespaces/two/' 91 | }, 92 | path: 'test/templates/namespaces_multiple.twig', 93 | load(template) { 94 | // Render the template 95 | template.render({ 96 | test: 'yes', 97 | flag: true 98 | }).should.equal('namespace one\nnamespace two'); 99 | 100 | done(); 101 | } 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /demos/node_express/app.js: -------------------------------------------------------------------------------- 1 | const twig = require('twig'); 2 | const {_} = require('underscore'); 3 | const markdown = require('markdown'); 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const app = express(); 7 | 8 | // Generate some 9 | function error_json(id, message) { 10 | return { 11 | error: true, 12 | id, 13 | message, 14 | json: true 15 | }; 16 | } 17 | 18 | function update_note(body) { 19 | const {title} = body; 20 | const {text} = body; 21 | let {id} = body; 22 | 23 | if (title) { 24 | if (id == '') { 25 | // Get new ID and increment ID counter 26 | id = id_ctr; 27 | id_ctr++; 28 | } 29 | 30 | notes[id] = { 31 | title, 32 | text, 33 | id 34 | }; 35 | 36 | console.log('Adding/Updating note'); 37 | console.log(notes[id]); 38 | } 39 | } 40 | 41 | // Some test data to pre-populate the notebook with 42 | var id_ctr = 4; 43 | var notes = { 44 | 1: { 45 | title: 'Note', 46 | text: 'These could be your **notes**.\n\nBut you would have to turn this demo program into something beautiful.', 47 | id: 1 48 | }, 49 | 2: { 50 | title: 'Templates', 51 | text: 'Templates are a way of enhancing content with markup. Or really anything that requires the merging of data and display.', 52 | id: 2 53 | }, 54 | 3: { 55 | title: 'Tasks', 56 | text: '* Wake Up\n* Drive to Work\n* Work\n* Drive Home\n* Sleep', 57 | id: 3 58 | } 59 | }; 60 | 61 | app.use(express.static(__dirname + '/public')); 62 | app.use(bodyParser()); 63 | app.set('views', __dirname + '/public/views'); 64 | app.set('view engine', 'twig'); 65 | // We don't need express to use a parent "page" layout 66 | // Twig.js has support for this using the {% extends parent %} tag 67 | app.set('view options', {layout: false}); 68 | 69 | // Routing for the notebook 70 | 71 | app.get('/', (req, res) => { 72 | res.render('pages/index', { 73 | message: 'Hello World' 74 | }); 75 | }); 76 | 77 | app.get('/add', (req, res) => { 78 | res.render('pages/note_form', {}); 79 | }); 80 | 81 | app.get('/edit/:id', (req, res) => { 82 | const id = parseInt(req.params.id); 83 | const note = notes[id]; 84 | 85 | res.render('pages/note_form', note); 86 | }); 87 | 88 | app.all('/notes', (req, res) => { 89 | update_note(req.body); 90 | 91 | res.render('pages/notes', { 92 | notes 93 | }); 94 | }); 95 | 96 | app.all('/notes/:id', (req, res) => { 97 | update_note(req.body); 98 | 99 | const id = parseInt(req.params.id); 100 | const note = notes[id]; 101 | 102 | if (note) { 103 | note.markdown = markdown.markdown.toHTML(note.text); 104 | res.render('pages/note', note); 105 | } else { 106 | res.render('pages/note_404'); 107 | } 108 | }); 109 | 110 | // RESTFUL endpoint for notes 111 | 112 | app.get('/api/notes', (req, res) => { 113 | res.json({ 114 | notes, 115 | json: true 116 | }); 117 | }); 118 | 119 | app.get('/api/notes/:id', (req, res) => { 120 | const id = parseInt(req.params.id); 121 | const note = notes[id]; 122 | 123 | if (note) { 124 | note.markdown = markdown.markdown.toHTML(note.text); 125 | res.json(_.extend({ 126 | json: true 127 | }, note)); 128 | } else { 129 | res.json(error_json(41, 'Unable to find note with id ' + id)); 130 | } 131 | }); 132 | 133 | const port = process.env.PORT || 9999; 134 | const host = process.env.IP || '0.0.0.0'; 135 | 136 | app.listen(port, host); 137 | 138 | console.log('Express Twig.js Demo is running on ' + host + ':' + port); 139 | 140 | -------------------------------------------------------------------------------- /demos/node_express/public/js/app.js: -------------------------------------------------------------------------------- 1 | // Notebook client code 2 | Twig.cache = true; 3 | 4 | (function (window, undefined) { 5 | const base = '/views/'; 6 | api_base = '/api'; 7 | 8 | crossroads.addRoute('/', () => { 9 | // Load notes page 10 | const template = twig({ref: 'index'}); 11 | const output = template.render({json: true}); 12 | 13 | $('#noteApp').html(output); 14 | }); 15 | 16 | crossroads.addRoute('/notes', () => { 17 | // Load notes page 18 | const template = twig({ref: 'notes'}); 19 | const url = api_base + '/notes'; 20 | 21 | $.getJSON(url, data => { 22 | const output = template.render(data); 23 | $('#noteApp').html(output); 24 | }); 25 | }); 26 | 27 | crossroads.addRoute('/notes/{id}', id => { 28 | // Load notes page 29 | const template = twig({ref: 'note'}); 30 | const error_template = twig({ref: '404'}); 31 | const url = api_base + '/notes/' + id; 32 | 33 | $.getJSON(url, data => { 34 | let output; 35 | if (data.error) { 36 | output = error_template.render(data); 37 | } else { 38 | output = template.render(data); 39 | } 40 | 41 | $('#noteApp').html(output); 42 | }); 43 | }); 44 | 45 | crossroads.addRoute('/add', () => { 46 | // Load notes page 47 | const template = twig({ref: 'form'}); 48 | const output = template.render({json: true}); 49 | 50 | $('#noteApp').html(output); 51 | }); 52 | 53 | crossroads.addRoute('/edit/{id}', id => { 54 | // Load notes page 55 | const template = twig({ref: 'form'}); 56 | const error_template = twig({ref: '404'}); 57 | const url = api_base + '/notes/' + id; 58 | 59 | $.getJSON(url, data => { 60 | let output; 61 | if (data.error) { 62 | output = error_template.render(data); 63 | } else { 64 | output = template.render(data); 65 | } 66 | 67 | $('#noteApp').html(output); 68 | }); 69 | }); 70 | 71 | // Preload templates 72 | (function () { 73 | let loaded = 0; 74 | const count = 5; 75 | const inc_loaded = function () { 76 | loaded++; 77 | if (loaded == count) { 78 | // Flag as loaded, signal any waiting events 79 | } 80 | }; 81 | 82 | const pages = { 83 | note: 'pages/note.twig', 84 | notes: 'pages/notes.twig', 85 | index: 'pages/index.twig', 86 | form: 'pages/note_form.twig', 87 | 404: 'pages/note_404.twig' 88 | }; 89 | 90 | for (id in pages) { 91 | if (pages.hasOwnProperty(id)) { 92 | twig({ 93 | id, 94 | href: base + pages[id], 95 | load() { 96 | inc_loaded(); 97 | } 98 | }); 99 | } 100 | } 101 | })(); 102 | 103 | const {History} = window; 104 | // Don't bind AJAX events without history support 105 | if (!History.enabled) { 106 | return false; 107 | } 108 | 109 | $(() => { 110 | // Bind to StateChange Event 111 | History.Adapter.bind(window, 'statechange', () => { // Note: We are using statechange instead of popstate 112 | const State = History.getState(); 113 | const {hash} = State; 114 | 115 | console.log(hash); 116 | // Trigger router 117 | crossroads.parse(hash); 118 | }); 119 | 120 | // Bind to links 121 | $('a.ajax_link').live('click', function (event) { 122 | event.preventDefault(); 123 | const href = $(this).attr('href'); 124 | History.pushState(null, null, href); 125 | }); 126 | }); 127 | })(window); 128 | -------------------------------------------------------------------------------- /demos/node_express/public/vendor/crossroads.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Crossroads.js 4 | Released under the MIT license 5 | Author: Miller Medeiros 6 | Version: 0.7.1 - Build: 88 (2012/01/06 05:17 PM) 7 | */ 8 | (function(i){i("crossroads",function(f){function j(a,b){if(a.indexOf)return a.indexOf(b);else{for(var c=a.length;c--;)if(a[c]===b)return c;return-1}}function h(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function i(a){return a===null||a==="null"?null:a==="true"?!0:a==="false"?!1:a===m||a==="undefined"?m:a===""||isNaN(a)?a:parseFloat(a)}function k(){this._routes=[];this.bypassed=new l.Signal;this.routed=new l.Signal}function n(a,b,c,e){var d=h(a,"RegExp");this._router=e;this._pattern= 9 | a;this._paramsIds=d?null:g.getParamIds(this._pattern);this._optionalParamsIds=d?null:g.getOptionalParamsIds(this._pattern);this._matchRegexp=d?a:g.compilePattern(a);this.matched=new l.Signal;b&&this.matched.add(b);this._priority=c||0}var l=f("signals"),g,m;k.prototype={normalizeFn:null,create:function(){return new k},shouldTypecast:!1,addRoute:function(a,b,c){a=new n(a,b,c,this);this._sortedInsert(a);return a},removeRoute:function(a){var b=j(this._routes,a);b!==-1&&this._routes.splice(b,1);a._destroy()}, 10 | removeAllRoutes:function(){for(var a=this.getNumRoutes();a--;)this._routes[a]._destroy();this._routes.length=0},parse:function(a){var a=a||"",b=this._getMatchedRoutes(a),c=0,e=b.length,d;if(e)for(;c', function () { 6 | it('Should load content in blocks that are not replaced', function () { 7 | twig({ 8 | id: 'remote-no-extends', 9 | href: 'templates/template.twig', 10 | async: false 11 | }); 12 | 13 | // Load the template 14 | twig({ref: 'remote-no-extends'}).render({ }).should.equal('Default Title - body'); 15 | }); 16 | 17 | it('Should replace block content from a child template', function (done) { 18 | // Test loading a template from a remote endpoint 19 | twig({ 20 | id: 'child-extends', 21 | href: 'templates/child.twig', 22 | 23 | load(template) { 24 | template.render({base: 'template.twig'}).should.equal('Other Title - child'); 25 | done(); 26 | } 27 | }); 28 | }); 29 | 30 | it('Should support horizontal reuse of blocks', function (done) { 31 | // Test horizontal reuse 32 | twig({ 33 | id: 'use', 34 | href: 'templates/use.twig', 35 | 36 | load(template) { 37 | template.render({place: 'user'}).should.equal('Coming soon to a user near you!'); 38 | done(); 39 | } 40 | }); 41 | }); 42 | 43 | it('should render nested blocks', function (done) { 44 | // Test rendering of blocks within blocks 45 | twig({ 46 | id: 'blocks-nested', 47 | href: 'templates/blocks-nested.twig', 48 | 49 | load(template) { 50 | template.render({ }).should.equal('parent:child'); 51 | done(); 52 | } 53 | }); 54 | }); 55 | 56 | it('should render extended nested blocks', function (done) { 57 | // Test rendering of blocks within blocks 58 | twig({ 59 | id: 'child-blocks-nested', 60 | href: 'templates/child-blocks-nested.twig', 61 | 62 | load(template) { 63 | template.render({base: 'template.twig'}).should.equal('Default Title - parent:child'); 64 | done(); 65 | } 66 | }); 67 | }); 68 | 69 | describe('block function ->', function () { 70 | it('should render block content from an included block', function (done) { 71 | twig({ 72 | href: 'templates/block-function.twig', 73 | 74 | load(template) { 75 | template.render({ 76 | base: 'block-function-parent.twig', 77 | val: 'abcd' 78 | }) 79 | .should.equal('Child content = abcd / Result: Child content = abcd'); 80 | 81 | done(); 82 | } 83 | }); 84 | }); 85 | 86 | it('should render block content from a parent block', function (done) { 87 | twig({ 88 | href: 'templates/block-parent.twig', 89 | 90 | load(template) { 91 | template.render({ 92 | base: 'block-function-parent.twig' 93 | }) 94 | .should.equal('parent block / Result: parent block'); 95 | 96 | done(); 97 | } 98 | }); 99 | }); 100 | }); 101 | 102 | describe('block shorthand ->', function () { 103 | it('should render block content using shorthand syntax', function () { 104 | twig({ 105 | data: '{% set prefix = "shorthand" %}{% block title (prefix ~ " - " ~ blockValue)|title %}' 106 | }) 107 | .render({blockValue: 'test succeeded'}) 108 | .should.equal('Shorthand - Test Succeeded'); 109 | }); 110 | it('should overload blocks from an extended template using shorthand syntax', function () { 111 | twig({ 112 | allowInlineIncludes: true, 113 | data: '{% extends "child-extends" %}{% block title "New Title" %}{% block body "new body uses the " ~ base ~ " template" %}' 114 | }) 115 | .render({base: 'template.twig'}) 116 | .should.equal('New Title - new body uses the template.twig template'); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/twig.path.js: -------------------------------------------------------------------------------- 1 | // ## twig.path.js 2 | // 3 | // This file handles path parsing 4 | module.exports = function (Twig) { 5 | 'use strict'; 6 | 7 | /** 8 | * Namespace for path handling. 9 | */ 10 | Twig.path = {}; 11 | 12 | /** 13 | * Generate the canonical version of a url based on the given base path and file path and in 14 | * the previously registered namespaces. 15 | * 16 | * @param {string} template The Twig Template 17 | * @param {string} _file The file path, may be relative and may contain namespaces. 18 | * 19 | * @return {string} The canonical version of the path 20 | */ 21 | Twig.path.parsePath = function (template, _file) { 22 | let k = null; 23 | const {namespaces} = template.options; 24 | let file = _file || ''; 25 | const hasNamespaces = namespaces && typeof namespaces === 'object'; 26 | 27 | if (hasNamespaces) { 28 | for (k in namespaces) { 29 | if (!file.includes(k)) { 30 | continue; 31 | } 32 | 33 | // Check if keyed namespace exists at path's start 34 | const colon = new RegExp('^' + k + '::'); 35 | const atSign = new RegExp('^@' + k + '/'); 36 | // Add slash to the end of path 37 | const namespacePath = namespaces[k].replace(/([^/])$/, '$1/'); 38 | 39 | if (colon.test(file)) { 40 | file = file.replace(colon, namespacePath); 41 | return file; 42 | } 43 | 44 | if (atSign.test(file)) { 45 | file = file.replace(atSign, namespacePath); 46 | return file; 47 | } 48 | } 49 | } 50 | 51 | return Twig.path.relativePath(template, file); 52 | }; 53 | 54 | /** 55 | * Generate the relative canonical version of a url based on the given base path and file path. 56 | * 57 | * @param {Twig.Template} template The Twig.Template. 58 | * @param {string} _file The file path, relative to the base path. 59 | * 60 | * @return {string} The canonical version of the path. 61 | */ 62 | Twig.path.relativePath = function (template, _file) { 63 | let base; 64 | let basePath; 65 | let sepChr = '/'; 66 | const newPath = []; 67 | let file = _file || ''; 68 | let val; 69 | 70 | if (template.url) { 71 | if (typeof template.base === 'undefined') { 72 | base = template.url; 73 | } else { 74 | // Add slash to the end of path 75 | base = template.base.replace(/([^/])$/, '$1/'); 76 | } 77 | } else if (template.path) { 78 | // Get the system-specific path separator 79 | const path = require('path'); 80 | const sep = path.sep || sepChr; 81 | const relative = new RegExp('^\\.{1,2}' + sep.replace('\\', '\\\\')); 82 | file = file.replace(/\//g, sep); 83 | 84 | if (template.base !== undefined && file.match(relative) === null) { 85 | file = file.replace(template.base, ''); 86 | base = template.base + sep; 87 | } else { 88 | base = path.normalize(template.path); 89 | } 90 | 91 | base = base.replace(sep + sep, sep); 92 | sepChr = sep; 93 | } else if ((template.name || template.id) && template.method && template.method !== 'fs' && template.method !== 'ajax') { 94 | // Custom registered loader 95 | base = template.base || template.name || template.id; 96 | } else { 97 | throw new Twig.Error('Cannot extend an inline template.'); 98 | } 99 | 100 | basePath = base.split(sepChr); 101 | 102 | // Remove file from url 103 | basePath.pop(); 104 | basePath = basePath.concat(file.split(sepChr)); 105 | 106 | while (basePath.length > 0) { 107 | val = basePath.shift(); 108 | if (val === '.') { 109 | // Ignore 110 | } else if (val === '..' && newPath.length > 0 && newPath[newPath.length - 1] !== '..') { 111 | newPath.pop(); 112 | } else { 113 | newPath.push(val); 114 | } 115 | } 116 | 117 | return newPath.join(sepChr); 118 | }; 119 | 120 | return Twig; 121 | }; 122 | -------------------------------------------------------------------------------- /test/test.macro.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Macro ->', function () { 6 | // Test loading a template from a remote endpoint 7 | it('it should load macro', function () { 8 | twig({ 9 | id: 'macro', 10 | path: 'test/templates/macro.twig', 11 | async: false 12 | }); 13 | // Load the template 14 | twig({ref: 'macro'}).render({ }).should.equal(''); 15 | }); 16 | 17 | it('it should import macro', function () { 18 | twig({ 19 | id: 'import-macro', 20 | path: 'test/templates/import.twig', 21 | async: false 22 | }); 23 | // Load the template 24 | twig({ref: 'import-macro'}).render({ }).trim().should.equal('Hello World'); 25 | }); 26 | 27 | it('it should run macro with self reference', function () { 28 | twig({ 29 | id: 'import-macro-self', 30 | path: 'test/templates/macro-self.twig', 31 | async: false 32 | }); 33 | // Load the template 34 | twig({ref: 'import-macro-self'}).render({ }).trim().should.equal('

'); 35 | }); 36 | 37 | it('it should run macro with self reference twice', function () { 38 | twig({ 39 | id: 'import-macro-self-twice', 40 | path: 'test/templates/macro-self-twice.twig', 41 | async: false 42 | }); 43 | // Load the template 44 | twig({ref: 'import-macro-self-twice'}).render({ }).trim().should.equal('

'); 45 | }); 46 | 47 | it('it should run wrapped macro with self reference', function () { 48 | twig({ 49 | id: 'import-wrapped-macro-self', 50 | path: 'test/templates/macro-wrapped.twig', 51 | async: false 52 | }); 53 | // Load the template 54 | twig({ref: 'import-wrapped-macro-self'}).render({ }).trim().should.equal('

'); 55 | }); 56 | 57 | it('it should run wrapped macro with context and self reference', function () { 58 | twig({ 59 | id: 'import-macro-context-self', 60 | path: 'test/templates/macro-context.twig', 61 | async: false 62 | }); 63 | // Load the template 64 | twig({ref: 'import-macro-context-self'}).render({greetings: 'Howdy'}).trim().should.equal('Howdy Twigjs'); 65 | }); 66 | 67 | it('it should run wrapped macro with default value for a parameter and self reference', function () { 68 | twig({ 69 | id: 'import-macro-defaults-self', 70 | path: 'test/templates/macro-defaults.twig', 71 | async: false 72 | }); 73 | // Load the template 74 | twig({ref: 'import-macro-defaults-self'}).render({ }).trim().should.equal('Howdy Twigjs'); 75 | }); 76 | 77 | it('it should run wrapped macro inside blocks', function () { 78 | twig({ 79 | id: 'import-macro-inside-block', 80 | path: 'test/templates/macro-blocks.twig', 81 | async: false 82 | }); 83 | // Load the template 84 | twig({ref: 'import-macro-inside-block'}).render({ }).trim().should.equal('Welcome
Twig Js
'); 85 | }); 86 | 87 | it('it should import selected macros from template', function () { 88 | twig({ 89 | id: 'from-macro-import', 90 | path: 'test/templates/from.twig', 91 | async: false 92 | }); 93 | // Load the template 94 | twig({ref: 'from-macro-import'}).render({ }).trim().should.equal('Hello Twig.js
'); 95 | }); 96 | 97 | it('should support inline includes by ID', function () { 98 | twig({ 99 | id: 'hello', 100 | data: '{% macro echo(name) %}Hello {{ name }}{% endmacro %}' 101 | }); 102 | 103 | const template = twig({ 104 | allowInlineIncludes: true, 105 | data: 'template with {% from "hello" import echo %}{{ echo("Twig.js") }}' 106 | }); 107 | const output = template.render(); 108 | 109 | output.should.equal('template with Hello Twig.js'); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/browser/test.browser.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Browser Loading ->', function () { 6 | it('Should load a template synchronously', function () { 7 | twig({ 8 | id: 'remote-browser', 9 | href: 'templates/test.twig', 10 | async: false 11 | }); 12 | 13 | // Verify the template was loaded 14 | twig({ref: 'remote-browser'}).render({ 15 | test: 'reload', 16 | flag: false 17 | }).should.equal('Test template = reload\n\n'); 18 | }); 19 | 20 | it('Should trigger the error callback for a missing template', function (done) { 21 | twig({ 22 | href: 'templates/notthere.twig', 23 | load(_) { 24 | // Failure 25 | throw new Error('Template didn\'t trigger error callback'); 26 | }, 27 | error(err) { 28 | console.log(err); 29 | done(); 30 | } 31 | }); 32 | }); 33 | 34 | it('Should load a template asynchronously', function (done) { 35 | // Test loading a template from a remote endpoint asynchronously 36 | twig({ 37 | id: 'remote-browser-async', 38 | href: 'templates/test.twig', 39 | 40 | // Callback after template loads 41 | load(template) { 42 | template.render({ 43 | test: 'yes', 44 | flag: true 45 | }).should.equal('Test template = yes\n\nFlag set!'); 46 | 47 | // Verify the template was saved 48 | twig({ref: 'remote-browser-async'}).render({ 49 | test: 'reload', 50 | flag: false 51 | }).should.equal('Test template = reload\n\n'); 52 | 53 | done(); 54 | } 55 | }); 56 | }); 57 | 58 | it('should be able to extend to a relative template path', function (done) { 59 | // Test loading a template from a remote endpoint 60 | twig({ 61 | href: 'templates/child.twig', 62 | 63 | load(template) { 64 | template.render({base: 'template.twig'}).should.equal('Other Title - child'); 65 | done(); 66 | } 67 | }); 68 | }); 69 | 70 | it('should be able to extend to a absolute template path', function (done) { 71 | // Test loading a template from a remote endpoint 72 | twig({ 73 | base: 'templates', 74 | href: 'templates/a/child.twig', 75 | 76 | load(template) { 77 | template.render({base: 'b/template.twig'}).should.equal('Other Title - child'); 78 | done(); 79 | } 80 | }); 81 | }); 82 | 83 | it('should load an included template with no context (sync)', function () { 84 | twig({ 85 | id: 'include', 86 | href: 'templates/include.twig', 87 | async: false 88 | }); 89 | 90 | // Load the template 91 | twig({ref: 'include'}).render({test: 'tst'}).should.equal('BeforeTest template = tst\n\nAfter'); 92 | }); 93 | 94 | it('should load an included template with additional context (sync)', function () { 95 | twig({ 96 | id: 'include-with', 97 | href: 'templates/include-with.twig', 98 | async: false 99 | }); 100 | 101 | // Load the template 102 | twig({ref: 'include-with'}).render({test: 'tst'}).should.equal('template: before,tst-mid-template: after,tst'); 103 | }); 104 | 105 | it('should load an included template with only additional context (sync)', function () { 106 | twig({ 107 | id: 'include-only', 108 | href: 'templates/include-only.twig', 109 | async: false 110 | }); 111 | 112 | // Load the template 113 | twig({ref: 'include-only'}).render({test: 'tst'}).should.equal('template: before,-mid-template: after,'); 114 | }); 115 | 116 | describe('source ->', function () { 117 | it('should load the non-compiled template source code', function () { 118 | twig({data: '{{ source("templates/source.twig") }}'}) 119 | .render() 120 | .should 121 | .equal('{% if isUserNew == true %}\n Hello {{ name }}\n{% else %}\n Welcome back {{ name }}\n{% endif %}\n'); 122 | }); 123 | 124 | it('should indicate if there was a problem loading the template if \'ignore_missing\' is false', function () { 125 | twig({data: '{{ source("templates/non-existing-source.twig", false) }}'}) 126 | .render() 127 | .should 128 | .equal('Template "templates/non-existing-source.twig" is not defined.'); 129 | }); 130 | 131 | it('should NOT indicate if there was a problem loading the template if \'ignore_missing\' is true', function () { 132 | twig({data: '{{ source("templates/non-existing-source.twig", true) }}'}) 133 | .render() 134 | .should 135 | .equal(''); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/test.extends.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Extensions ->', function () { 6 | it('should be able to extend a meta-type tag', function () { 7 | const flags = {}; 8 | 9 | Twig.extend(Twig => { 10 | Twig.exports.extendTag({ 11 | type: 'flag', 12 | regex: /^flag\s+(.+)$/, 13 | next: [], 14 | open: true, 15 | compile(token) { 16 | const expression = token.match[1]; 17 | 18 | // Compile the expression. 19 | token.stack = Reflect.apply(Twig.expression.compile, this, [{ 20 | type: Twig.expression.type.expression, 21 | value: expression 22 | }]).stack; 23 | 24 | delete token.match; 25 | return token; 26 | }, 27 | parse(token, context, _) { 28 | const name = Reflect.apply(Twig.expression.parse, this, [token.stack, context]); 29 | const output = ''; 30 | 31 | flags[name] = true; 32 | 33 | return { 34 | chain: false, 35 | output 36 | }; 37 | } 38 | }); 39 | }); 40 | 41 | twig({data: '{% flag \'enabled\' %}'}).render(); 42 | flags.enabled.should.equal(true); 43 | }); 44 | 45 | it('should be able to extend paired tags', function () { 46 | // Demo data 47 | const App = { 48 | user: 'john', 49 | users: { 50 | john: {level: 'admin'}, 51 | tom: {level: 'user'} 52 | } 53 | }; 54 | 55 | Twig.extend(Twig => { 56 | // Example of extending a tag type that would 57 | // restrict content to the specified "level" 58 | Twig.exports.extendTag({ 59 | type: 'auth', 60 | regex: /^auth\s+(.+)$/, 61 | next: ['endauth'], // Match the type of the end tag 62 | open: true, 63 | compile(token) { 64 | const expression = token.match[1]; 65 | 66 | // Turn the string expression into tokens. 67 | token.stack = Reflect.apply(Twig.expression.compile, this, [{ 68 | type: Twig.expression.type.expression, 69 | value: expression 70 | }]).stack; 71 | 72 | delete token.match; 73 | return token; 74 | }, 75 | parse(token, context, chain) { 76 | const level = Reflect.apply(Twig.expression.parse, this, [token.stack, context]); 77 | let output = ''; 78 | 79 | if (App.users[App.currentUser].level === level) { 80 | output = this.parse(token.output, context); 81 | } 82 | 83 | return { 84 | chain, 85 | output 86 | }; 87 | } 88 | }); 89 | Twig.exports.extendTag({ 90 | type: 'endauth', 91 | regex: /^endauth$/, 92 | next: [], 93 | open: false 94 | }); 95 | }); 96 | 97 | const template = twig({data: 'Welcome{% auth \'admin\' %} ADMIN{% endauth %}!'}); 98 | 99 | App.currentUser = 'john'; 100 | template.render().should.equal('Welcome ADMIN!'); 101 | 102 | App.currentUser = 'tom'; 103 | template.render().should.equal('Welcome!'); 104 | }); 105 | 106 | it('should be able to extend the same tag twice, replacing it', function () { 107 | let result; 108 | 109 | Twig.extend(Twig => { 110 | Twig.exports.extendTag({ 111 | type: 'noop', 112 | regex: /^noop$/, 113 | next: [], 114 | open: true, 115 | parse(_) { 116 | return { 117 | chain: false, 118 | output: 'noop1' 119 | }; 120 | } 121 | }); 122 | }); 123 | 124 | result = twig({data: '{% noop %}'}).render(); 125 | result.should.equal('noop1'); 126 | 127 | Twig.extend(Twig => { 128 | Twig.exports.extendTag({ 129 | type: 'noop', 130 | regex: /^noop$/, 131 | next: [], 132 | open: true, 133 | parse(_) { 134 | return { 135 | chain: false, 136 | output: 'noop2' 137 | }; 138 | } 139 | }); 140 | }); 141 | 142 | result = twig({data: '{% noop %}'}).render(); 143 | result.should.equal('noop2'); 144 | }); 145 | 146 | it('should extend the parent context when extending', function () { 147 | const template = twig({ 148 | path: 'test/templates/extender.twig', 149 | async: false 150 | }); 151 | 152 | const output = template.render(); 153 | 154 | output.trim().should.equal('ok!'); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /ASYNC.md: -------------------------------------------------------------------------------- 1 | # Twig Asynchronous Rendering 2 | 3 | ## Synchronous promises 4 | 5 | The asynchronous behaviour of Twig.js relies on promises, in order to support both the synchronous and asynchronous behaviour there is an internal promise implementation that runs fully synchronous. 6 | 7 | The internal implementation of promises does not use `setTimeout` to run through the promise chain, but instead synchronously runs through the promise chain. 8 | 9 | The different promise implementations can be mixed, synchronous behaviour however is no longer guaranteed as soon as the regular promise implementation is run. 10 | 11 | ### Examples 12 | 13 | **Internal (synchronous) implementation** 14 | 15 | [Internal implementation](https://github.com/JorgenEvens/twig.js/tree/master/src/twig.async.js#L40) 16 | 17 | ```javascript 18 | console.log('start'); 19 | Twig.Promise.resolve('1') 20 | .then(function(v) { 21 | console.log(v); 22 | return '2'; 23 | }) 24 | .then(function(v) { 25 | console.log(v); 26 | }); 27 | console.log('stop'); 28 | 29 | /** 30 | * Prints to the console: 31 | * start 32 | * 1 33 | * 2 34 | * stop 35 | */ 36 | ``` 37 | 38 | **Regular / native promises** 39 | 40 | Implementations such as the native promises or [bluebird](http://bluebirdjs.com/docs/getting-started.html) promises. 41 | 42 | ```javascript 43 | console.log('start'); 44 | Promise.resolve('1') 45 | .then(function(v) { 46 | console.log(v); 47 | return '2'; 48 | }) 49 | .then(function(v) { 50 | console.log(v); 51 | }); 52 | console.log('stop'); 53 | 54 | /** 55 | * Prints to the console: 56 | * start 57 | * stop 58 | * 1 59 | * 2 60 | */ 61 | ``` 62 | 63 | **Mixing promises** 64 | 65 | ```javascript 66 | console.log('start'); 67 | Twig.Promise.resolve('1') 68 | .then(function(v) { 69 | console.log(v); 70 | return Promise.resolve('2'); 71 | }) 72 | .then(function(v) { 73 | console.log(v); 74 | }); 75 | console.log('stop'); 76 | 77 | /** 78 | * Prints to the console: 79 | * start 80 | * 1 81 | * stop 82 | * 2 83 | */ 84 | ``` 85 | 86 | 87 | ## Async helpers 88 | 89 | To preserve the correct order of execution there is an implemenation of `Twig.forEach()` that waits any promises returned from the callback before executing the next iteration of the loop. If no promise is returned the next iteration is invoked immediately. 90 | 91 | ```javascript 92 | var arr = new Array(5); 93 | 94 | Twig.async.forEach(arr, function(value, index) { 95 | console.log(index); 96 | 97 | if (index % 2 == 0) 98 | return index; 99 | 100 | return Promise.resolve(index); 101 | }) 102 | .then(function() { 103 | console.log('finished'); 104 | }); 105 | 106 | /** 107 | * Prints to the console: 108 | * 0 109 | * 1 110 | * 2 111 | * 3 112 | * 4 113 | * finished 114 | */ 115 | ``` 116 | 117 | ## Switching render mode 118 | 119 | The rendering mode of Twig.js internally is determined by the `allow_async` argument that can be passed into `Twig.expression.parse`, `Twig.logic.parse`, `Twig.parse` and `Twig.Template.render`. Detecting if at any point code runs asynchronously is explained in [detecting asynchronous behaviour](#detecting-asynchronous-behaviour). 120 | 121 | For the end user switching between synchronous and asynchronous is as simple as using a different method on the template instance. 122 | 123 | **Render template synchronously** 124 | 125 | ```javascript 126 | var output = twig({ 127 | data: 'a {{value}}' 128 | }).render({ 129 | value: 'test' 130 | }); 131 | 132 | /** 133 | * Prints to the console: 134 | * a test 135 | */ 136 | ``` 137 | 138 | **Render template asynchronously** 139 | 140 | ```javascript 141 | var template = twig({ 142 | data: 'a {{value}}' 143 | }).renderAsync({ 144 | value: 'test' 145 | }) 146 | .then(function(output) { 147 | console.log(output); 148 | }); 149 | 150 | /** 151 | * Prints to the console: 152 | * a test 153 | */ 154 | ``` 155 | 156 | ## Detecting asynchronous behaviour 157 | 158 | The pattern used to detect asynchronous behaviour is the same everywhere it is used and follows a simple pattern. 159 | 160 | 1. Set a variable `is_async = true` 161 | 2. Run the promise chain that might contain some asynchronous behaviour. 162 | 3. As the last method in the promise chain set `is_async = false` 163 | 4. Underneath the promise chain test whether `is_async` is `true` 164 | 165 | This pattern works because the last method in the chain will be executed in the next run of the eventloop (`setTimeout`/`setImmediate`). 166 | 167 | ### Examples 168 | 169 | **Synchronous promises only** 170 | 171 | ```javascript 172 | var is_async = true; 173 | 174 | Twig.Promise.resolve() 175 | .then(function() { 176 | // We run our work in here such to allow for asynchronous work 177 | // This example is fully synchronous 178 | return 'hello world'; 179 | }) 180 | .then(function() { 181 | is_async = false; 182 | }); 183 | 184 | if (is_async) 185 | console.log('method ran asynchronous'); 186 | 187 | console.log('method ran synchronous'); 188 | 189 | /** 190 | * Prints to the console: 191 | * method ran synchronous 192 | */ 193 | ``` 194 | 195 | **Mixed promises** 196 | 197 | ```javascript 198 | var is_async = true; 199 | 200 | Twig.Promise.resolve() 201 | .then(function() { 202 | // We run our work in here such to allow for asynchronous work 203 | return Promise.resolve('hello world'); 204 | }) 205 | .then(function() { 206 | is_async = false; 207 | }); 208 | 209 | if (is_async) 210 | console.log('method ran asynchronous'); 211 | 212 | console.log('method ran synchronous'); 213 | 214 | /** 215 | * Prints to the console: 216 | * method ran asynchronous 217 | */ 218 | ``` 219 | -------------------------------------------------------------------------------- /test/test.embed.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | Twig.cache(false); 6 | 7 | describe('Twig.js Embed ->', function () { 8 | // Test loading a template from a remote endpoint 9 | it('it should load embed and render', function () { 10 | twig({ 11 | id: 'embed', 12 | path: 'test/templates/embed-simple.twig', 13 | async: false 14 | }); 15 | // Load the template 16 | twig({ref: 'embed'}).render({ }).trim().should.equal([ 17 | 'START', 18 | 'A', 19 | 'new header', 20 | 'base footer', 21 | 'B', 22 | '', 23 | 'A', 24 | 'base header', 25 | 'base footer', 26 | 'extended', 27 | 'B', 28 | '', 29 | 'A', 30 | 'base header', 31 | 'extended', 32 | 'base footer', 33 | 'extended', 34 | 'B', 35 | '', 36 | 'A', 37 | 'Super cool new header', 38 | 'Cool footer', 39 | 'B', 40 | 'END' 41 | ].join('\n')); 42 | }); 43 | 44 | it('should skip non-existent embeds flagged with "ignore missing"', function () { 45 | [ 46 | '', 47 | ' with {}', 48 | ' with {} only', 49 | ' only' 50 | ].forEach(options => { 51 | twig({ 52 | allowInlineIncludes: true, 53 | data: 'ignore-{% embed "embed-not-there.twig" ignore missing' + options + ' %}{% endembed %}missing' 54 | }).render().should.equal('ignore-missing'); 55 | }); 56 | }); 57 | 58 | it('should include the correct context using "with" and "only"', function () { 59 | twig({ 60 | data: '|{{ foo }}||{{ baz }}|', 61 | id: 'embed.twig' 62 | }); 63 | 64 | [ 65 | { 66 | expected: '|bar||qux|', 67 | options: '' 68 | }, 69 | { 70 | expected: '|bar||qux|', 71 | options: ' with {}' 72 | }, 73 | { 74 | expected: '|bar||override|', 75 | options: ' with {"baz": "override"}' 76 | }, 77 | { 78 | expected: '||||', 79 | options: ' only' 80 | }, 81 | { 82 | expected: '||||', 83 | options: ' with {} only' 84 | }, 85 | { 86 | expected: '|override|||', 87 | options: ' with {"foo": "override"} only' 88 | } 89 | ].forEach(test => { 90 | twig({ 91 | allowInlineIncludes: true, 92 | data: '{% embed "embed.twig"' + test.options + ' %}{% endembed %}' 93 | }).render({ 94 | foo: 'bar', 95 | baz: 'qux' 96 | }).should.equal(test.expected); 97 | }); 98 | }); 99 | 100 | it('should override blocks in a for loop', function () { 101 | twig({ 102 | data: '<{% block content %}original{% endblock %}>', 103 | id: 'embed.twig' 104 | }); 105 | 106 | twig({ 107 | allowInlineIncludes: true, 108 | data: '{% for i in 1..3 %}{% embed "embed.twig" %}{% block content %}override{% endblock %}{% endembed %}{% endfor %}' 109 | }).render().should.equal(''); 110 | }); 111 | 112 | it('should support complex nested embeds', function () { 113 | twig({ 114 | data: '<{% block header %}outer-header{% endblock %}><{% block footer %}outer-footer{% endblock %}>', 115 | id: 'embed-outer.twig' 116 | }); 117 | twig({ 118 | data: '{% block content %}inner-content{% endblock %}', 119 | id: 'embed-inner.twig' 120 | }); 121 | 122 | twig({ 123 | allowInlineIncludes: true, 124 | data: '{% embed "embed-outer.twig" %}{% block header %}{% embed "embed-inner.twig" %}{% block content %}override-header{% endblock %}{% endembed %}{% endblock %}{% block footer %}{% embed "embed-inner.twig" %}{% block content %}override-footer{% endblock %}{% endembed %}{% endblock %}{% endembed %}' 125 | }).render().should.equal(''); 126 | }); 127 | 128 | it('should support multiple inheritance and embeds', function () { 129 | twig({ 130 | data: '<{% block header %}base-header{% endblock %}>{% block body %}{% endblock %}<{% block footer %}base-footer{% endblock %}>', 131 | id: 'base.twig' 132 | }); 133 | twig({ 134 | data: '{% extends "base.twig" %}{% block header %}layout-header{% endblock %}{% block body %}<{% block body_header %}layout-body-header{% endblock %}>{% block body_content %}layout-body-content{% endblock %}<{% block body_footer %}layout-body-footer{% endblock %}>{% endblock %}', 135 | id: 'layout.twig' 136 | }); 137 | twig({ 138 | data: '<{% block section_title %}section-title{% endblock %}><{% block section_content %}section-content{% endblock %}>', 139 | id: 'section.twig' 140 | }); 141 | 142 | twig({ 143 | allowInlineIncludes: true, 144 | data: '{% extends "layout.twig" %}{% block body_header %}override-body-header{% endblock %}{% block body_content %}{% embed "section.twig" %}{% block section_content %}override-section-content{% endblock %}{% endembed %}{% endblock %}' 145 | }).render().should.equal(''); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Known Vulnerabilities](https://snyk.io/test/github/twigjs/twig.js/badge.svg)](https://snyk.io/test/github/twigjs/twig.js) 2 | [![Build Status](https://secure.travis-ci.org/twigjs/twig.js.svg)](http://travis-ci.org/twigjs/twig.js) 3 | [![NPM version](https://badge.fury.io/js/twig.svg)](http://badge.fury.io/js/twig) 4 | [![Gitter](https://badges.gitter.im/twigjs/twig.js.svg)](https://gitter.im/twigjs/twig.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | 6 | # About 7 | 8 | 11 | 12 | Twig.js is a pure JavaScript implementation of the Twig PHP templating language 13 | () 14 | 15 | The goal is to provide a library that is compatible with both browsers and server side JavaScript environments such as node.js. 16 | 17 | Twig.js is currently a work in progress and supports a limited subset of the Twig templating language (with more coming). 18 | 19 | ### Docs 20 | 21 | Documentation is available in the [twig.js wiki](https://github.com/twigjs/twig.js/wiki) on Github. 22 | 23 | ### Feature Support 24 | 25 | For a list of supported tags/filters/functions/tests see the [Implementation Notes](https://github.com/twigjs/twig.js/wiki/Implementation-Notes) page on the wiki. 26 | 27 | # Install 28 | 29 | Download the latest twig.js release from github: https://github.com/twigjs/twig.js/releases or via NPM: 30 | 31 | ```bash 32 | npm install twig --save 33 | ``` 34 | 35 | # Bower 36 | 37 | A bower package is available from [philsbury](https://github.com/philsbury/twigjs-bower). Please direct any Bower support issues to that repo. 38 | 39 | ## Browser Usage 40 | 41 | Include twig.js or twig.min.js in your page, then: 42 | 43 | ```js 44 | var template = Twig.twig({ 45 | data: 'The {{ baked_good }} is a lie.' 46 | }); 47 | 48 | console.log( 49 | template.render({baked_good: 'cupcake'}) 50 | ); 51 | // outputs: "The cupcake is a lie." 52 | ``` 53 | 54 | ## Webpack 55 | 56 | A loader is available from [zimmo.be](https://github.com/zimmo-be/twig-loader). 57 | 58 | ## Node Usage (npm) 59 | 60 | Tested on node >=6.0. 61 | 62 | You can use twig in your app with 63 | 64 | ```js 65 | var Twig = require('twig'), // Twig module 66 | twig = Twig.twig; // Render function 67 | ``` 68 | 69 | ### Usage without Express 70 | 71 | If you don't want to use Express, you can render a template with the following method: 72 | 73 | ```js 74 | import Twig from 'twig'; 75 | Twig.renderFile('./path/to/someFile.twig', {foo:'bar'}, (err, html) => { 76 | html; // compiled string 77 | }); 78 | ``` 79 | 80 | ### Usage with Express 81 | 82 | Twig is compatible with express 2 and 3. You can create an express app using the twig.js templating language by setting the view engine to twig. 83 | 84 | ### app.js 85 | 86 | **Express 3** 87 | 88 | ```js 89 | var Twig = require("twig"), 90 | express = require('express'), 91 | app = express(); 92 | 93 | // This section is optional and used to configure twig. 94 | app.set("twig options", { 95 | allow_async: true, // Allow asynchronous compiling 96 | strict_variables: false 97 | }); 98 | 99 | app.get('/', function(req, res){ 100 | res.render('index.twig', { 101 | message : "Hello World" 102 | }); 103 | }); 104 | 105 | app.listen(9999); 106 | ``` 107 | 108 | ## views/index.twig 109 | 110 | ```html 111 | Message of the moment: {{ message }} 112 | ``` 113 | 114 | An [Express 2 Example](https://github.com/twigjs/twig.js/wiki/Express-2) is available on the wiki. 115 | 116 | # Alternatives 117 | 118 | - [Twing](https://github.com/ericmorand/twing) 119 | 120 | # Contributing 121 | 122 | If you have a change you want to make to twig.js, feel free to fork this repository and submit a pull request on Github. The source files are located in `src/*.js`. 123 | 124 | twig.js is built by running `npm run build` 125 | 126 | For more details on getting setup, see the [contributing page](https://github.com/twigjs/twig.js/wiki/Contributing) on the wiki. 127 | 128 | ## Environment Requirements 129 | When developing on Windows, the repository must be checked out **without** automatic conversion of LF to CRLF. Failure to do so will cause tests that would otherwise pass on Linux or Mac to fail instead. 130 | 131 | ## Tests 132 | 133 | The twig.js tests are written in [Mocha][mocha] and can be invoked with `npm test`. 134 | 135 | ## License 136 | 137 | Twig.js is available under a [BSD 2-Clause License][bsd-2], see the LICENSE file for more information. 138 | 139 | ## Acknowledgments 140 | 141 | See the LICENSES.md file for copies of the referenced licenses. 142 | 143 | 1. The JavaScript Array fills in src/twig.fills.js are from and are available under the [MIT License][mit] or are [public domain][mdn-license]. 144 | 145 | 2. The Date.format function in src/twig.lib.js is from and used under a [MIT license][mit-jpaq]. 146 | 147 | 3. The sprintf implementation in src/twig.lib.js used for the format filter is from and used under a [BSD 3-Clause License][bsd-3]. 148 | 149 | 4. The strip_tags implementation in src/twig.lib.js used for the striptags filter is from and used under and [MIT License][mit-phpjs]. 150 | 151 | [mit-jpaq]: http://jpaq.org/license/ 152 | [mit-phpjs]: http://phpjs.org/pages/license/#MIT 153 | [mit]: http://www.opensource.org/licenses/mit-license.php 154 | [mdn-license]: https://developer.mozilla.org/Project:Copyrights 155 | 156 | [bsd-2]: http://www.opensource.org/licenses/BSD-2-Clause 157 | [bsd-3]: http://www.opensource.org/licenses/BSD-3-Clause 158 | [cc-by-sa-2.5]: http://creativecommons.org/licenses/by-sa/2.5/ "Creative Commons Attribution-ShareAlike 2.5 License" 159 | 160 | [mocha]: http://mochajs.org/ 161 | [qunit]: http://docs.jquery.com/QUnit 162 | -------------------------------------------------------------------------------- /test/test.expressions.operators.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Expression Operators ->', function () { 6 | describe('Precedence ->', function () { 7 | it('should correctly order \'in\'', function () { 8 | const testTemplate = twig({data: '{% if true or "anything" in ["a","b","c"] %}OK!{% endif %}'}); 9 | const output = testTemplate.render({}); 10 | 11 | output.should.equal('OK!'); 12 | }); 13 | }); 14 | 15 | describe('// ->', function () { 16 | it('should handle positive values', function () { 17 | const testTemplate = twig({data: '{{ 20 // 7 }}'}); 18 | const output = testTemplate.render({}); 19 | 20 | output.should.equal('2'); 21 | }); 22 | 23 | it('should handle negative values', function () { 24 | const testTemplate = twig({data: '{{ -20 // -7 }}'}); 25 | const output = testTemplate.render({}); 26 | 27 | output.should.equal('2'); 28 | }); 29 | 30 | it('should handle mixed sign values', function () { 31 | const testTemplate = twig({data: '{{ -20 // 7 }}'}); 32 | const output = testTemplate.render({}); 33 | 34 | output.should.equal('-3'); 35 | }); 36 | }); 37 | 38 | describe('?: ->', function () { 39 | it('should support the extended ternary operator for true conditions', function () { 40 | const testTemplate = twig({data: '{{ a ? b }}'}); 41 | const outputT = testTemplate.render({a: true, b: 'one'}); 42 | const outputF = testTemplate.render({a: false, b: 'one'}); 43 | 44 | outputT.should.equal('one'); 45 | outputF.should.equal(''); 46 | }); 47 | 48 | it('should support the extended ternary operator for false conditions', function () { 49 | const testTemplate = twig({data: '{{ a ?: b }}'}); 50 | const outputT = testTemplate.render({a: 'one', b: 'two'}); 51 | const outputF = testTemplate.render({a: false, b: 'two'}); 52 | 53 | outputT.should.equal('one'); 54 | outputF.should.equal('two'); 55 | }); 56 | }); 57 | 58 | describe('?? ->', function () { 59 | it('should support the null-coalescing operator for true conditions', function () { 60 | const testTemplate = twig({data: '{{ a ?? b }}'}); 61 | const outputT = testTemplate.render({a: 'one', b: 'two'}); 62 | const outputF = testTemplate.render({a: false, b: 'two'}); 63 | 64 | outputT.should.equal('one'); 65 | outputF.should.equal('false'); 66 | }); 67 | 68 | it('should support the null-coalescing operator for false conditions', function () { 69 | const testTemplate = twig({data: '{{ a ?? b }}'}); 70 | const outputT = testTemplate.render({a: undefined, b: 'two'}); 71 | const outputF = testTemplate.render({a: null, b: 'two'}); 72 | 73 | outputT.should.equal('two'); 74 | outputF.should.equal('two'); 75 | }); 76 | 77 | it('should support the null-coalescing operator for true conditions on objects or arrays', function () { 78 | const testTemplate = twig({data: '{% set b = a ?? "nope" %}{{ b | join("") }}'}); 79 | const outputArr = testTemplate.render({a: [1, 2]}); 80 | const outputObj = testTemplate.render({a: {b: 3, c: 4}}); 81 | const outputNull = testTemplate.render(); 82 | 83 | outputArr.should.equal('12'); 84 | outputObj.should.equal('34'); 85 | outputNull.should.equal('nope'); 86 | }); 87 | }); 88 | 89 | describe('b-and ->', function () { 90 | it('should return correct value if needed bit is set or 0 if not', function () { 91 | const testTemplate = twig({data: '{{ a b-and b }}'}); 92 | const output0 = testTemplate.render({a: 25, b: 1}); 93 | const output1 = testTemplate.render({a: 25, b: 2}); 94 | const output2 = testTemplate.render({a: 25, b: 4}); 95 | const output3 = testTemplate.render({a: 25, b: 8}); 96 | const output4 = testTemplate.render({a: 25, b: 16}); 97 | 98 | output0.should.equal('1'); 99 | output1.should.equal('0'); 100 | output2.should.equal('0'); 101 | output3.should.equal('8'); 102 | output4.should.equal('16'); 103 | }); 104 | }); 105 | 106 | describe('b-or ->', function () { 107 | it('should return initial value if needed bit is set or sum of bits if not', function () { 108 | const testTemplate = twig({data: '{{ a b-or b }}'}); 109 | const output0 = testTemplate.render({a: 25, b: 1}); 110 | const output1 = testTemplate.render({a: 25, b: 2}); 111 | const output2 = testTemplate.render({a: 25, b: 4}); 112 | const output3 = testTemplate.render({a: 25, b: 8}); 113 | const output4 = testTemplate.render({a: 25, b: 16}); 114 | 115 | output0.should.equal('25'); 116 | output1.should.equal('27'); 117 | output2.should.equal('29'); 118 | output3.should.equal('25'); 119 | output4.should.equal('25'); 120 | }); 121 | }); 122 | 123 | describe('b-xor ->', function () { 124 | it('should subtract bit if it\'s already set or add it if it\'s not', function () { 125 | const testTemplate = twig({data: '{{ a b-xor b }}'}); 126 | const output0 = testTemplate.render({a: 25, b: 1}); 127 | const output1 = testTemplate.render({a: 25, b: 2}); 128 | const output2 = testTemplate.render({a: 25, b: 4}); 129 | const output3 = testTemplate.render({a: 25, b: 8}); 130 | const output4 = testTemplate.render({a: 25, b: 16}); 131 | 132 | output0.should.equal('24'); 133 | output1.should.equal('27'); 134 | output2.should.equal('29'); 135 | output3.should.equal('17'); 136 | output4.should.equal('9'); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/test.async.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Async ->', function () { 6 | // Add some test functions to work with 7 | Twig.extendFunction('echoAsync', a => { 8 | return Promise.resolve(a); 9 | }); 10 | 11 | Twig.extendFunction('echoAsyncInternal', a => { 12 | return new Twig.Promise((resolve => { 13 | setTimeout(() => { 14 | resolve(a); 15 | }, 100); 16 | })); 17 | }); 18 | 19 | Twig.extendFilter('asyncUpper', txt => { 20 | return Promise.resolve(txt.toUpperCase()); 21 | }); 22 | 23 | Twig.extendFilter('rejectAsync', _ => { 24 | return Promise.reject(new Error('async error test')); 25 | }); 26 | 27 | it('should throw when detecting async behaviour in sync mode', function () { 28 | try { 29 | return twig({ 30 | data: '{{ echoAsync("hello world") }}' 31 | }).render(); 32 | } catch (error) { 33 | error.message.should.equal('You are using Twig.js in sync mode in combination with async extensions.'); 34 | } 35 | }); 36 | 37 | describe('Functions ->', function () { 38 | it('should handle functions that return promises', function () { 39 | return twig({ 40 | data: '{{ echoAsync("hello world") }}' 41 | }).renderAsync() 42 | .then(output => { 43 | output.should.equal('hello world'); 44 | }); 45 | }); 46 | it('should handle functions that return rejected promises', function () { 47 | return twig({ 48 | data: '{{ rejectAsync("hello world") }}', 49 | rethrow: true 50 | }).renderAsync({ 51 | rejectAsync() { 52 | return Promise.reject(new Error('async error test')); 53 | } 54 | }) 55 | .then(_ => { 56 | throw new Error('should not resolve'); 57 | }, err => { 58 | err.message.should.equal('async error test'); 59 | }); 60 | }); 61 | it('should handle slow executors for promises', function () { 62 | return twig({ 63 | data: '{{ echoAsyncInternal("hello world") }}' 64 | }).renderAsync() 65 | .then(output => { 66 | output.should.equal('hello world'); 67 | }); 68 | }); 69 | }); 70 | 71 | describe('Filters ->', function () { 72 | it('should handle filters that return promises', function () { 73 | return twig({ 74 | data: '{{ "hello world"|asyncUpper }}' 75 | }).renderAsync() 76 | .then(output => { 77 | output.should.equal('HELLO WORLD'); 78 | }); 79 | }); 80 | it('should handle filters that return rejected promises', function () { 81 | return twig({ 82 | data: '{{ "hello world"|rejectAsync }}', 83 | rethrow: true 84 | }).renderAsync() 85 | .then(_ => { 86 | throw new Error('should not resolve'); 87 | }, err => { 88 | err.message.should.equal('async error test'); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('Logic ->', function () { 94 | it('should handle logic containing async functions', function () { 95 | return twig({ 96 | data: 'hello{% if incrAsync(10) > 10 %} world{% endif %}' 97 | }).renderAsync({ 98 | incrAsync(nr) { 99 | return Promise.resolve(nr + 1); 100 | } 101 | }) 102 | .then(output => { 103 | output.should.equal('hello world'); 104 | }); 105 | }); 106 | it('should set variables to return value of promise', function () { 107 | return twig({ 108 | data: '{% set name = readName() %}hello {{ name }}', 109 | rethrow: true 110 | }).renderAsync({ 111 | readName() { 112 | return Promise.resolve('john'); 113 | } 114 | }) 115 | .then(output => { 116 | output.should.equal('hello john'); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('Macros ->', function () { 122 | it('should handle macros with async content correctly', function () { 123 | const tpl = '{% macro test(asyncIn, syncIn) %}{{asyncIn}}-{{syncIn}}{% endmacro %}' + 124 | '{% import _self as m %}' + 125 | '{{ m.test(echoAsync("hello"), "world") }}'; 126 | 127 | return twig({ 128 | data: tpl 129 | }) 130 | .renderAsync() 131 | .then(output => { 132 | output.should.equal('hello-world'); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('Twig.js Control Structures ->', function () { 138 | it('should have a loop context item available for arrays', function () { 139 | function run(tpl, result) { 140 | const testTemplate = twig({data: tpl}); 141 | return testTemplate.renderAsync({ 142 | test: [1, 2, 3, 4], async: () => Promise.resolve() 143 | }) 144 | .then(res => res.should.equal(result)); 145 | } 146 | 147 | return Promise.resolve() 148 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.index }}{% endfor %}', '1234')) 149 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.index0 }}{% endfor %}', '0123')) 150 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.revindex }}{% endfor %}', '4321')) 151 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.revindex0 }}{% endfor %}', '3210')) 152 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.length }}{% endfor %}', '4444')) 153 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.first }}{% endfor %}', 'truefalsefalsefalse')) 154 | .then(() => run('{% for key,value in test %}{{async()}}{{ loop.last }}{% endfor %}', 'falsefalsefalsetrue')); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/test.fs.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Loader ->', function () { 6 | it('should load a template from the filesystem asynchronously', function (done) { 7 | twig({ 8 | id: 'fs-node-async', 9 | path: 'test/templates/test.twig', 10 | load(template) { 11 | // Render the template 12 | template.render({ 13 | test: 'yes', 14 | flag: true 15 | }).should.equal('Test template = yes\n\nFlag set!'); 16 | 17 | done(); 18 | } 19 | }); 20 | }); 21 | it('should load a template from the filesystem synchronously', function () { 22 | const template = twig({ 23 | id: 'fs-node-sync', 24 | path: 'test/templates/test.twig', 25 | async: false 26 | }); 27 | // Render the template 28 | template.render({ 29 | test: 'yes', 30 | flag: true 31 | }).should.equal('Test template = yes\n\nFlag set!'); 32 | }); 33 | 34 | describe('source ->', function () { 35 | it('should load the non-compiled template source code', function () { 36 | twig({data: '{{ source("test/templates/source.twig") }}'}) 37 | .render() 38 | .should 39 | .equal('{% if isUserNew == true %}\n Hello {{ name }}\n{% else %}\n Welcome back {{ name }}\n{% endif %}\n'); 40 | }); 41 | 42 | it('should indicate if there was a problem loading the template if \'ignore_missing\' is false', function () { 43 | twig({data: '{{ source("test/templates/non-existing-source.twig", false) }}'}) 44 | .render() 45 | .should 46 | .equal('Template "test/templates/non-existing-source.twig" is not defined.'); 47 | }); 48 | 49 | it('should NOT indicate if there was a problem loading the template if \'ignore_missing\' is true', function () { 50 | twig({data: '{{ source("test/templates/non-existing-source.twig", true) }}'}) 51 | .render() 52 | .should 53 | .equal(''); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('Twig.js Include ->', function () { 59 | it('should load an included template with no context', function () { 60 | twig({ 61 | id: 'include', 62 | path: 'test/templates/include.twig', 63 | async: false 64 | }); 65 | 66 | // Load the template 67 | twig({ref: 'include'}).render({test: 'tst'}).should.equal('BeforeTest template = tst\n\nAfter'); 68 | }); 69 | 70 | it('should load an included template using relative path', function () { 71 | twig({ 72 | id: 'include-relative', 73 | path: 'test/templates/include/relative.twig', 74 | async: false 75 | }); 76 | 77 | // Load the template 78 | twig({ref: 'include-relative'}).render().should.equal('Twig.js!'); 79 | }); 80 | 81 | it('should load the first template when passed an array', function () { 82 | twig({ 83 | id: 'include-array', 84 | path: 'test/templates/include-array.twig', 85 | async: false 86 | }); 87 | 88 | // Load the template 89 | twig({ref: 'include-array'}).render({test: 'tst'}).should.equal('BeforeTest template = tst\n\nAfter'); 90 | }); 91 | 92 | it('should load the second template when passed an array where the first value does not exist', function () { 93 | twig({ 94 | id: 'include-array-second-exists', 95 | path: 'test/templates/include-array-second-exists.twig', 96 | async: false 97 | }); 98 | 99 | // Load the template 100 | twig({ref: 'include-array'}).render({test: 'tst'}).should.equal('BeforeTest template = tst\n\nAfter'); 101 | }); 102 | 103 | it('should load an included template with additional context', function () { 104 | twig({ 105 | id: 'include-with', 106 | path: 'test/templates/include-with.twig', 107 | async: false 108 | }); 109 | 110 | // Load the template 111 | twig({ref: 'include-with'}).render({test: 'tst'}).should.equal('template: before,tst-mid-template: after,tst'); 112 | }); 113 | 114 | it('should load an included template with only additional context', function () { 115 | twig({ 116 | id: 'include-only', 117 | path: 'test/templates/include-only.twig', 118 | async: false 119 | }); 120 | 121 | // Load the template 122 | twig({ref: 'include-only'}).render({test: 'tst'}).should.equal('template: before,-mid-template: after,'); 123 | }); 124 | 125 | it('should skip a nonexistent included template flagged wth \'ignore missing\'', function () { 126 | twig({ 127 | id: 'include-ignore-missing', 128 | path: 'test/templates/include-ignore-missing.twig', 129 | async: false 130 | }); 131 | 132 | twig({ref: 'include-ignore-missing'}).render().should.equal('ignore-missing'); 133 | }); 134 | 135 | it('should fail including a nonexistent included template not flagged wth \'ignore missing\'', function () { 136 | try { 137 | twig({ 138 | id: 'include-ignore-missing-missing', 139 | path: 'test/templates/include-ignore-missing-missing.twig', 140 | async: false, 141 | rethrow: true 142 | }).render(); 143 | } catch (error) { 144 | error.type.should.equal('TwigException'); 145 | } 146 | }); 147 | 148 | it('should fail including a nonexistent included template asynchronously', function (done) { 149 | twig({ 150 | id: 'include-ignore-missing-missing-async', 151 | path: 'test/templates/include-ignore-missing-missing-async.twig', 152 | async: true, 153 | load(template) { 154 | template.should.not.exist(); 155 | done(); 156 | }, 157 | error(err) { 158 | err.type.should.equal('TwigException'); 159 | done(); 160 | }, 161 | rethrow: true 162 | }); 163 | }); 164 | }); 165 | 166 | describe('Twig.js Extends ->', function () { 167 | it('should load the first template when passed an array', function () { 168 | const template = twig({ 169 | path: 'test/templates/extender-array.twig', 170 | async: false 171 | }); 172 | 173 | const output = template.render(); 174 | output.trim().should.equal('Hello, world!'); 175 | }); 176 | 177 | it('should load the second template when passed an array where the first value does not exist', function () { 178 | const template = twig({ 179 | path: 'test/templates/extender-array-second-exists.twig', 180 | async: false 181 | }); 182 | 183 | const output = template.render(); 184 | output.trim().should.equal('Hello, world!'); 185 | }); 186 | 187 | it('should silently fail when passed an array with no templates that exist', function () { 188 | const template = twig({ 189 | path: 'test/templates/extender-array-none-exist.twig', 190 | async: false 191 | }); 192 | 193 | const output = template.render(); 194 | output.trim().should.equal('Nothing to see here'); 195 | }); 196 | }); -------------------------------------------------------------------------------- /test/test.tests.js: -------------------------------------------------------------------------------- 1 | const Twig = require('../twig').factory(); 2 | 3 | const {twig} = Twig; 4 | 5 | describe('Twig.js Tests ->', function () { 6 | describe('empty test ->', function () { 7 | it('should identify numbers as not empty', function () { 8 | // Number 9 | twig({data: '{{ 1 is empty }}'}).render().should.equal('false'); 10 | twig({data: '{{ 0 is empty }}'}).render().should.equal('false'); 11 | }); 12 | 13 | it('should identify empty strings', function () { 14 | // String 15 | twig({data: '{{ "" is empty }}'}).render().should.equal('true'); 16 | twig({data: '{{ "test" is empty }}'}).render().should.equal('false'); 17 | }); 18 | 19 | it('should identify empty arrays', function () { 20 | // Array 21 | twig({data: '{{ [] is empty }}'}).render().should.equal('true'); 22 | twig({data: '{{ ["1"] is empty }}'}).render().should.equal('false'); 23 | }); 24 | 25 | it('should identify empty objects', function () { 26 | // Object 27 | twig({data: '{{ {} is empty }}'}).render().should.equal('true'); 28 | twig({data: '{{ {"a":"b"} is empty }}'}).render().should.equal('false'); 29 | twig({data: '{{ {"a":"b"} is not empty }}'}).render().should.equal('true'); 30 | }); 31 | }); 32 | 33 | describe('odd test ->', function () { 34 | it('should identify a number as odd', function () { 35 | twig({data: '{{ (1 + 4) is odd }}'}).render().should.equal('true'); 36 | twig({data: '{{ 6 is odd }}'}).render().should.equal('false'); 37 | }); 38 | }); 39 | 40 | describe('even test ->', function () { 41 | it('should identify a number as even', function () { 42 | twig({data: '{{ (1 + 4) is even }}'}).render().should.equal('false'); 43 | twig({data: '{{ 6 is even }}'}).render().should.equal('true'); 44 | }); 45 | }); 46 | 47 | describe('divisibleby test ->', function () { 48 | it('should determine if a number is divisible by the given number', function () { 49 | twig({data: '{{ 5 is divisibleby(3) }}'}).render().should.equal('false'); 50 | twig({data: '{{ 6 is divisibleby(3) }}'}).render().should.equal('true'); 51 | }); 52 | }); 53 | 54 | describe('defined test ->', function () { 55 | it('should identify a key as defined if it exists in the render context', function () { 56 | twig({data: '{{ key is defined }}'}).render().should.equal('false'); 57 | twig({data: '{{ key is defined }}'}).render({key: 'test'}).should.equal('true'); 58 | const context = { 59 | key: { 60 | foo: 'bar', 61 | nothing: null 62 | }, 63 | nothing: null 64 | }; 65 | twig({data: '{{ key.foo is defined }}'}).render(context).should.equal('true'); 66 | twig({data: '{{ key.bar is defined }}'}).render(context).should.equal('false'); 67 | twig({data: '{{ key.foo.bar is defined }}'}).render(context).should.equal('false'); 68 | twig({data: '{{ foo.bar is defined }}'}).render(context).should.equal('false'); 69 | twig({data: '{{ nothing is defined }}'}).render(context).should.equal('true'); 70 | twig({data: '{{ key.nothing is defined }}'}).render(context).should.equal('true'); 71 | }); 72 | }); 73 | 74 | describe('none test ->', function () { 75 | it('should identify a key as none if it exists in the render context and is null', function () { 76 | twig({data: '{{ key is none }}'}).render().should.equal('false'); 77 | twig({data: '{{ key is none }}'}).render({key: 'test'}).should.equal('false'); 78 | twig({data: '{{ key is none }}'}).render({key: null}).should.equal('true'); 79 | twig({data: '{{ key is null }}'}).render({key: null}).should.equal('true'); 80 | }); 81 | }); 82 | 83 | describe('`sameas` backwards compatibility with `same as`', function () { 84 | it('should identify the exact same type as true', function () { 85 | twig({data: '{{ true is sameas(true) }}'}).render().should.equal('true'); 86 | twig({data: '{{ a is sameas(1) }}'}).render({a: 1}).should.equal('true'); 87 | twig({data: '{{ a is sameas("test") }}'}).render({a: 'test'}).should.equal('true'); 88 | twig({data: '{{ a is sameas(true) }}'}).render({a: true}).should.equal('true'); 89 | }); 90 | it('should identify the different types as false', function () { 91 | twig({data: '{{ false is sameas(true) }}'}).render().should.equal('false'); 92 | twig({data: '{{ true is sameas(1) }}'}).render().should.equal('false'); 93 | twig({data: '{{ false is sameas("") }}'}).render().should.equal('false'); 94 | twig({data: '{{ a is sameas(1) }}'}).render({a: '1'}).should.equal('false'); 95 | }); 96 | }); 97 | 98 | describe('same as test ->', function () { 99 | it('should identify the exact same type as true', function () { 100 | twig({data: '{{ true is same as(true) }}'}).render().should.equal('true'); 101 | twig({data: '{{ a is same as(1) }}'}).render({a: 1}).should.equal('true'); 102 | twig({data: '{{ a is same as("test") }}'}).render({a: 'test'}).should.equal('true'); 103 | twig({data: '{{ a is same as(true) }}'}).render({a: true}).should.equal('true'); 104 | }); 105 | it('should identify the different types as false', function () { 106 | twig({data: '{{ false is same as(true) }}'}).render().should.equal('false'); 107 | twig({data: '{{ true is same as(1) }}'}).render().should.equal('false'); 108 | twig({data: '{{ false is same as("") }}'}).render().should.equal('false'); 109 | twig({data: '{{ a is same as(1) }}'}).render({a: '1'}).should.equal('false'); 110 | }); 111 | }); 112 | 113 | describe('iterable test ->', function () { 114 | const data = { 115 | foo: [], 116 | traversable: 15, 117 | obj: {}, 118 | val: 'test' 119 | }; 120 | 121 | it('should fail on non-iterable data types', function () { 122 | twig({data: '{{ val is iterable ? \'ok\' : \'ko\' }}'}).render(data).should.equal('ko'); 123 | twig({data: '{{ val is iterable ? \'ok\' : \'ko\' }}'}).render({val: null}).should.equal('ko'); 124 | twig({data: '{{ val is iterable ? \'ok\' : \'ko\' }}'}).render({}).should.equal('ko'); 125 | }); 126 | 127 | it('should pass on iterable data types', function () { 128 | twig({data: '{{ foo is iterable ? \'ok\' : \'ko\' }}'}).render(data).should.equal('ok'); 129 | twig({data: '{{ obj is iterable ? \'ok\' : \'ko\' }}'}).render(data).should.equal('ok'); 130 | }); 131 | }); 132 | 133 | describe('Context test ->', function () { 134 | class Foo { 135 | constructor(a) { 136 | this.x = { 137 | test: a 138 | }; 139 | this.y = 9; 140 | } 141 | 142 | get test() { 143 | return this.x.test; 144 | } 145 | 146 | runme() { 147 | // This is out of context when runme() is called from the view 148 | return '1' + this.y; 149 | } 150 | } 151 | 152 | const foobar = new Foo('123'); 153 | 154 | it('should pass when test.runme returns 19', function () { 155 | twig({data: '{{test.runme()}}'}).render({test: foobar}).should.equal('19'); 156 | }); 157 | 158 | it('should pass when test.test returns 123', function () { 159 | twig({data: '{{test.test}}'}).render({test: foobar}).should.equal('123'); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/twig.exports.js: -------------------------------------------------------------------------------- 1 | // ## twig.exports.js 2 | // 3 | // This file provides extension points and other hooks into the twig functionality. 4 | 5 | module.exports = function (Twig) { 6 | 'use strict'; 7 | Twig.exports = { 8 | VERSION: Twig.VERSION 9 | }; 10 | 11 | /** 12 | * Create and compile a twig.js template. 13 | * 14 | * @param {Object} param Paramteres for creating a Twig template. 15 | * 16 | * @return {Twig.Template} A Twig template ready for rendering. 17 | */ 18 | Twig.exports.twig = function (params) { 19 | 'use strict'; 20 | const {id} = params; 21 | const options = { 22 | strictVariables: params.strict_variables || false, 23 | // TODO: turn autoscape on in the next major version 24 | autoescape: (params.autoescape !== null && params.autoescape) || false, 25 | allowInlineIncludes: params.allowInlineIncludes || false, 26 | rethrow: params.rethrow || false, 27 | namespaces: params.namespaces 28 | }; 29 | 30 | if (Twig.cache && id) { 31 | Twig.validateId(id); 32 | } 33 | 34 | if (params.debug !== undefined) { 35 | Twig.debug = params.debug; 36 | } 37 | 38 | if (params.trace !== undefined) { 39 | Twig.trace = params.trace; 40 | } 41 | 42 | if (params.data !== undefined) { 43 | return Twig.Templates.parsers.twig({ 44 | data: params.data, 45 | path: Object.hasOwnProperty.call(params, 'path') ? params.path : undefined, 46 | module: params.module, 47 | id, 48 | options 49 | }); 50 | } 51 | 52 | if (params.ref !== undefined) { 53 | if (params.id !== undefined) { 54 | throw new Twig.Error('Both ref and id cannot be set on a twig.js template.'); 55 | } 56 | 57 | return Twig.Templates.load(params.ref); 58 | } 59 | 60 | if (params.method !== undefined) { 61 | if (!Twig.Templates.isRegisteredLoader(params.method)) { 62 | throw new Twig.Error('Loader for "' + params.method + '" is not defined.'); 63 | } 64 | 65 | return Twig.Templates.loadRemote(params.name || params.href || params.path || id || undefined, { 66 | id, 67 | method: params.method, 68 | parser: params.parser || 'twig', 69 | base: params.base, 70 | module: params.module, 71 | precompiled: params.precompiled, 72 | async: params.async, 73 | options 74 | 75 | }, params.load, params.error); 76 | } 77 | 78 | if (params.href !== undefined) { 79 | return Twig.Templates.loadRemote(params.href, { 80 | id, 81 | method: 'ajax', 82 | parser: params.parser || 'twig', 83 | base: params.base, 84 | module: params.module, 85 | precompiled: params.precompiled, 86 | async: params.async, 87 | options 88 | 89 | }, params.load, params.error); 90 | } 91 | 92 | if (params.path !== undefined) { 93 | return Twig.Templates.loadRemote(params.path, { 94 | id, 95 | method: 'fs', 96 | parser: params.parser || 'twig', 97 | base: params.base, 98 | module: params.module, 99 | precompiled: params.precompiled, 100 | async: params.async, 101 | options 102 | }, params.load, params.error); 103 | } 104 | }; 105 | 106 | // Extend Twig with a new filter. 107 | Twig.exports.extendFilter = function (filter, definition) { 108 | Twig.filter.extend(filter, definition); 109 | }; 110 | 111 | // Extend Twig with a new function. 112 | Twig.exports.extendFunction = function (fn, definition) { 113 | Twig._function.extend(fn, definition); 114 | }; 115 | 116 | // Extend Twig with a new test. 117 | Twig.exports.extendTest = function (test, definition) { 118 | Twig.test.extend(test, definition); 119 | }; 120 | 121 | // Extend Twig with a new definition. 122 | Twig.exports.extendTag = function (definition) { 123 | Twig.logic.extend(definition); 124 | }; 125 | 126 | // Provide an environment for extending Twig core. 127 | // Calls fn with the internal Twig object. 128 | Twig.exports.extend = function (fn) { 129 | fn(Twig); 130 | }; 131 | 132 | /** 133 | * Provide an extension for use with express 2. 134 | * 135 | * @param {string} markup The template markup. 136 | * @param {array} options The express options. 137 | * 138 | * @return {string} The rendered template. 139 | */ 140 | Twig.exports.compile = function (markup, options) { 141 | const id = options.filename; 142 | const path = options.filename; 143 | 144 | // Try to load the template from the cache 145 | const template = new Twig.Template({ 146 | data: markup, 147 | path, 148 | id, 149 | options: options.settings['twig options'] 150 | }); // Twig.Templates.load(id) || 151 | 152 | return function (context) { 153 | return template.render(context); 154 | }; 155 | }; 156 | 157 | /** 158 | * Provide an extension for use with express 3. 159 | * 160 | * @param {string} path The location of the template file on disk. 161 | * @param {Object|Function} The options or callback. 162 | * @param {Function} fn callback. 163 | * 164 | * @throws Twig.Error 165 | */ 166 | Twig.exports.renderFile = function (path, options, fn) { 167 | // Handle callback in options 168 | if (typeof options === 'function') { 169 | fn = options; 170 | options = {}; 171 | } 172 | 173 | options = options || {}; 174 | 175 | const settings = options.settings || {}; 176 | 177 | // Mixin any options provided to the express app. 178 | const viewOptions = settings['twig options']; 179 | 180 | const params = { 181 | path, 182 | base: settings.views, 183 | load(template) { 184 | // Render and return template as a simple string, see https://github.com/twigjs/twig.js/pull/348 for more information 185 | if (!viewOptions || !viewOptions.allowAsync) { 186 | fn(null, String(template.render(options))); 187 | return; 188 | } 189 | 190 | template.renderAsync(options) 191 | .then(out => fn(null, out), fn); 192 | }, 193 | error(err) { 194 | fn(err); 195 | } 196 | }; 197 | 198 | if (viewOptions) { 199 | for (const option in viewOptions) { 200 | if (Object.hasOwnProperty.call(viewOptions, option)) { 201 | params[option] = viewOptions[option]; 202 | } 203 | } 204 | } 205 | 206 | Twig.exports.twig(params); 207 | }; 208 | 209 | // Express 3 handler 210 | Twig.exports.__express = Twig.exports.renderFile; 211 | 212 | /** 213 | * Shoud Twig.js cache templates. 214 | * Disable during development to see changes to templates without 215 | * reloading, and disable in production to improve performance. 216 | * 217 | * @param {boolean} cache 218 | */ 219 | Twig.exports.cache = function (cache) { 220 | Twig.cache = cache; 221 | }; 222 | 223 | // We need to export the path module so we can effectively test it 224 | Twig.exports.path = Twig.path; 225 | 226 | // Export our filters. 227 | // Resolves #307 228 | Twig.exports.filters = Twig.filters; 229 | 230 | // Export our tests. 231 | Twig.exports.tests = Twig.tests; 232 | 233 | // Export our functions. 234 | Twig.exports.functions = Twig.functions; 235 | 236 | Twig.exports.Promise = Twig.Promise; 237 | 238 | return Twig; 239 | }; 240 | --------------------------------------------------------------------------------