'
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 | [](https://snyk.io/test/github/twigjs/twig.js)
2 | [](http://travis-ci.org/twigjs/twig.js)
3 | [](http://badge.fury.io/js/twig)
4 | [](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 |
--------------------------------------------------------------------------------