',
34 | actual: await render_strict('{{ steps.fetch_labels.records | map: "value"=id, "label"=name }}', {
35 | steps: {
36 | fetch_labels: {
37 | records: [
38 | { id: 1, name: 'doowb' },
39 | { id: 2, name: 'jonschlinkert' },
40 | { id: 3, name: 'foo' },
41 | { id: 4, name: 'bar' },
42 | { id: 5, name: 'baz' }
43 | ]
44 | }
45 | }
46 | }, {
47 | output: { type: 'array' }
48 | })
49 | });
50 |
51 | })();
52 |
--------------------------------------------------------------------------------
/examples/floats.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | const Dry = require('..');
4 | const { Template } = Dry;
5 |
6 | // const locals = { 0: { 0: { 0: 'It worked!' } } };
7 | const locals = { 0: [['foo', 'bar']] };
8 |
9 | Template.render(`
10 |
11 | {% assign arr = ["a", "b"] %}
12 | "0.0.0" => {{ "0.0.0" }},
13 | 0[0] => {{ 0[0] }},
14 | 0["0"] => {{ 0["0"] }}
15 | 0[0].1 => {{ 0[0].1 }}
16 | 0.0.1 => {{ 0.0.1 }}
17 | 0."0".1 => {{ 0."0".1 }}
18 | "0.0.0" => {{ "0.0.0" }}
19 | "arr[0]" => {{ arr[0] }}
20 |
21 | `, locals)
22 | .then(console.log)
23 | .catch(console.error);
24 |
--------------------------------------------------------------------------------
/examples/render.js:
--------------------------------------------------------------------------------
1 | const start = Date.now();
2 | const { Template } = require('..');
3 |
4 | console.log('Elapsed:', Date.now() - start);
5 |
6 | const template = new Template();
7 | template.parse('Hello {{ name }}!');
8 |
9 | console.log('Elapsed:', Date.now() - start);
10 |
11 | template.render({ name: 'Unai' })
12 | .then(output => {
13 | console.log(output);
14 | console.log('Total:', Date.now() - start);
15 | });
16 |
--------------------------------------------------------------------------------
/examples/tags/apply.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | const source = `
5 | {%- apply upcase -%}
6 | This is inner
7 | {% endapply %}
8 |
9 | {%- apply split: '' | join: '-' -%}
10 | {% apply upcase -%}
11 | This is inner
12 | {%- endapply %}
13 | This is outer
14 | {%- endapply %}
15 |
16 | This should not render:{{ apply }}
17 | `;
18 |
19 | const template = Dry.Template.parse(source);
20 | template.render({}).then(console.log).catch(console.error);
21 |
--------------------------------------------------------------------------------
/examples/tags/blanks.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | const source = ' {%- assign foo = "bar" -%} {%- case foo -%} {%- when "bar" -%} {%- when "whatever" -%} {% else %} {%- endcase -%} ';
5 |
6 | const template = Dry.Template.parse(source);
7 |
8 | (async () => {
9 |
10 | console.log({
11 | expected: '',
12 | actual: await template.render()
13 | });
14 |
15 | })();
16 |
--------------------------------------------------------------------------------
/examples/tags/case.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | const source = `{% case shipping_method.title %}
5 | {% when 'International Shipping' %}
6 | You're shipping internationally. Your order should arrive in 2–3 weeks.
7 | {% when 'Domestic Shipping' %}
8 | Your order should arrive in 3–4 days.
9 | {% when 'Local Pick-Up' %}
10 | Your order will be ready for pick-up tomorrow.
11 | {% else %}
12 | Thank you for your order!
13 | {% endcase %}`;
14 |
15 | const template = Dry.Template.parse(source);
16 |
17 | template.render_strict({ shipping_method: { title: 'Domestic Shipping' } })
18 | .then(console.log)
19 | .catch(console.error);
20 |
21 | template.render_strict({ shipping_method: { title: 'Local Pick-Up' } })
22 | .then(console.log)
23 | .catch(console.error);
24 |
--------------------------------------------------------------------------------
/examples/tags/cycle.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 | const { Template } = Dry;
4 |
5 | const source = `
6 | {%- for i in (1..10) -%}
7 | {% cycle 1,2,3 -%}{%- assign n = i % 2 -%}
8 | {%- if n == 0 %}
9 | A
10 | {% else %}
11 | B
12 | {% endif %}
13 | {%- endfor -%}
14 | `;
15 |
16 | const template = Template.parse(source, { path: 'source.html' });
17 | template.render({}).then(console.log).catch(console.error);
18 |
19 |
--------------------------------------------------------------------------------
/examples/tags/embed.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | class FileSystem {
5 | constructor(values) {
6 | this.values = values;
7 | }
8 | read_template_file(template_path) {
9 | return this.values[template_path];
10 | }
11 | }
12 |
13 | Dry.Template.file_system = new FileSystem({
14 | 'vertical_boxes_skeleton.liquid': `
15 | {{ foo }}
16 | {{ bar }}
17 |
18 | {%- block top %}Top box default content{% endblock %}
19 |
20 |
21 |
22 | {% block middle %}Middle box default content{% endblock %}
23 |
24 |
25 |
26 | {% block bottom %}Bottom box default content{% endblock %}
27 |
28 |
29 | {% block footer -%}
30 |
31 | {% endblock %}
32 | `
33 | });
34 |
35 | const source = `
36 | Before
37 | {% embed "vertical_boxes_skeleton.liquid" with data %}
38 | {% block top %}
39 | Some content for the top box
40 | {%- endblock %}
41 |
42 | {% block bottom -%}
43 | Some content for the bottom box
44 | {%- endblock %}
45 | {% endembed %}
46 | After
47 | `;
48 |
49 | const template = Dry.Template.parse(source, { path: 'source.html' });
50 | template.render({ data: { foo: 'one', bar: 'two' } }).then(console.log).catch(console.error);
51 |
52 |
--------------------------------------------------------------------------------
/examples/tags/extends-block-function.js:
--------------------------------------------------------------------------------
1 |
2 | const { Template } = require('../..');
3 |
4 | class FileSystem {
5 | constructor(values) {
6 | this.values = values;
7 | }
8 | read_template_file(name) {
9 | return this.values[name];
10 | }
11 | }
12 |
13 | const templates = {
14 | 'common_blocks.liquid': `
15 | {% block 'title' %}
16 | This is from comment_blocks.liquid
17 | {% endblock %}
18 | `
19 | };
20 |
21 | const source = `
22 | {% extends "common_blocks.liquid" %}
23 | {% block 'title' %}This is a title{% endblock %}
24 | {% block 'footer' %}This is a footer{% endblock %}
25 | {{ block('title') }}
26 | {{ block('title') }}
27 |
28 | {{ block("title", "common_blocks.liquid") }}
29 |
30 | {{ block('title') }}
31 | {{ block('title') }}
32 |
33 | {% if block("footer") is defined %}
34 | Footer is defined
35 | {% endif %}
36 |
37 | `;
38 |
39 | Template.file_system = new FileSystem(templates);
40 |
41 | Template.render_strict(source, {}, { path: 'source.html' })
42 | .then(console.log)
43 | .catch(console.error);
44 |
--------------------------------------------------------------------------------
/examples/tags/extends-block-super.js:
--------------------------------------------------------------------------------
1 |
2 | const { Template } = require('../..');
3 |
4 | class FileSystem {
5 | constructor(values) {
6 | this.values = values;
7 | }
8 | read_template_file(name) {
9 | return this.values[name];
10 | }
11 | }
12 |
13 | const templates = {
14 | 'base.html': `
15 | {% block 'sidebar' %}
16 | This is from base.html
17 | {% endblock %}
18 | `
19 | };
20 |
21 | const source = `
22 | {% extends "base.html" %}
23 | {% block 'sidebar' %}
24 | Table Of Contents
25 | ...
26 | {{ super() }}
27 | {% endblock %}
28 | `;
29 |
30 | Template.file_system = new FileSystem(templates);
31 |
32 | Template
33 | .parse(source, { path: 'source.html' })
34 | .render_strict()
35 | .then(console.log);
36 |
--------------------------------------------------------------------------------
/examples/tags/extends-with-parent.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | class FileSystem {
5 | constructor(files) {
6 | this.files = files;
7 | }
8 |
9 | read_template_file(path) {
10 | return this.files[path];
11 | }
12 | }
13 |
14 | const files = {
15 | 'base.html': `
16 |
17 |
18 |
19 | {% block 'head' %}
20 | {% block 'title' %}Default Title{% endblock %}
21 | {% endblock %}
22 |
23 |
24 | {% block 'content' %} {% endblock %}
25 | {% block 'footer' %}{% endblock %}
26 |
27 |
28 | `
29 | };
30 |
31 | const source = `
32 | {% extends "base.html" %}
33 | {% block 'title' %}{{ page.title }}{% endblock %}
34 | {% block 'head' %}
35 | {{ super() }}
36 |
39 | {% endblock %}
40 | {% block 'content' %}
41 | Index
42 |
43 | Welcome on my awesome homepage.
44 |
45 | {% endblock %}
46 | `;
47 |
48 | const layouts = Dry.Template.file_system = new FileSystem(files);
49 | console.log(layouts);
50 | const template = Dry.Template.parse(source);
51 | template.render({ page: { title: 'Home' } }, { registers: { layouts } })
52 | .then(v => console.log({ v }))
53 | .catch(console.error);
54 |
--------------------------------------------------------------------------------
/examples/tags/extends.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | class FileSystem {
5 | constructor(files) {
6 | this.files = files;
7 | }
8 |
9 | read_template_file(path) {
10 | return this.files[path];
11 | }
12 | }
13 |
14 | const templates = {
15 | base: `
16 |
17 |
18 |
19 | {% block head %}
20 | {% block title %}Default Title{% endblock %}
21 | {% endblock %}
22 |
23 |
24 | {% block content %} {% endblock %}
25 | {% block footer %}{% endblock %}
26 |
27 |
28 | `
29 | };
30 |
31 | const templates2 = {
32 | layouts: {
33 | 'base.html': `
34 |
35 |
36 |
37 | {% block head %}{% endblock %}
38 |
39 |
40 | {% block content %}Default content{% endblock %}
41 | {% block footer %}{% endblock %}
42 |
43 |
44 | `,
45 | 'foo.html': `
46 | {%- extends "layouts/base.html" -%}
47 | {% block content %}{{ parent() }}Foo content{% endblock %}
48 | {% block footer %}{{ parent() }}Foo footer{% endblock %}
49 | `,
50 | 'bar.html': `
51 | {%- extends "layouts/foo.html" %}
52 | {% block content mode="append" %}{{ parent() }}Bar content{% endblock %}
53 | {% block footer mode="append" %}{{ parent() }}Bar footer{% endblock %}
54 | `,
55 | 'baz.html': `
56 | {%- extends "layouts/bar.html" %}
57 | {% block content mode="append" %}Baz content{% endblock %}
58 | {% block footer mode="append" %}Baz footer{% endblock %}
59 | `
60 | }
61 | };
62 |
63 | Dry.Template.file_system = new FileSystem(templates2);
64 |
65 | const source = `
66 | {%- extends "layouts/foo.html" -%}
67 | {%- block head %} Home {% endblock -%}
68 | {%- block content mode="append" %} New content {% endblock -%}
69 | `;
70 |
71 | // const block = `
72 | // {% block content mode="append" %} New content {% endblock %}
73 | // `;
74 |
75 | const template = Dry.Template.parse(source, { path: 'source.html' });
76 | template.render({}, { registers: templates2 }).then(console.log);
77 |
--------------------------------------------------------------------------------
/examples/tags/for.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 | const pkg = require('../../package');
4 |
5 | const fixtures = {
6 | // key_value: `
7 | // {% for key, user in users -%}
8 | // {{ key }}: {{ user.username|e }}
9 | // {%- endfor %}
10 | // `,
11 |
12 | // key_value2: `
13 | // {% for key in pkg %}
14 | // {{ key }}
15 | // {% endfor %}
16 | // `,
17 |
18 | // key_value3: `
19 | // {% for key, value in pkg -%}
20 | // {{ key }}: {{ value }}
21 | // {% endfor %}
22 | // `,
23 |
24 | // key_value4: `
25 | // {%- for link in page.links %}
26 | // {{ link[0] }}: {{ link[1].version | default: link[1] }}
27 | // {%- endfor %}
28 | // `,
29 |
30 | // key_value5: `
31 | // {% for item in pkg -%}
32 | // {{ item[0] }}: {{ item[1] | json }}
33 | // {% endfor %}
34 | // `
35 |
36 | // for_loop_vars: `
37 | // {% for a in (1..20) -%}
38 | // {{forloop.index0}}
39 | // {%- endfor %}
40 | // `,
41 |
42 | // for_loop_vars_at: `
43 | // {% for letter in (1..20) %}
44 | // {{ @rindex }}
45 | // {%- endfor %}
46 | // `,
47 |
48 | // range: `
49 | // {% for a in (1..10) -%}
50 | // {{ forloop.index0 }}
51 | // {%- endfor -%}
52 |
53 | // `,
54 |
55 | // range_filters: `
56 | // {%- for letter in ('a'|upcase..'z'|upcase) %}
57 | // * {{ letter }} - {{ @index }}
58 | // {%- endfor -%}
59 | // `,
60 |
61 | kv: `
62 | {%- for k, v in pkg -%}
63 | {%- assign val = v | typeof -%}
64 | {%- if val != 'object' %}
65 | - {{k}} = {{v}}
66 | {%- endif %}
67 | {%- endfor -%}
68 | `
69 |
70 | // first: `
71 | // {% for product in products %}
72 | // {% if forloop.first == true %}
73 | // First time through!
74 | // {% else %}
75 | // Not the first time.
76 | // {% endif %}
77 | // {% endfor %}
78 | // `
79 | };
80 |
81 | const locals = {
82 | pkg,
83 | users: [
84 | { username: 'doowb' },
85 | { username: 'jonschlinkert' }
86 | ],
87 |
88 | products: [1, 2, 3],
89 |
90 | page: {
91 | links: {
92 | demo: 'http://www.github.com/copperegg/mongo-scaling-demo',
93 | more: 'http://www.github.com/copperegg/mongo-scaling-more',
94 | deps: {
95 | version: 'v1.0.1'
96 | }
97 | }
98 | }
99 | };
100 |
101 | (async () => {
102 | for (const [key, source] of Object.entries(fixtures)) {
103 | process.stdout.write(` --- ${key}`);
104 | // console.log({ source, locals });
105 | const output = await Dry.Template.render_strict(source, locals) || '';
106 | process.stdout.write(output);
107 | process.stdout.write('\n ---\n');
108 | console.log();
109 | }
110 | })();
111 |
112 |
--------------------------------------------------------------------------------
/examples/tags/ga.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | Dry.Template.register_tag('ga', require('./custom/GoogleAnalytics'));
5 |
6 | const source = `
7 | {% ga "G-2RJ8P0I4GC" %}
8 | {% ga 'G-2RJ8P0I4GC' %}
9 | {% ga \`G-2RJ8P0I4GC\` %}
10 | {% ga google_analytics_id %}
11 | {% ga foo | default: "G-2RJ8P0I4GC" %}
12 | {{ foo | default: "G-2RJ8P0I4GC" }}
13 | `;
14 |
15 | const template = Dry.Template.parse(source);
16 | const output = template.render({ google_analytics_id: 'G-2RJ8P0I4GC' });
17 | console.log(output);
18 |
--------------------------------------------------------------------------------
/examples/tags/if.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | // {% if line_item.grams > 20000 or line_item.weight > 1000 and customer_address.city == 'Ottawa' %}
5 | // You're buying a heavy item, and live in the same city as our store. Choose local pick-up as a shipping option to avoid paying high shipping costs.
6 | // {% else %}
7 | // ...
8 | // {% endif %}
9 | const source = `
10 | {% if linkpost -%}
11 | {%- capture title %}→ {{ post.title}}{% endcapture -%}
12 | {% else %}
13 | {%- capture title %}★ {{ post.title }}{% endcapture -%}
14 | {% endif -%}
15 | {{ title }}
16 | {% if linkpost %}→{% else %}★{% endif %} [{{ post.title }}](#{{ post.id }})
17 | `;
18 |
19 | const template = Dry.Template.parse(source);
20 | const locals = { line_item: { grams: 19000, weight: 1001 }, customer_address: { city: 'Ottawa' } };
21 |
22 | template.render_strict({ linkpost: false, post: { title: 'My Blog', id: 'abc' } })
23 | .then(console.log)
24 | .catch(console.error);
25 |
26 |
--------------------------------------------------------------------------------
/examples/tags/layout.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 |
4 | class FileSystem {
5 | constructor(files) {
6 | this.files = files;
7 | }
8 |
9 | read_template_file(path) {
10 | return this.files[path];
11 | }
12 | }
13 |
14 | const files = {
15 | base: `
16 |
17 |
18 |
19 | {% block head %}
20 | {% block title %}Default Title{% endblock %}
21 | {% endblock %}
22 |
23 |
24 | {% block content %} {% endblock %}
25 | {% block footer %}{% endblock %}
26 |
27 |
28 | `
29 | };
30 |
31 | const source = `
32 | {% extends "base.html" %}
33 |
34 | {% block title %}Home{% endblock %}
35 | {% block head %}
36 | {{ parent() }}
37 |
40 | {% endblock %}
41 | {% block content %}
42 | Index
43 |
44 | Welcome on my awesome homepage.
45 |
46 | {% endblock %}
47 | `;
48 |
49 | const layouts = Dry.Template.layouts = new FileSystem(files);
50 | const template = Dry.Template.parse(source);
51 |
52 | template.render_strict({}, { registers: { layouts } })
53 | .then(console.log)
54 | .catch(console.error);
55 |
--------------------------------------------------------------------------------
/examples/tags/macro/_fields.html:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type = "text", size = 20) %}
2 |
3 | {% endmacro %}
4 |
5 | {% macro textarea(name, text, rows = 5, cols = 40) %}
6 |
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/examples/tags/macro/_fields.liquid:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type = "text", size = 20) %}
2 |
3 | {% endmacro %}
4 |
5 | {% macro textarea(name, value, rows = 10, cols = 40) %}
6 |
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/examples/tags/macro/_macros.liquid:
--------------------------------------------------------------------------------
1 | {% macro one() -%}
2 | This is macro "one"
3 | {% endmacro %}
4 |
5 | {% macro two() -%}
6 | This is macro "two"
7 | {% endmacro %}
8 |
9 | {% macro three() -%}
10 | This is macro "three"
11 | {% endmacro %}
12 |
13 | {% macro four(name="four") -%}
14 | This is macro "{{ name }}"
15 | {% endmacro %}
16 |
17 | {% macro hello(name="friend") -%}
18 | Hello, {{ name }}!
19 | {% endmacro %}
20 |
--------------------------------------------------------------------------------
/examples/tags/macro/_signup.liquid:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type = "text", size = 20) %}
2 |
3 | {% endmacro %}
4 |
5 | {% macro textarea(name, value, rows = 10, cols = 40) %}
6 |
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/examples/tags/macro/_test.liquid:
--------------------------------------------------------------------------------
1 | Test
--------------------------------------------------------------------------------
/examples/tags/macro/from-as.js:
--------------------------------------------------------------------------------
1 |
2 | // const path = require('path');
3 | const Dry = require('../../..');
4 | const { FileSystem: { LocalFileSystem } } = Dry;
5 | const file_system = new LocalFileSystem(__dirname, '_%s.html');
6 |
7 | const source = `
8 | {% from 'fields' import input as input_field, textarea %}
9 |
10 | {{ input_field('password', '', 'password') }}
11 | {{ textarea('comment', 'This is a comment') }}
12 | `;
13 |
14 | const template = Dry.Template.parse(source);
15 | template.render({}, { registers: { file_system } })
16 | .then(console.log)
17 | .catch(console.error);
18 |
19 |
--------------------------------------------------------------------------------
/examples/tags/macro/from.js:
--------------------------------------------------------------------------------
1 |
2 | // const path = require('path');
3 | const Dry = require('../../..');
4 | const { FileSystem: { LocalFileSystem } } = Dry;
5 | const file_system = new LocalFileSystem(__dirname);
6 |
7 | const source = [
8 | '{%- import "signup" as forms -%}',
9 | '{%- from "macros" import hello -%}',
10 | '{%- import "macros" as foo -%}',
11 | '',
12 | '{%- assign bar = foo -%}',
13 | 'Bar: {{ bar.one() }}',
14 | 'Bar: {{ bar.two() }}',
15 | 'Bar: {{ bar.three() }}',
16 | 'Bar: {{ bar.four() }}',
17 | '----',
18 | 'Foo: {{ foo.one() }}',
19 | 'Foo: {{ foo.two() }}',
20 | 'Foo: {{ foo.three() }}',
21 | 'Foo: {{ foo.four() }}',
22 | // '',
23 | '{%- if foo.hello is defined -%}',
24 | ' {{- foo.hello() -}}',
25 | ' {{- foo.hello("Jon") -}}',
26 | ' {%- for i in (1..3) -%}',
27 | ' {{- foo.hello(i) -}}',
28 | ' {%- endfor -%}',
29 | '{%- endif -%}',
30 | '',
31 | '{% if hello -%}',
32 | 'OK',
33 | '{% endif %}',
34 | '',
35 | '{{ forms.input("username") }}
',
36 | '{{ forms.input("password", null, "password") }}
',
37 | '',
38 | ' ',
39 | '',
40 | '{{ forms.textarea("bio") }}
'
41 | ].join('\n');
42 |
43 | const source2 = `
44 | {% from 'forms.html' import input as input_field, textarea %}
45 |
46 | {{ input_field('password', '', 'password') }}
47 | {{ textarea('comment') }}
48 | `;
49 |
50 | const template = Dry.Template.parse(source);
51 | template.render({}, { registers: { file_system } })
52 | .then(console.log)
53 | .catch(console.error);
54 |
55 |
--------------------------------------------------------------------------------
/examples/tags/macro/import.js:
--------------------------------------------------------------------------------
1 |
2 | // const path = require('path');
3 | const Dry = require('../../..');
4 | const { FileSystem: { LocalFileSystem } } = Dry;
5 | const file_system = new LocalFileSystem(__dirname);
6 |
7 | const source = `
8 | {% import "signup" as forms %}
9 |
10 | The above import call imports the forms.html file (which can contain only macros, or a template and some macros), and import the macros as items of the forms local variable.
11 |
12 | The macros can then be called at will in the current template:
13 |
14 | {{ forms.input('username') | trim }}
15 | {{ forms.input('password', null, 'password') | trim }}
16 |
17 |
18 |
19 | {{ forms.textarea('bio') | trim }}
20 | `;
21 |
22 | // const source2 = '{% import "signup" as forms %}{{ forms.input("username") | trim }}
';
23 |
24 | const template = Dry.Template.parse(source);
25 | template.render({}, { registers: { file_system } })
26 | .then(console.log)
27 | .catch(console.error);
28 |
--------------------------------------------------------------------------------
/examples/tags/macro/macro-with-dot-param.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../..');
3 |
4 | const source = `
5 | {%- macro foo(a, b=true, c=foo.bar, d) %}
6 | a: {{a}}
7 | b: {{b}}
8 | c: {{c}}
9 | {% if d %}d: {{d}}{% endif %}
10 | {% endmacro %}
11 |
12 | {%- assign args1 = [undefined, foo.baz, 'gamma'] -%}
13 | {%- assign args = [...args1, "alpha", "beta"] -%}
14 |
15 | {{ foo('doowb', ...args, "whatever") }}
16 | `;
17 |
18 | // {{ foo() }}
19 | // {{ foo("one", false, foo.baz, 'd') }}
20 |
21 | const template = Dry.Template.parse(source);
22 | template.render({ data: { foo: 'one', bar: 'two' }, foo: { bar: 'from context', baz: 'other from context' } })
23 | .then(console.log)
24 | .catch(console.error);
25 |
--------------------------------------------------------------------------------
/examples/tags/macro/macro.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../..');
3 |
4 | const source = `
5 | {{ input('password', '', 'password') }}
6 |
7 | {% macro input(name, value, type = "text", size = 20) %}
8 |
9 | {% endmacro %}
10 |
11 | {% macro foo(a, b=true, c=variable, d) %}
12 | a: {{a}}
13 | b: {{b}}
14 | c: {{c}}
15 | d: {{d}}
16 | {% endmacro %}
17 |
18 | {{ foo() }}
19 |
20 | ---
21 |
22 | {{ foo("one", "
Inside
", undefined, "ddd") }}
23 |
24 | ---
25 |
26 | {{ input("username", "
Inside
", undefined) }}
27 | `;
28 |
29 | const template = Dry.Template.parse(source);
30 | template.render({ data: { foo: 'one', bar: 'two' }, variable: 'from context' })
31 | .then(console.log)
32 | .catch(console.error);
33 |
--------------------------------------------------------------------------------
/examples/tags/markdown.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../..');
3 | const { Remarkable } = require('remarkable');
4 |
5 | class Markdown extends Dry.Tag {
6 | async render(context) {
7 | const node = Dry.Expression.parse(this.match[3]);
8 | const value = await context.evaluate(node);
9 | const md = new Remarkable();
10 | return md.render(value);
11 | }
12 | }
13 |
14 | Dry.Template.register_tag('markdown', Markdown);
15 |
16 | (async () => {
17 | const value = `
18 | # Heading
19 |
20 | > This is markdown!
21 |
22 | Let's see if this works.
23 | `;
24 |
25 | console.log({
26 | expected: '',
27 | actual: await Dry.Template.render_strict('foo {% markdown value %} bar', { value })
28 | });
29 |
30 | })();
31 |
--------------------------------------------------------------------------------
/examples/ten-million-dots.js:
--------------------------------------------------------------------------------
1 | const Template = require('../lib/Template');
2 |
3 | // def test_for_dynamic_find_var
4 | // assert_template_result(' 1 2 3 ', '{%for item in (bar..[key]) %} {{item}} {%endfor%}', 'key' => 'foo', 'foo' => 3, 'bar' => 1)
5 | // end
6 |
7 | // # Regression test for old regex that has backtracking issues
8 | // def test_for_regular_expression_backtracking
9 | // with_error_mode(:strict) do
10 | // assert_raises(Liquid::SyntaxError) do
11 | // Template.parse("{%for item in (1#{'.' * 50000})! %} {{item}} {%endfor%}")
12 | // end
13 | // end
14 | // end
15 |
16 | const generateFixture = () => {
17 | const result = `{% for i in ('${'.'.repeat(10_000_000)}') %}\n{% endfor %}\n`;
18 | return result;
19 | };
20 |
21 | const template = Template.parse(generateFixture());
22 |
23 | template.render()
24 | .then(console.log)
25 | .catch(console.error);
26 |
--------------------------------------------------------------------------------
/examples/variables.js:
--------------------------------------------------------------------------------
1 |
2 | const { render_strict, Template } = require('..');
3 | const pause = (v, ms = 1000) => new Promise(res => setTimeout(() => res(v), ms));
4 | const upper = v => v.toUpperCase();
5 | const append = (a, b) => a + b;
6 |
7 | Template.register_filter('append', append);
8 | Template.register_filter('upper', upper);
9 |
10 | (async () => {
11 | const baz = () => pause('doowb', 10);
12 | // const foo = () => pause({ bar: { baz: 'doowb' } }, 10);
13 | const foo = () => pause({ bar: pause({ baz: 'doowb' }, 10) }, 10);
14 |
15 | // console.log({
16 | // expected: '',
17 | // actual: await render('<{{ foo.bar.baz }}>', { foo: { bar: { baz } } })
18 | // });
19 |
20 | // console.log({
21 | // expected: '',
22 | // actual: await render('<{{ foo.bar.baz }}>', { foo })
23 | // });
24 |
25 | console.log({
26 | expected: '',
27 | actual: await render_strict('<{{ upper(append(foo.bar.baz, "-after")) }}>', { foo })
28 | });
29 |
30 | console.log({
31 | expected: '',
32 | actual: await render_strict('<{{ foo.bar.baz | upper }}>', { foo })
33 | });
34 | })();
35 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('./lib/Dry');
3 |
--------------------------------------------------------------------------------
/lib/Expression.js:
--------------------------------------------------------------------------------
1 | const Dry = require('./Dry');
2 | const { regex, utils, RangeLookup, VariableLookup } = Dry;
3 |
4 | class Expression {
5 | static LITERALS = {
6 | 'nil': null, 'null': null, '': null,
7 | 'true': true,
8 | 'false': false,
9 | 'blank': '',
10 | 'empty': ''
11 | };
12 |
13 | // static RANGES_REGEX = /^\(\s*([-+]?[0-9]+|[a-zA-Z])\s*\.\.\s*([-+]?[0-9]+|[a-zA-Z])\s*\)$/; // (1..10)
14 | static RANGES_REGEX = /^\(\s*(?:(\S+)\s*\.\.)\s*(\S+)\s*\)$/; // (1..10)
15 | // static RANGES_REGEX = /^\s*\(.*\)\s*$/;
16 | static INTEGERS_REGEX = /^([-+]?[0-9]+(?:[eE][-+]?[0-9]+)?|0x[0-9a-fA-F]+)$/;
17 | static FLOATS_REGEX = /^([-+]?[0-9]+(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?)$/;
18 | static SPREAD_REGEX = /^\.{3}([a-zA-Z_]\w*)/;
19 | static QUOTED_STRING = utils.r('m')`^\\s*(${regex.QuotedString.source.slice(1)})\\s*`;
20 |
21 | static parse(markup) {
22 | if (markup == null) return markup;
23 | if (typeof markup === 'number') return markup;
24 | if (typeof markup === 'symbol') return markup;
25 | if (typeof markup === 'string') markup = markup.trim();
26 |
27 | if (hasOwnProperty.call(this.LITERALS, markup)) {
28 | return this.LITERALS[markup];
29 | }
30 |
31 | const exec = regex => (this.match = regex.exec(markup));
32 |
33 | if (exec(this.QUOTED_STRING)) return this.match[1].slice(1, -1);
34 | if (exec(this.INTEGERS_REGEX)) return Number(this.match[1]);
35 | if (exec(this.RANGES_REGEX)) {
36 | const parts = markup.trim().slice(1, -1).split(/\.{2,}/);
37 | if (parts.length === 2) {
38 | return RangeLookup.parse(parts[0], parts[1]);
39 | }
40 | }
41 |
42 | if (exec(this.FLOATS_REGEX)) return Number(this.match[0]);
43 | if (exec(this.SPREAD_REGEX)) {
44 | const output = VariableLookup.parse(this.match[1]);
45 | output.spread = true;
46 | return output;
47 | }
48 |
49 | return VariableLookup.parse(markup);
50 | }
51 |
52 | static isRange(markup) {
53 | return this.RANGES_REGEX.test(markup);
54 | }
55 | }
56 |
57 | module.exports = Expression;
58 |
--------------------------------------------------------------------------------
/lib/I18n.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { default: get } = require('get-value');
5 | const { kLocale } = require('./constants/symbols');
6 | const { isPlainObject } = require('./shared/utils');
7 |
8 | const DEFAULT_LOCALE = path.join(__dirname, 'locales', 'en.yml');
9 |
10 | class I18n {
11 | constructor(path = DEFAULT_LOCALE) {
12 | this.path = path;
13 | }
14 |
15 | translate(key, variables = {}) {
16 | return this.interpolate(this.get(key), variables);
17 | }
18 |
19 | t(...args) {
20 | return this.translate(...args);
21 | }
22 |
23 | interpolate(value, variables = {}) {
24 | if (Array.isArray(value)) {
25 | return value.map(v => this.interpolate(v, variables));
26 | }
27 |
28 | if (isPlainObject(value)) {
29 | for (const key of Object.keys(value)) {
30 | value[key] = this.interpolate(value[key], variables);
31 | }
32 | return value;
33 | }
34 |
35 | if (typeof value === 'string') {
36 | return value.replace(/%\{(\w+)\}/g, (m, $1) => {
37 | if (!variables[$1]) {
38 | throw new Error(`Undefined key ${$1} for interpolation in translation ${value}`);
39 | }
40 | return variables[$1];
41 | });
42 | }
43 | return value;
44 | }
45 |
46 | get(key) {
47 | const value = get(this.locale, key);
48 |
49 | if (!value) {
50 | throw new Error(`Translation for ${key} does not exist in locale ${path}`);
51 | }
52 |
53 | return value;
54 | }
55 |
56 | set locale(value) {
57 | this[kLocale] = null;
58 | this.path = value;
59 | }
60 | get locale() {
61 | if (!this[kLocale]) {
62 | const yaml = require('yaml');
63 | this[kLocale] = yaml.parse(fs.readFileSync(this.path, 'utf8'));
64 | }
65 | return this[kLocale];
66 | }
67 |
68 | static get DEFAULT_LOCALE() {
69 | return DEFAULT_LOCALE;
70 | }
71 | }
72 |
73 | module.exports = I18n;
74 |
--------------------------------------------------------------------------------
/lib/Location.js:
--------------------------------------------------------------------------------
1 |
2 | const { Newline } = require('./constants/regex');
3 |
4 | class Position {
5 | constructor(loc) {
6 | this.index = loc.index;
7 | this.line = loc.line;
8 | this.col = loc.col;
9 | }
10 | }
11 |
12 | class Location {
13 | constructor(start, end) {
14 | this.start = start;
15 | this.end = end;
16 | }
17 |
18 | slice(input) {
19 | return input.slice(...this.range);
20 | }
21 |
22 | get range() {
23 | return [this.start.index, this.end.index];
24 | }
25 |
26 | get lines() {
27 | return [this.start.line, this.end.line];
28 | }
29 |
30 | static get Position() {
31 | return Position;
32 | }
33 |
34 | static get Location() {
35 | return Location;
36 | }
37 |
38 | static updateLocation(loc, value = '', length = value.length) {
39 | const lines = value.split(Newline);
40 | const last = lines[lines.length - 1];
41 | loc.index += length;
42 | loc.col = lines.length > 1 ? last.length : loc.col + length;
43 | loc.line += Math.max(0, lines.length - 1);
44 | }
45 |
46 | static location(loc) {
47 | const start = new Position(loc);
48 |
49 | return node => {
50 | node.loc = new Location(start, new Position(loc));
51 | return node;
52 | };
53 | }
54 | }
55 |
56 | module.exports = Location;
57 |
--------------------------------------------------------------------------------
/lib/ParseTreeVisitor.js:
--------------------------------------------------------------------------------
1 |
2 | class ParseTreeVisitor {
3 | static for(node, callbacks) {
4 | const Visitor = node.constructor.ParseTreeVisitor || this;
5 | const visitor = new Visitor(node, callbacks);
6 | return visitor;
7 | }
8 |
9 | constructor(node, callbacks = new Map()) {
10 | this.node = node;
11 | this.callbacks = callbacks;
12 | }
13 |
14 | add_callback_for(...classes) {
15 | const block = classes.pop();
16 | const callback = node => block(node);
17 | classes.forEach(Node => this.callbacks.set(Node, callback));
18 | return this;
19 | }
20 |
21 | visit(context = null) {
22 | return this.children.map(node => {
23 | const callback = this.callbacks.get(node.constructor);
24 | if (!callback) return [node.name || node];
25 |
26 | const new_context = callback(node, context) || context;
27 | return [
28 | ParseTreeVisitor.for(node, this.callbacks).visit(new_context)
29 | ];
30 | });
31 | }
32 |
33 | get children() {
34 | return this.node.nodes || [];
35 | }
36 | }
37 |
38 | module.exports = ParseTreeVisitor;
39 |
--------------------------------------------------------------------------------
/lib/PartialCache.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('./Dry');
3 |
4 | class PartialCache {
5 | static load_type(type, template_name, { context, state = {} } = {}) {
6 | const registers_key = type === 'partials' ? 'file_system' : type;
7 | const factory_key = type === 'partials' ? 'template_factory' : `${type}_factory`;
8 |
9 | try {
10 | const cached_partials = context.registers[`cached_${type}`] ||= {};
11 | const cached = cached_partials[template_name];
12 | if (cached) return cached;
13 |
14 | const file_system = context.registers[registers_key] ||= Dry.Template[registers_key];
15 | const source = file_system.read_template_file(template_name);
16 |
17 | state.path = template_name;
18 | state.partial = true;
19 |
20 | const template_factory = context.registers[factory_key] ||= new Dry.TemplateFactory();
21 | const template = template_factory.for(template_name);
22 | const partial = template.parse(source, state);
23 |
24 | cached_partials[template_name] = partial;
25 | return partial;
26 | } catch (err) {
27 | if (process.env.DEBUG) console.error(err);
28 | state.partial = false;
29 | }
30 | }
31 |
32 | static load(template_name, { context, state = {} } = {}) {
33 | return this.load_type('partials', template_name, { context, state });
34 | }
35 | }
36 |
37 | module.exports = PartialCache;
38 |
--------------------------------------------------------------------------------
/lib/RangeLookup.js:
--------------------------------------------------------------------------------
1 | const fill = require('fill-range');
2 | const Dry = require('./Dry');
3 | const { isPrimitive } = Dry.utils;
4 |
5 | const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
6 | const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
7 |
8 | const toInteger = value => {
9 | if (typeof value === 'number') {
10 | if (value > MAX_SAFE_INTEGER || value < MIN_SAFE_INTEGER) {
11 | throw new Dry.RangeError('Integer value out of bounds');
12 | }
13 | return value;
14 | }
15 |
16 | if (typeof value === 'bigint') {
17 | if (value > BigInt(MAX_SAFE_INTEGER) || value < BigInt(MIN_SAFE_INTEGER)) {
18 | throw new Dry.RangeError('Integer value out of bounds');
19 | }
20 | return Number(value);
21 | }
22 |
23 | try {
24 | const number = Number(value);
25 | if (isNaN(number)) return 0;
26 | if (number > MAX_SAFE_INTEGER || number < MIN_SAFE_INTEGER) {
27 | throw new Dry.RangeError('Integer value out of bounds');
28 | }
29 | return number;
30 | } catch {
31 | throw new Dry.SyntaxError('invalid integer');
32 | }
33 | };
34 |
35 | class RangeLookup {
36 | constructor(start, end) {
37 | this.type = 'range';
38 | this.set('start', start);
39 | this.set('end', end);
40 | }
41 |
42 | set(key, value) {
43 | const markup = value?.markup || value;
44 |
45 | if (typeof markup === 'string' && markup.includes('|')) {
46 | value = new Dry.Variable(markup);
47 | }
48 |
49 | this[key] = value;
50 | }
51 |
52 | async evaluate(context) {
53 | const start = await context.evaluate(this.start);
54 | const end = await context.evaluate(this.end);
55 |
56 | try {
57 | if (!isPrimitive(start)) throw new Dry.RangeError(`Invalid range start: "${JSON.stringify(start)}"`);
58 | if (!isPrimitive(end)) throw new Dry.RangeError(`Invalid range end: "${JSON.stringify(end)}"`);
59 | } catch (error) {
60 | return context.handle_error(error);
61 | }
62 |
63 | return this.toString(this.toInteger(start), this.toInteger(end));
64 | }
65 |
66 | toInteger(input) {
67 | return toInteger(input);
68 | }
69 |
70 | toString(start = this.start, end = this.end) {
71 | return this.constructor.toString(start, end);
72 | }
73 |
74 | static toString(start = this.start, end = this.end) {
75 | return `(${start}..${end})`;
76 | }
77 |
78 | static parse(startValue, endValue) {
79 | const start = Dry.Expression.parse(startValue);
80 | const end = Dry.Expression.parse(endValue);
81 |
82 | if (start?.evaluate || end?.evaluate) {
83 | return new RangeLookup(start, end);
84 | }
85 |
86 | return fill(start, end);
87 | }
88 | }
89 |
90 | module.exports = RangeLookup;
91 |
--------------------------------------------------------------------------------
/lib/State.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('./Dry');
3 | const { kOptions, kPartial } = require('./constants/symbols');
4 |
5 | class State {
6 | static blocks = {};
7 |
8 | constructor(options = {}, parent = {}) {
9 | this.parent = parent;
10 | this.parent.blocks ||= {};
11 |
12 | this.path = options.path || 'unknown';
13 | this.locale = options.locale || new Dry.I18n();
14 | this.template_options = options;
15 |
16 | this.queue = new Set();
17 | this.error_mode = this.template_options.error_mode || Dry.Template.error_mode;
18 | this.warnings = [];
19 |
20 | this.registry = { blocks: {}, imports: {}, macros: {}, scoped: [] };
21 | this.depth = 0;
22 | this[kPartial] = false;
23 |
24 | return new Proxy(this, {
25 | get(target, key) {
26 | if (target.options && hasOwnProperty.call(target.options, key)) {
27 | return target?.options[key];
28 | }
29 | return target[key];
30 | }
31 | });
32 | }
33 |
34 | new_tokenizer(markup, { start_line_number = null, for_liquid_tag = false }) {
35 | return new Dry.Tokenizer(markup, { line_number: start_line_number, for_liquid_tag });
36 | }
37 |
38 | parse_expression(markup) {
39 | return Dry.Expression.parse(markup);
40 | }
41 |
42 | set_block(name, block) {
43 | if (this.path) {
44 | this.parent.blocks[this.path] ||= {};
45 | this.parent.blocks[this.path][name] ||= block;
46 | } else {
47 | this.parent.blocks[name] = block;
48 | }
49 | return block;
50 | }
51 |
52 | get_block(name) {
53 | return this.blocks[name];
54 | }
55 |
56 | get blocks() {
57 | return this.parent.blocks[this.path] || this.parent.blocks;
58 | }
59 |
60 | get line_number() {
61 | return this.loc && this.loc.line;
62 | }
63 |
64 | set partial(value) {
65 | this[kPartial] = value;
66 | this.options = value ? this.partial_options : this.template_options;
67 | this.error_mode = this.options.error_mode || Dry.Template.error_mode;
68 | }
69 | get partial() {
70 | return this[kPartial];
71 | }
72 |
73 | create_partial_options() {
74 | const dont_pass = this.template_options.include_options_blacklist;
75 |
76 | if (dont_pass === true) {
77 | return { locale: this.locale };
78 | }
79 |
80 | if (Array.isArray(dont_pass)) {
81 | const new_options = {};
82 | for (const key of Object.keys(this.template_options)) {
83 | if (!dont_pass.includes(key)) {
84 | new_options[key] = this.template_options[key];
85 | }
86 | }
87 | return new_options;
88 | }
89 |
90 | return this.template_options;
91 | }
92 |
93 | set partial_options(value) {
94 | this[kOptions] = value;
95 | }
96 | get partial_options() {
97 | return (this[kOptions] ||= this.create_partial_options());
98 | }
99 | }
100 |
101 | module.exports = State;
102 |
--------------------------------------------------------------------------------
/lib/StaticRegisters.js:
--------------------------------------------------------------------------------
1 |
2 | const { hasOwnProperty } = Reflect;
3 | const Dry = require('./Dry');
4 | const { handlers } = require('./shared/utils');
5 |
6 | class StaticRegisters {
7 | constructor(registers = {}) {
8 | this.static = registers.static || registers;
9 | this.registers = {};
10 | return new Proxy(this, handlers);
11 | }
12 |
13 | set(key, value) {
14 | return (this.registers[key] = value);
15 | }
16 |
17 | get(key) {
18 | return hasOwnProperty.call(this.registers, key) ? this.registers[key] : this.static[key];
19 | }
20 |
21 | delete(key) {
22 | const value = this.registers[key];
23 | delete this.registers[key];
24 | return value;
25 | }
26 |
27 | fetch(key, fallback, block) {
28 | if (hasOwnProperty.call(this.registers, key)) return this.registers[key];
29 | if (hasOwnProperty.call(this.static, key)) return this.static[key];
30 | if (fallback === undefined) { throw new Dry.KeyError(key); }
31 | if (block) return typeof block === 'function' ? block.call(this, key, fallback) : block;
32 | return fallback;
33 | }
34 |
35 | key(key) {
36 | return hasOwnProperty.call(this.registers, key) || hasOwnProperty.call(this.static, key);
37 | }
38 | }
39 |
40 | module.exports = StaticRegisters;
41 |
--------------------------------------------------------------------------------
/lib/StrainerFactory.js:
--------------------------------------------------------------------------------
1 |
2 | const StrainerTemplate = require('./StrainerTemplate');
3 |
4 | // StrainerFactory is the factory for the filters system.
5 | class StrainerFactory {
6 | static strainer_class_cache = new Map();
7 | static global_filters = [];
8 |
9 | static create(context, filters = []) {
10 | const Strainer = this.strainer_from_cache(filters);
11 | return new Strainer(context);
12 | }
13 |
14 | static strainer_from_cache(filters) {
15 | let Strainer = this.strainer_class_cache.get(filters);
16 | if (!Strainer) {
17 | Strainer = class extends StrainerTemplate {};
18 | Strainer.filter_methods.clear();
19 | this.global_filters.forEach(f => Strainer.add_filter(f));
20 | filters.forEach(f => Strainer.add_filter(f));
21 | }
22 | this.strainer_class_cache.set(filters, Strainer);
23 | return Strainer;
24 | }
25 |
26 | static add_global_filter(filters) {
27 | this.strainer_class_cache.clear();
28 | this.global_filters.push(filters);
29 | }
30 | }
31 |
32 | module.exports = StrainerFactory;
33 |
--------------------------------------------------------------------------------
/lib/StrainerTemplate.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('./Dry');
3 | const to_s = method => typeof method === 'string' ? method : method.name;
4 | const instance_cache = new Map();
5 |
6 | const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
7 |
8 | /**
9 | * StrainerTemplate is the computed class for the filters system.
10 | * New filters are mixed into the strainer class which is then instantiated
11 | * for each liquid template render run.
12 | *
13 | * The Strainer only allows method calls defined in filters given to it via
14 | * StrainerFactory.add_global_filter, Context#add_filters or Template.register_filter
15 | */
16 |
17 | class StrainerTemplate {
18 | static filter_methods = new Map();
19 |
20 | static add_filter(filters) {
21 | if (Array.isArray(filters)) {
22 | filters.forEach(f => this.add_filter(f));
23 | return;
24 | }
25 |
26 | StrainerTemplate.include(filters);
27 | }
28 |
29 | static include(filters) {
30 | if (typeof filters === 'function') {
31 | throw new Dry.TypeError('wrong argument type "function" (expected an object)');
32 | }
33 |
34 | if (typeof filters.included === 'function') {
35 | const instances = instance_cache.get(filters) || new Set();
36 | instance_cache.set(filters, instances);
37 |
38 | if (instances.has(this)) return;
39 | instances.add(this);
40 | filters.included(this);
41 | }
42 |
43 | for (const [key, filter] of Object.entries(filters)) {
44 | if (key === 'included') continue;
45 | if (key in Object) {
46 | throw new Dry.MethodOverrideError(`Dry error: Filter overrides registered public methods as non public: ${key}`);
47 | }
48 |
49 | StrainerTemplate.filter_methods.set(key, filter);
50 | }
51 | }
52 |
53 | static invokable(method) {
54 | return StrainerTemplate.filter_methods.has(to_s(method));
55 | }
56 |
57 | constructor(context) {
58 | this.context = context;
59 | }
60 |
61 | send(method, ...args) {
62 | const filter = StrainerTemplate.filter_methods.get(method);
63 | const value = filter(...args);
64 | return value && value?.to_liquid ? value.to_liquid() : value;
65 | }
66 |
67 | invoke(method, ...args) {
68 | if (this.constructor.invokable(method)) {
69 | return this.send(method, ...args);
70 | }
71 |
72 | // console.log({ method, args });
73 | const error = new Dry.UndefinedFilter(`undefined filter ${method}`);
74 | if (this.context.strict_filters) {
75 | throw error;
76 | }
77 |
78 | this.context.errors.push(error);
79 | return args[0];
80 | }
81 | }
82 |
83 | module.exports = StrainerTemplate;
84 |
--------------------------------------------------------------------------------
/lib/TemplateFactory.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('./Dry');
3 |
4 | class TemplateFactory {
5 | for(_template_name) {
6 | return new Dry.Template();
7 | }
8 | }
9 |
10 | module.exports = TemplateFactory;
11 |
--------------------------------------------------------------------------------
/lib/Tokenizer.js:
--------------------------------------------------------------------------------
1 |
2 | const { regex: { Newline, TemplateRegex } } = require('./constants');
3 |
4 | class Tokenizer {
5 | constructor(source, state = {}, { line_numbers = false } = {}) {
6 | const { line_number = null, for_liquid_tag = false } = state;
7 | this.loc = { index: 0, line: 0, col: 0 };
8 | this.source = source.toString();
9 | this.line_number = line_number || (line_numbers ? 1 : null);
10 | this.for_liquid_tag = for_liquid_tag;
11 | this.tokens = this.tokenize();
12 | this.index = 0;
13 | }
14 |
15 | eos() {
16 | return this.index > this.tokens.length - 1;
17 | }
18 |
19 | shift() {
20 | const token = this.tokens[this.index];
21 | this.index++;
22 |
23 | if (!token) return;
24 | if (this.line_number) {
25 | this.line_number += this.for_liquid_tag ? 1 : token.split(Newline).length - 1;
26 | }
27 |
28 | return token;
29 | }
30 |
31 | tokenize() {
32 | if (!this.source) return [];
33 | if (this.for_liquid_tag) return this.source.split(Newline);
34 | const tokens = this.source.split(TemplateRegex);
35 |
36 | // remove empty element at the beginning of the array
37 | if (!tokens[0]) tokens.shift();
38 | return tokens;
39 | }
40 | }
41 |
42 | module.exports = Tokenizer;
43 |
--------------------------------------------------------------------------------
/lib/Usage.js:
--------------------------------------------------------------------------------
1 |
2 | class Usage {
3 | static increment(name) {}
4 | }
5 |
6 | module.exports = Usage;
7 |
--------------------------------------------------------------------------------
/lib/constants/characters.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | '.': 'dot',
4 | '|': 'pipe',
5 | ',': 'comma',
6 | ':': 'colon',
7 | '{': 'brace_open',
8 | '}': 'brace_close',
9 | '[': 'bracket_open',
10 | ']': 'bracket_close',
11 | '(': 'paren_open',
12 | ')': 'paren_close'
13 | };
14 |
--------------------------------------------------------------------------------
/lib/constants/index.js:
--------------------------------------------------------------------------------
1 |
2 | const { getOwnPropertyNames } = Object;
3 | const constants = require('export-files')(__dirname);
4 | const Dry = require('../Dry');
5 |
6 | constants.REVERSE_OPERATOR = Object.freeze({
7 | is: '===',
8 | isnt: '!==',
9 | '==': '!=',
10 | '===': '!==',
11 | '!=': '==',
12 | '!==': '==='
13 | });
14 |
15 | constants.PROTECTED_KEYS = new Set(Object
16 | .getOwnPropertyNames(Object)
17 | .concat(['constructor', '__proto__', 'inspect', 'prototype']));
18 |
19 | constants.DROP_KEYS = new Set(getOwnPropertyNames(Dry.Drop.prototype)
20 | .concat('each')
21 | .filter(k => k !== 'to_liquid'));
22 |
23 | module.exports = constants;
24 |
--------------------------------------------------------------------------------
/lib/constants/regex.js:
--------------------------------------------------------------------------------
1 |
2 | const { r } = require('../shared/utils');
3 |
4 | const FilterSeparator = /\|/;
5 | const ArgumentSeparator = ',';
6 | const FilterArgumentSeparator = ':';
7 | const VarStart = '{{';
8 | const WhitespaceControl = '-';
9 | const TagStart = /{%/;
10 | const TagEnd = /%}/;
11 | const VariableSignature = /(?:\((@?[\w-.[\]]+)\)|(@?[\w-.[\]]+))/;
12 | const VariableSegment = /(?:[\w-]|,\s*|\.{2}\/)/;
13 | const VariableStart = /{{/;
14 | const VariableEnd = /}}/;
15 | const VariableIncompleteEnd = /}}?/;
16 | const QuotedString = /^`(?:\\.|[^`])*`|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'/;
17 | const QuotedFragment = r('g')`(?:${QuotedString}|(?:[^\\s,(|'"]|${QuotedString})+)`;
18 | const TagAttributes = r`(\\w+)\\s*:\\s*(${QuotedFragment})`;
19 | const AnyStartingTag = r`${TagStart}|${VariableStart}`;
20 | const PartialTemplateParser = r('m')`${TagStart}[\\s\\S]*?${TagEnd}|${VariableStart}[\\s\\S]*?${VariableIncompleteEnd}`;
21 | const TemplateRegex = r`(${PartialTemplateParser}|${AnyStartingTag})`;
22 | const VariableParser = r('g')`(?:\\[[^\\]]+\\]|!*@?${VariableSegment}+\\??(?:\\([\\s\\S]*?\\))?)`;
23 |
24 | // custom
25 | const TernarySyntax = /(?.*?)\(\s*(?.+?)\s+\?\s+(?.+?)\s+:\s+(?.+?)\s*\)(?.+)?/;
26 | const FiltersSyntax = /(?.*?)(?(^|[^|])\|\s+.+)/;
27 |
28 | const regex = {
29 | Newline: /\r?\n/g,
30 | ArgumentSeparator,
31 | FilterArgumentSeparator,
32 | FilterSeparator,
33 | QuotedFragment,
34 | QuotedString,
35 | TagAttributes,
36 | TagEnd,
37 | TagStart,
38 | TemplateRegex,
39 | VarStart,
40 | VariableEnd,
41 | VariableParser,
42 | VariableSegment,
43 | VariableSignature,
44 | VariableStart,
45 | WhitespaceControl,
46 | FiltersSyntax,
47 | TernarySyntax
48 | };
49 |
50 | module.exports = regex;
51 |
--------------------------------------------------------------------------------
/lib/constants/symbols.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | RAISE_EXCEPTION_LAMBDA: Symbol('RAISE_EXCEPTION_LAMBDA'),
4 | kAssigns: Symbol(':assigns'),
5 | kBlank: Symbol(':blank'),
6 | kErrors: Symbol(':errors'),
7 | kInstanceAssigns: Symbol(':instance_assigns'),
8 | kInput: Symbol(':input'),
9 | kLocale: Symbol(':locale'),
10 | kName: Symbol(':name'),
11 | kParentContext: Symbol(':parent_context'),
12 | kParentTag: Symbol(':parent_tag'),
13 | kPartial: Symbol(':partial'),
14 | kReceiver: Symbol(':receiver'),
15 | kStrainer: Symbol(':strainer'),
16 | kTags: Symbol(':tags'),
17 | kToken: Symbol(':token'),
18 | kWithParent: Symbol(':with_parent')
19 | };
20 |
--------------------------------------------------------------------------------
/lib/drops/Drop.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const cache = new Map();
4 |
5 | /**
6 | * A drop in liquid is a class which allows you to export DOM like things to liquid.
7 | * Methods of drops are callable.
8 | *
9 | * The main use for liquid drops is to implement lazy loaded objects. If you would
10 | * like to make data available to the web designers which you don't want loaded unless
11 | * needed then a drop is a great way to do that.
12 | *
13 | * Example:
14 | *
15 | * class ProductDrop extends Liquid.Drop
16 | * top_sales() {
17 | * return Shop.current.products.find({ all: true, order: 'sales', limit: 10 });
18 | * }
19 | * }
20 | *
21 | * tmpl.render({ product: new ProductDrop() }) // will invoke top_sales query.
22 | *
23 | * Your drop can either implement the methods sans any parameters
24 | * or implement the liquid_method_missing(name) method which is a catch all.
25 | */
26 |
27 | class Drop {
28 | constructor() {
29 | return new Proxy(this, {
30 | get(target, key, receiver) {
31 | return key in target ? target[key] : target.invoke_drop(key);
32 | }
33 | });
34 | }
35 |
36 | // called by liquid to invoke a drop
37 | invoke_drop(method_or_key) {
38 | return this.liquid_method_missing(method_or_key);
39 | }
40 |
41 | liquid_method_missing(method) {
42 | if (this.context && this.context.strict_variables) {
43 | throw new Dry.UndefinedDropMethod(`Undefined drop method: "${method}"`);
44 | }
45 | }
46 |
47 | to_liquid() {
48 | return this;
49 | }
50 |
51 | to_s() {
52 | return this.toString();
53 | }
54 |
55 | toString() {
56 | return this.constructor.name;
57 | }
58 |
59 | // Check for method existence
60 | static invokable(method_name) {
61 | return this.invokable_methods.has(method_name.toString());
62 | }
63 |
64 | static get invokable_methods() {
65 | if (cache.has(this)) return cache.get(this);
66 | const blacklist = ['each', 'map', 'invoke_drop', 'liquid_method_missing', 'constructor', '__proto__', 'prototype', 'toString', 'to_s', 'inspect'];
67 | const methods = Reflect.ownKeys(this.prototype);
68 | const public_instance_methods = methods.filter(m => !blacklist.includes(m));
69 | const whitelist = ['to_liquid'].concat(public_instance_methods);
70 | const invokable_methods = new Set(whitelist.filter(n => typeof n === 'string').sort());
71 | cache.set(this, invokable_methods);
72 | return invokable_methods;
73 | }
74 | }
75 |
76 | module.exports = Drop;
77 |
--------------------------------------------------------------------------------
/lib/drops/ForLoopDrop.js:
--------------------------------------------------------------------------------
1 | const Dry = require('../Dry');
2 |
3 | const kIndex = Symbol(':index');
4 | const kLocked = Symbol(':locked');
5 | const kName = Symbol(':name');
6 | const kVersion = Symbol(':version');
7 |
8 | const MAX_SAFE_INDEX = Number.MAX_SAFE_INTEGER - 1;
9 |
10 | class ForLoopDrop extends Dry.Drop {
11 | constructor(name, length = 0, parentloop) {
12 | super();
13 | this.name = name;
14 | this.length = Math.min(length, MAX_SAFE_INDEX);
15 | this[kIndex] = 0;
16 | this[kLocked] = false;
17 | this[kVersion] = 0;
18 |
19 | if (parentloop) {
20 | this.parentloop = parentloop;
21 | }
22 | }
23 |
24 | get index() {
25 | this.checkModification();
26 | return Math.min(this[kIndex] + 1, MAX_SAFE_INDEX);
27 | }
28 |
29 | get index0() {
30 | this.checkModification();
31 | return Math.min(this[kIndex], MAX_SAFE_INDEX);
32 | }
33 |
34 | get rindex() {
35 | this.checkModification();
36 | return Math.max(0, Math.min(this.length - this[kIndex], MAX_SAFE_INDEX));
37 | }
38 |
39 | get rindex0() {
40 | this.checkModification();
41 | return Math.max(0, Math.min(this.length - this[kIndex] - 1, MAX_SAFE_INDEX));
42 | }
43 |
44 | get first() {
45 | this.checkModification();
46 | return this[kIndex] === 0;
47 | }
48 |
49 | get last() {
50 | this.checkModification();
51 | return this[kIndex] === this.length - 1;
52 | }
53 |
54 | set name(value) {
55 | this[kName] = value;
56 | }
57 |
58 | get name() {
59 | Dry.Usage.increment('forloop_drop_name');
60 | return this[kName];
61 | }
62 |
63 | get parent() {
64 | return this.parentLoop;
65 | }
66 |
67 | increment() {
68 | if (this[kLocked]) {
69 | throw new Dry.Error('Cannot modify ForLoopDrop while iterating');
70 | }
71 |
72 | if (this[kIndex] < MAX_SAFE_INDEX) {
73 | this[kLocked] = true;
74 | try {
75 | this[kIndex]++;
76 | this[kVersion]++;
77 | } finally {
78 | this[kLocked] = false;
79 | }
80 | }
81 | }
82 |
83 | checkModification() {
84 | if (this[kLocked]) {
85 | throw new Dry.Error('Cannot read ForLoopDrop while being modified');
86 | }
87 | }
88 |
89 | static get kIndex() {
90 | return kIndex;
91 | }
92 | }
93 |
94 | module.exports = ForLoopDrop;
95 |
--------------------------------------------------------------------------------
/lib/drops/TableRowLoopDrop.js:
--------------------------------------------------------------------------------
1 | const Dry = require('../Dry');
2 | const Drop = require('./Drop');
3 |
4 | const kIndex = Symbol('index');
5 | const kLocked = Symbol('locked');
6 | const kVersion = Symbol('version');
7 |
8 | const MAX_SAFE_INDEX = Number.MAX_SAFE_INTEGER - 1;
9 |
10 | class TablerowloopDrop extends Drop {
11 | constructor(length, cols) {
12 | super();
13 | this.length = Math.min(length, MAX_SAFE_INDEX);
14 | this.row = 1;
15 | this.col = 1;
16 | this.cols = Math.max(1, Math.min(cols, MAX_SAFE_INDEX));
17 | this[kIndex] = 0;
18 | this[kLocked] = false;
19 | this[kVersion] = 0;
20 | }
21 |
22 | set index(index) {
23 | this.checkModification();
24 | this[kIndex] = Math.min(index, MAX_SAFE_INDEX);
25 | this.recalculatePosition();
26 | }
27 |
28 | get index() {
29 | this.checkModification();
30 | return Math.min(this[kIndex] + 1, MAX_SAFE_INDEX);
31 | }
32 |
33 | get index0() {
34 | this.checkModification();
35 | return Math.min(this[kIndex], MAX_SAFE_INDEX);
36 | }
37 |
38 | get col0() {
39 | this.checkModification();
40 | return Math.max(0, this.col - 1);
41 | }
42 |
43 | get rindex() {
44 | this.checkModification();
45 | return Math.max(0, Math.min(this.length - this[kIndex], MAX_SAFE_INDEX));
46 | }
47 |
48 | get rindex0() {
49 | this.checkModification();
50 | return Math.max(0, Math.min(this.length - this[kIndex] - 1, MAX_SAFE_INDEX));
51 | }
52 |
53 | get first() {
54 | this.checkModification();
55 | return this[kIndex] === 0;
56 | }
57 |
58 | get last() {
59 | this.checkModification();
60 | return this[kIndex] === this.length - 1;
61 | }
62 |
63 | get col_first() {
64 | this.checkModification();
65 | return this.col === 1;
66 | }
67 |
68 | get col_last() {
69 | this.checkModification();
70 | return this.col === this.cols;
71 | }
72 |
73 | recalculatePosition() {
74 | if (this.cols > 0) {
75 | this.row = Math.floor(this[kIndex] / this.cols) + 1;
76 | this.col = (this[kIndex] % this.cols) + 1;
77 | }
78 | }
79 |
80 | increment() {
81 | if (this[kLocked]) {
82 | throw new Dry.Error('Cannot modify TableRowLoopDrop while iterating');
83 | }
84 |
85 | if (this[kIndex] < MAX_SAFE_INDEX) {
86 | this[kLocked] = true;
87 | try {
88 | this[kIndex]++;
89 | this[kVersion]++;
90 | this.recalculatePosition();
91 | } finally {
92 | this[kLocked] = false;
93 | }
94 | }
95 | }
96 |
97 | checkModification() {
98 | if (this[kLocked]) {
99 | throw new Dry.Error('Cannot read TableRowLoopDrop while being modified');
100 | }
101 | }
102 |
103 | setCols(newCols) {
104 | this.checkModification();
105 | this[kLocked] = true;
106 | try {
107 | this.cols = Math.max(1, Math.min(newCols, MAX_SAFE_INDEX));
108 | this.recalculatePosition();
109 | this[kVersion]++;
110 | } finally {
111 | this[kLocked] = false;
112 | }
113 | }
114 | }
115 |
116 | module.exports = TablerowloopDrop;
117 |
--------------------------------------------------------------------------------
/lib/drops/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('export-files')(__dirname);
3 |
--------------------------------------------------------------------------------
/lib/expressions/Lexer.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const Scanner = require('./Scanner');
4 |
5 | const REPLACEMENTS = Object.freeze({ and: '&&', or: '||', is: '===', isnt: '!==' });
6 | const SPECIALS = Object.freeze({
7 | '|': 'pipe',
8 | '.': 'dot',
9 | ':': 'colon',
10 | ',': 'comma',
11 | '=': 'equal',
12 | '[': 'open_square',
13 | ']': 'close_square',
14 | '{': 'open_brace',
15 | '}': 'close_brace',
16 | '(': 'open_round',
17 | ')': 'close_round',
18 | '?': 'question',
19 | '-': 'dash'
20 | });
21 |
22 | const IDENTIFIER = /^(!*)(\.{2}\/)*[a-zA-Z_][\w-]*\??/;
23 | const STRING_LITERAL = /^(?:`(\\.|[^`])*`|"(\\.|[^"])*"|'(\\.|[^'])*')/;
24 | const NUMBER_LITERAL = /^[-+]?[0-9]+(\.[0-9]+)?/;
25 | const SPREAD = /^\.{3}[a-zA-Z_][\w-]*/;
26 | const DOTDOT = /^\.\./;
27 | const WHITESPACE = /^\s+/;
28 | const MATH_OPERATOR = /^[%^&*~+/-]+/;
29 | const COMPARISON_OPERATOR = /^(?:<>|(?:==|!=)=?|[<>]=?|\?\?|\|\||&&|contains(?!\w))/;
30 | const THIS_EXPRESSION = /^\s*(this|\.)\s*$/;
31 |
32 | class Lexer {
33 | constructor(input, node) {
34 | this.scanner = new Scanner(input);
35 | this.node = node;
36 | }
37 |
38 | eos() {
39 | return !this.scanner.remaining;
40 | }
41 |
42 | scan(regex) {
43 | return this.scanner.scan(regex);
44 | }
45 |
46 | tokenize() {
47 | this.output = [];
48 | let tok;
49 | let t;
50 |
51 | while (!this.eos()) {
52 | if ((t = this.scan(THIS_EXPRESSION))) {
53 | this.output.push(['this', 'this']);
54 | continue;
55 | }
56 |
57 | this.scan(WHITESPACE);
58 | if (this.eos()) break;
59 |
60 | if ((t = this.scan(COMPARISON_OPERATOR))) {
61 | tok = ['comparison', REPLACEMENTS[t] || t];
62 | } else if ((t = this.scan(STRING_LITERAL))) {
63 | tok = ['string', t];
64 | } else if ((t = this.scan(NUMBER_LITERAL))) {
65 | tok = ['number', t];
66 | } else if ((t = this.scan(IDENTIFIER))) {
67 | tok = ['id', t];
68 | } else if ((t = this.scan(SPREAD))) {
69 | tok = ['spread', t];
70 | } else if ((t = this.scan(DOTDOT))) {
71 | tok = ['dotdot', t];
72 | } else if ((t = this.scan(MATH_OPERATOR))) {
73 | tok = ['operator', t];
74 | } else {
75 | const c = this.scanner.consume(1);
76 | const s = SPECIALS[c];
77 |
78 | if (s) {
79 | tok = [s, c];
80 | } else {
81 | const v = this.node?.value || this.scanner.input;
82 | throw new Dry.SyntaxError(`Unexpected character ${c} in "${v}"`);
83 | }
84 | }
85 |
86 | this.output.push(tok);
87 | }
88 |
89 | this.output.push(['end_of_string']);
90 | return this.output;
91 | }
92 | }
93 |
94 | module.exports = Lexer;
95 |
--------------------------------------------------------------------------------
/lib/expressions/Scanner.js:
--------------------------------------------------------------------------------
1 |
2 | class Scanner {
3 | constructor(input) {
4 | this.input = input;
5 | this.remaining = this.input;
6 | this.consumed = '';
7 | }
8 |
9 | consume(n = 1) {
10 | const char = this.remaining[0];
11 | this.remaining = this.remaining.slice(1);
12 | this.consumed += char;
13 | return char;
14 | }
15 |
16 | scan(pattern) {
17 | const match = this.remaining && pattern.exec(this.remaining);
18 | if (match) {
19 | const value = match[0];
20 | this.remaining = this.remaining.slice(value.length);
21 | this.consumed += value;
22 | return value;
23 | }
24 | }
25 | }
26 |
27 | module.exports = Scanner;
28 |
--------------------------------------------------------------------------------
/lib/expressions/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | Lexer: require('./Lexer'),
4 | Parser: require('./Parser'),
5 | Scanner: require('./Scanner')
6 | };
7 |
--------------------------------------------------------------------------------
/lib/locales/en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | errors:
3 | argument:
4 | extends: "Argument error in tag 'extends' - Illegal template name"
5 | include: "Argument error in tag 'include' - Illegal template name"
6 | layout: "Argument error in tag 'layout' - Illegal template name"
7 | template: "Argument error in tag '%{type}' - Illegal template name"
8 |
9 | disabled:
10 | tag: "usage is not allowed in this context"
11 |
12 | file_system:
13 | missing: "Cannot locate %{type}: '%{template_name}'"
14 |
15 | layout:
16 | exponential: 'Exponentially recursive layout defined: "%{expression}"'
17 |
18 | syntax:
19 | assign: "Syntax Error in 'assign' - Valid syntax: assign = [source]"
20 | block: "Syntax Error in 'block' - Valid syntax: block "
21 | capture: "Syntax Error in 'capture' - Valid syntax: capture "
22 | case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
23 | case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [or condition2...] %}"
24 | case: "Syntax Error in 'case' - Valid syntax: case "
25 | conditional: "Syntax Error in tag '%{tag_name}' - Valid syntax: %{tag_name} "
26 | cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
27 | for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
28 | for_invalid_in: "For loops require an 'in' clause"
29 | for: "Syntax Error in 'for loop' - Valid syntax: for - in
"
30 | if: "Syntax Error in tag 'if' - Valid syntax: if "
31 | include: "Error in tag 'include' - Valid syntax: include (with|for) [object|collection]"
32 | invalid_delimiter: "'%{tag_name}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
33 | layout: "Error in tag 'layout' - Valid syntax: layout "
34 | macro: "Error in tag 'macro' - Valid syntax: macro (with|for) [object|collection]"
35 | paginate: "Syntax Error in tag 'paginate' - Valid syntax: paginate by number"
36 | render: "Syntax error in tag 'render' - Template name must be a quoted string"
37 | table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row - in
cols=3"
38 | tag_never_closed: "'%{block_name}' tag was never closed"
39 | tag_termination: "Tag '%{token}' was not properly terminated with: %{tag_end}"
40 | tag_unexpected_args: "Syntax Error in '%{tag_name}' - Valid syntax: %{tag_name}"
41 | unexpected_else: "%{block_name} tag does not expect 'else' tag"
42 | unexpected_outer_tag: "Unexpected outer '%{tag_name}' tag"
43 | unknown_tag: "Unknown tag '%{tag_name}'"
44 | variable_termination: "Variable '%{token}' was not properly terminated with: '%{tag_end}'"
45 |
--------------------------------------------------------------------------------
/lib/nodes/BlockBody.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const toString = s => s == null ? '' : String(s);
4 |
5 | class BlockBody extends Dry.BlockNode {
6 | constructor(...args) {
7 | super(...args);
8 | this.blank = true;
9 | this.nodes = [];
10 | }
11 |
12 | async render(context, output = '') {
13 | context.resource_limits.increment_render_score(this.nodes.length);
14 |
15 | for (const node of this.nodes) {
16 | output += toString(await node.render(context));
17 | this.blank &&= node.blank;
18 |
19 | if (context.interrupted()) {
20 | break;
21 | }
22 |
23 | try {
24 | context.resource_limits.increment_write_score(output);
25 | } catch (error) {
26 | return context.handle_error(error);
27 | }
28 | }
29 |
30 | return output;
31 | }
32 | }
33 |
34 | module.exports = BlockBody;
35 |
--------------------------------------------------------------------------------
/lib/nodes/BlockNode.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const render = require('../shared/helpers/render');
4 |
5 | class BlockNode extends Dry.Node {
6 | constructor(node, state, parent) {
7 | super(node, state, parent);
8 | this.nodes = [];
9 | this.blank = true;
10 | }
11 |
12 | push(node) {
13 | Reflect.defineProperty(node, 'parent', { value: this });
14 | this.nodes.push(node);
15 | }
16 |
17 | render_nodes(nodes = [], context, output = '') {
18 | return render.render_nodes(this, this.nodes, context, output);
19 | }
20 |
21 | render_inner(context, output) {
22 | return this.nodes ? this.render_nodes(this.nodes, context, output) : '';
23 | }
24 |
25 | render_to_output_buffer(context, output) {
26 | return this.render_inner(context, output);
27 | }
28 |
29 | render(context, output) {
30 | return this.render_to_output_buffer(context, output);
31 | }
32 | }
33 |
34 | module.exports = BlockNode;
35 |
--------------------------------------------------------------------------------
/lib/nodes/BlockTag.js:
--------------------------------------------------------------------------------
1 | const Dry = require('../Dry');
2 |
3 | const kTagname = Symbol(':tag_name');
4 | const kDelimiter = Symbol(':block_delimiter');
5 |
6 | class BlockTag extends Dry.BlockNode {
7 | static disabled_tags = Dry.Tag.disabled_tags;
8 |
9 | static parse(node, tokenizer, state) {
10 | const tag = new this(node, state);
11 | tag.parse(tokenizer);
12 | return tag;
13 | }
14 |
15 | constructor(node, state, parent) {
16 | super(node, state, parent);
17 | this.type = 'block_tag';
18 | this.blank = true;
19 | this.args = [];
20 |
21 | if (this.name === 'elsif' || this.name === 'else') {
22 | this.else = true;
23 | }
24 |
25 | if (this.name && this.name.startsWith('end')) {
26 | this.end = true;
27 | }
28 | }
29 |
30 | parse() {
31 | this.body = this.new_body();
32 | while (this.parse_body(this.body, this.nodes));
33 | return this.body;
34 | }
35 |
36 | parse_whitespace(node, prev, next) {
37 | if (node && node.trim_left && prev) {
38 | prev.value = prev.value.trimEnd();
39 | }
40 |
41 | if (node && node.trim_right && next) {
42 | next.value = next.value.trimStart();
43 | }
44 | }
45 |
46 | parse_expression(markup) {
47 | const expr = this.state.parse_expression(markup);
48 |
49 | if (this.parent && expr instanceof Dry.VariableLookup) {
50 | Reflect.defineProperty(expr, 'parent', { value: this.parent });
51 | }
52 |
53 | return expr;
54 | }
55 |
56 | new_body() {
57 | return new Dry.BlockBody();
58 | }
59 |
60 | ParseSyntax(markup, regex) {
61 | return Dry.utils.ParseSyntax(this, markup, regex);
62 | }
63 |
64 | raise_syntax_error(key, state = this.state, options) {
65 | return this.constructor.raise_syntax_error(key, state, options);
66 | }
67 |
68 | raise_tag_never_closed(block_name, state) {
69 | return this.constructor.raise_syntax_error(block_name, state);
70 | }
71 |
72 | static raise_syntax_error(key, state, options) {
73 | throw new Dry.SyntaxError(state.locale.t(`errors.syntax.${key}`, options));
74 | }
75 |
76 | static raise_tag_never_closed(block_name, state) {
77 | this.raise_syntax_error('tag_never_closed', state, { block_name });
78 | }
79 |
80 | set tag_name(value) {
81 | this[kTagname] = value;
82 | }
83 | get tag_name() {
84 | return this[kTagname] || this.name;
85 | }
86 |
87 | get block_name() {
88 | return this.tag_name || this.name;
89 | }
90 |
91 | get block_delimiter() {
92 | return (this[kDelimiter] ||= `end${this.block_name}`);
93 | }
94 |
95 | get currentBranch() {
96 | return this.branches[this.branches.length - 1];
97 | }
98 |
99 | get raw() {
100 | return `${this.tag_name || this.name} ${this.value}`;
101 | }
102 |
103 | get tag_end_name() {
104 | return `end${this.name}`;
105 | }
106 | }
107 |
108 | module.exports = BlockTag;
109 |
--------------------------------------------------------------------------------
/lib/nodes/Branch.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | const toString = s => {
5 | if (s !== null && typeof s === 'object') {
6 | return '';
7 | }
8 |
9 | return String(s);
10 | };
11 |
12 | class Branch extends Dry.Node {
13 | constructor(node, state, parent) {
14 | super(node, state, parent);
15 | this.name = node.name;
16 | this.markup = node.match[3];
17 | this.body = [];
18 | }
19 |
20 | push(node) {
21 | this.body.push(node);
22 | }
23 |
24 | evaluate(context) {
25 | if (this.expression instanceof Dry.VariableLookup) {
26 | return context.evaluate(this.expression);
27 | }
28 | return this.condition.evaluate(context);
29 | }
30 |
31 | async render_nodes(nodes = [], context, output = '') {
32 | context.resource_limits.increment_render_score(nodes.length);
33 |
34 | for (const node of nodes) {
35 | output += toString(await node.render(context));
36 | this.blank &&= node.blank;
37 |
38 | // If we get an Interrupt that means the block must stop processing. An
39 | // Interrupt is any command that stops block execution such as {% break %}
40 | // or {% continue %}. These tags may also occur through Block or Include tags.
41 | if (context.interrupted()) break; // might have happened in a for-block
42 |
43 | try {
44 | context.resource_limits.increment_write_score(output);
45 | } catch (error) {
46 | return context.handle_error(error);
47 | }
48 | }
49 |
50 | return output;
51 | }
52 |
53 | async render(context) {
54 | return this.render_nodes(this.body, context);
55 | }
56 |
57 | get nodelist() {
58 | return this.body.map(node => node.value).filter(Boolean);
59 | }
60 | }
61 |
62 | module.exports = Branch;
63 |
--------------------------------------------------------------------------------
/lib/nodes/Close.js:
--------------------------------------------------------------------------------
1 |
2 | const Node = require('./Node');
3 |
4 | class Close extends Node {
5 | constructor(node, state, parent) {
6 | super(node, state, parent);
7 | this.type = 'close';
8 | this.trim_left = node.match[1].includes('-');
9 | this.trim_right = (node.match[4] || node.match[3]).includes('-');
10 | this.blank = true;
11 | }
12 |
13 | render() {
14 | return '';
15 | }
16 | }
17 |
18 | module.exports = Close;
19 |
--------------------------------------------------------------------------------
/lib/nodes/Node.js:
--------------------------------------------------------------------------------
1 |
2 | const { defineProperty } = Reflect;
3 | const { kBlank, kToken } = require('../constants/symbols');
4 |
5 | class Node {
6 | constructor(token = {}, state, parent) {
7 | const { type = 'node', value = '', ...rest } = token;
8 | this.type = type;
9 | this.value = value;
10 | if (token.input) this.input = token.input;
11 | Object.assign(this, rest);
12 | defineProperty(this, kToken, { value: token[kToken], writable: true });
13 | defineProperty(this, 'state', { value: state, writable: true });
14 | defineProperty(this, 'parent', { value: parent, writable: true });
15 | defineProperty(this, 'match', { value: token.match, writable: true });
16 | defineProperty(this, 'loc', { value: token.loc, writable: true });
17 | if (state?.error_mode) this.error_mode = state.error_mode;
18 | if (state?.loc) this.line_number = state.loc.line;
19 | }
20 |
21 | clone() {
22 | return new this.constructor(this, this.state, this.parent);
23 | }
24 |
25 | append(node) {
26 | this.value += node.value;
27 |
28 | if (node.loc && this.loc) {
29 | this.loc.end = node.loc.end;
30 | }
31 |
32 | if (node.match && this.match) {
33 | this.match ||= [''];
34 | this.match[0] += node.match[0];
35 | }
36 |
37 | if (this.parent && this.parent.append) {
38 | this.parent.append(node);
39 | }
40 | }
41 |
42 | render(locals) {
43 | throw new Error(`The "${this.constructor.name}" node does not have a .render() method`);
44 | }
45 |
46 | set blank(value) {
47 | this[kBlank] = value;
48 | }
49 | get blank() {
50 | return this[kBlank];
51 | }
52 |
53 | get index() {
54 | return this.parent ? this.parent.nodes.indexOf(this) : -1;
55 | }
56 |
57 | get siblings() {
58 | return this.parent?.nodes || [];
59 | }
60 |
61 | get first_node() {
62 | return this.nodes && this.nodes[0] || null;
63 | }
64 |
65 | get last_node() {
66 | return this.nodes && this.nodes[this.nodes.length - 1] || null;
67 | }
68 |
69 | get prev() {
70 | if (this.parent) {
71 | const prev = this.parent.nodes[this.index - 1] || this.parent.prev;
72 | return prev !== this ? prev : null;
73 | }
74 | return null;
75 | }
76 |
77 | get next() {
78 | if (this.parent) {
79 | const next = this.parent.nodes[this.index + 1] || this.parent.next;
80 | return next !== this ? next : null;
81 | }
82 | return null;
83 | }
84 | }
85 |
86 | module.exports = Node;
87 |
--------------------------------------------------------------------------------
/lib/nodes/Open.js:
--------------------------------------------------------------------------------
1 |
2 | const Node = require('./Node');
3 |
4 | class Open extends Node {
5 | constructor(node) {
6 | super(node);
7 | this.type = 'open';
8 | this.name ||= this.match[2];
9 | this.markup = this.match[3].trim();
10 | this.trim_left = this.match[1].includes('-');
11 | this.trim_right = this.match[4].includes('-');
12 | this.blank = true;
13 | }
14 |
15 | render() {
16 | return '';
17 | }
18 | }
19 |
20 | module.exports = Open;
21 |
--------------------------------------------------------------------------------
/lib/nodes/Root.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-case-declarations */
2 |
3 | const Dry = require('../Dry');
4 |
5 | class Root extends Dry.BlockTag {
6 | constructor(node, state, parent) {
7 | super(node, state, parent);
8 | this.options = this.state.template_options;
9 | this.type = this.name = 'root';
10 | this.blank = true;
11 | this.depth = 0;
12 | }
13 |
14 | first_node() {
15 | return this.nodes.find(node => node.type !== 'text' || node.value.trim() !== '');
16 | }
17 |
18 | get nodelist() {
19 | return this.nodes;
20 | }
21 |
22 | static get ParseTreeVisitor() {
23 | return ParseTreeVisitor;
24 | }
25 | }
26 |
27 | class ParseTreeVisitor extends Dry.ParseTreeVisitor {
28 | Parent = Root;
29 | get children() {
30 | return this.node.nodes.slice();
31 | }
32 | }
33 |
34 | module.exports = Root;
35 |
--------------------------------------------------------------------------------
/lib/nodes/Tag.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | class Tag extends Dry.Node {
5 | static disabled_tags = new Set();
6 |
7 | static parse(node, tokenizer, state) {
8 | const tag = new this(node, state);
9 | tag.tokenizer = tokenizer;
10 | tag.parse(tokenizer);
11 | return tag;
12 | }
13 |
14 | constructor(node, state, parent) {
15 | super(node, state, parent);
16 | this.type = 'tag';
17 | this.tag_name = node.name;
18 | this.markup = node.value.trim();
19 | this.disabled_tags = Tag.disabled_tags;
20 | this.blank = false;
21 | }
22 |
23 | parse() {}
24 | render() {
25 | return '';
26 | }
27 |
28 | disable_tags(...tag_names) {
29 | for (const tag_name of tag_names.flat()) {
30 | this.disabled_tags.add(tag_name);
31 | }
32 | // prepend(Disabler);
33 | }
34 |
35 | parse_expression(markup) {
36 | const expr = this.state.parse_expression(markup);
37 |
38 | if (expr instanceof Dry.VariableLookup) {
39 | Reflect.defineProperty(expr, 'parent', { value: this });
40 | }
41 |
42 | return expr;
43 | }
44 |
45 | ParseSyntax(markup, regex) {
46 | return Dry.utils.ParseSyntax(this, markup, regex);
47 | }
48 |
49 | raise_syntax_error(key, state = this.state, options) {
50 | return this.constructor.raise_syntax_error(key, state, options, this);
51 | }
52 |
53 | raise_file_system_error(key, options, state = this.state) {
54 | const opts = { ...options, type: 'template' };
55 | throw new Dry.FileSystemError(state.locale.t(`errors.file_system.${key}`, opts));
56 | }
57 |
58 | get loc() {
59 | return this.token.loc;
60 | }
61 |
62 | get raw() {
63 | return `${this.tag_name || this.name} ${this.value}`;
64 | }
65 |
66 | static raise_syntax_error(key, state, options, node) {
67 | const err = new Dry.SyntaxError(state.locale.t(`errors.syntax.${key}`, options));
68 | if (state.line_numbers) err.line_number = node.loc.end.line;
69 | err.message = err.toString();
70 | throw err;
71 | }
72 | }
73 |
74 | module.exports = Tag;
75 |
--------------------------------------------------------------------------------
/lib/nodes/Text.js:
--------------------------------------------------------------------------------
1 |
2 | const Node = require('./Node');
3 |
4 | class Text extends Node {
5 | constructor(node, state, parent) {
6 | super(node, state, parent);
7 | this.type = node.type || 'text';
8 | this.blank = false;
9 | }
10 |
11 | parse() {}
12 |
13 | render(context) {
14 | try {
15 | context.resource_limits.increment_render_score(this.value.length);
16 | } catch (error) {
17 | return context.handle_error(error);
18 | }
19 | return this.value;
20 | }
21 | }
22 |
23 | module.exports = Text;
24 |
--------------------------------------------------------------------------------
/lib/nodes/Token.js:
--------------------------------------------------------------------------------
1 |
2 | const { defineProperty } = Reflect;
3 | const { kToken } = require('../constants/symbols');
4 |
5 | class Token {
6 | constructor(token, prev = token.prev) {
7 | this.type = token.type;
8 | this.value = token.value;
9 | defineProperty(this, kToken, { value: token, writable: true });
10 | defineProperty(this, 'match', { value: token.match, writable: true });
11 | defineProperty(this, 'prev', { value: token.prev, writable: true });
12 | defineProperty(this, 'loc', { value: token.loc, writable: true });
13 | }
14 | }
15 |
16 | module.exports = Token;
17 |
--------------------------------------------------------------------------------
/lib/nodes/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('export-files')(__dirname, undefined, { case: ['name', 'lower'] });
3 |
--------------------------------------------------------------------------------
/lib/profiler/hooks.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Dry => {
3 | const Context = Dry.Context;
4 | const BlockNode = Dry.BlockNode;
5 | const Parser = Dry.Parser;
6 |
7 | const render = Parser.prototype.render;
8 | const render_node = BlockNode.render_node;
9 | const new_isolated_subcontext = Context.prototype.new_isolated_subcontext;
10 |
11 | Dry.Profiler = require('../Profiler');
12 |
13 | BlockNode.render_node = function(context, output, node) {
14 | const { profiler, template_name } = context;
15 | const line_number = node.loc?.end?.line || node.line_number;
16 |
17 | if (profiler && node.type !== 'text' && node.type !== 'literal') {
18 | return profiler.profile_node(template_name, { code: node.raw, line_number }, () => {
19 | return render_node.call(BlockNode, context, output, node);
20 | });
21 | } else {
22 | return render_node.call(BlockNode, context, output, node);
23 | }
24 | };
25 |
26 | Parser.prototype.render = function(context) {
27 | if (context.profiler) {
28 | return context.profiler.profile(context.template_name, this.options, () => {
29 | return render.call(this, context);
30 | });
31 | } else {
32 | return render.call(this, context);
33 | }
34 | };
35 |
36 | Context.prototype.new_isolated_subcontext = function() {
37 | const new_context = new_isolated_subcontext.call(this);
38 | new_context.profiler = this.profiler;
39 | return new_context;
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/lib/shared/hash.js:
--------------------------------------------------------------------------------
1 | function fold(hash, text, max = text.length) {
2 | if (text.length === 0) {
3 | return hash;
4 | }
5 |
6 | for (let i = 0, len = max; i < len; i++) {
7 | const chr = text.charCodeAt(i);
8 | hash = (hash << 5) - hash + chr;
9 | hash |= 0;
10 | }
11 |
12 | return hash < 0 ? hash * -2 : hash;
13 | }
14 |
15 | function foldValue(input, value, key) {
16 | return fold(fold(fold(input, key), String(value)), typeof value);
17 | }
18 |
19 | function hash(value, seed = '', length = 5) {
20 | return foldValue(0, value, seed).toString(16).padStart(length, '0');
21 | }
22 |
23 | module.exports = hash;
24 |
--------------------------------------------------------------------------------
/lib/shared/helpers/content_for.js:
--------------------------------------------------------------------------------
1 |
2 | exports.get_block = (context, block) => {
3 | context.environments[0]['content_for'] ||= {};
4 | context.environments[0]['content_for'][block] ||= [];
5 | return context.environments[0]['content_for'][block];
6 | };
7 |
8 | exports.render = (context, block) => {
9 | return exports.get_block(context, block).join('');
10 | };
11 |
12 | exports.append_to_block = (context, block, content = '') => {
13 | const converter = context.environments[0]['converter'];
14 | const output = converter.convert(content).replace(/\r?\n$/, '');
15 | return exports.get_block(context, block) + output;
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/lib/shared/helpers/create_error_context.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../..');
3 |
4 | const create_error_context = (token, state) => {
5 | const { red } = require('ansi-colors');
6 |
7 | const match = token.match;
8 | const { start, end } = token.loc;
9 |
10 | const remaining = match.input;
11 | const consumed = state.source.slice(0, start.index).toString();
12 |
13 | const line_count = Math.min(5, end.line);
14 | const lines = consumed.split(Dry.regex.Newline).slice(-line_count);
15 | const last_line = lines[lines.length - 1];
16 |
17 | const pipe = '|';
18 | const len = String(end.line).length;
19 | const prefix = n => String(n).padEnd(len);
20 | const output = [];
21 |
22 | for (let i = 0; i < lines.length; i++) {
23 | output.push([prefix(end.line - line_count + i), pipe, lines[i]].join(' '));
24 | }
25 |
26 | const nindex = remaining.indexOf('\n');
27 | const append = nindex > -1 ? remaining.slice(0, Math.min(20, nindex)) : remaining.slice(0, 20);
28 | const tag = `{% ${token.match.slice(1, 3).join(' ').trim()}`;
29 |
30 | output[output.length - 1] += append;
31 | output[output.length - 1] = output[output.length - 1].split(token.value).join(tag + ' ' + red(token.match[3]));
32 | const caret = ' '.repeat(last_line.length + tag.length + 1) + '^';
33 |
34 | output.push([prefix(lines.length), pipe, caret].join(' '));
35 | return output.join('\n');
36 | };
37 |
38 | module.exports = create_error_context;
39 |
--------------------------------------------------------------------------------
/lib/shared/helpers/find_template.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../..');
3 | const { PartialCache, Template, shared: { helpers } } = Dry;
4 |
5 | module.exports = async (type, node, context) => {
6 | // Though we evaluate this here we will only ever parse it as a string literal.
7 | const template_name = await context.evaluate(node.template_name_expr);
8 | if (!template_name) helpers.raise.argument_error(node, 'template', { type });
9 |
10 | const context_variable_name = node.alias_name || template_name.split('/').pop();
11 | let partial = PartialCache.load_type(type, template_name, { context, state: node.state });
12 |
13 | if (!partial) {
14 | partial = context.registers;
15 | const segs = template_name.split('/');
16 |
17 | while (segs.length) {
18 | partial = partial[segs.shift()];
19 | }
20 |
21 | if (partial && typeof partial === 'string') {
22 | partial = context.registers[template_name] = Template.parse(partial);
23 | }
24 | }
25 |
26 | if (!partial && node?.state?.options?.strict_errors) {
27 | node.raise_file_system_error('missing', { template_name, type });
28 | }
29 |
30 | return { context_variable_name, partial, template_name };
31 | };
32 |
--------------------------------------------------------------------------------
/lib/shared/helpers/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('export-files')(__dirname);
3 |
--------------------------------------------------------------------------------
/lib/shared/helpers/methods.js:
--------------------------------------------------------------------------------
1 |
2 | const { empty, size, toArray } = require('../utils');
3 |
4 | exports.empty = context => empty(context);
5 | exports.size = context => size(context);
6 | exports.length = context => size(context);
7 |
8 | exports.first = context => {
9 | if (context !== null && typeof context === 'object') {
10 | return typeof context.first === 'function' ? context.first() : context.first;
11 | }
12 | return toArray(context)[0];
13 | };
14 |
15 | exports.last = context => {
16 | if (context !== null && typeof context === 'object') {
17 | return typeof context.last === 'function' ? context.last() : context.last;
18 | }
19 | const array = toArray(context);
20 | return array[array.length - 1];
21 | };
22 |
--------------------------------------------------------------------------------
/lib/shared/helpers/raise.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../..');
3 |
4 | exports.syntax_error = (node, key, options) => {
5 | throw new Dry.SyntaxError(node.state.locale.t(`errors.syntax.${key}`, options));
6 | };
7 |
8 | exports.argument_error = (node, key, options) => {
9 | throw new Dry.ArgumentError(node.state.locale.t(`errors.argument.${key}`, options));
10 | };
11 |
--------------------------------------------------------------------------------
/lib/shared/helpers/ternary.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../../Dry');
3 | const ConditionalSyntax = /^(?.*?)\s+(?if|unless)\s+(?.+)/;
4 | const TernarySyntax = /^\(?([^\s?]+)\s*\?\s*([^\s:]+)\s*:\s*(.*?)\)?$/;
5 |
6 | exports.isTernary = input => {
7 | return ConditionalSyntax.test(input.trim()) || TernarySyntax.test(input.trim());
8 | };
9 |
10 | exports.parse = async (markup, context) => {
11 | const conditional = ConditionalSyntax.exec(markup.trim());
12 | if (conditional) {
13 | const { prefix = '', condition, expression } = conditional.groups;
14 | const output = prefix.trim() ? Dry.utils.unquote(prefix) : 'true';
15 | const tag = condition === 'unless'
16 | ? Dry.Template.parse(`{% unless ${expression} %}${output}{% endunless %}`)
17 | : Dry.Template.parse(`{% if ${expression} %}${output}{% endif %}`);
18 |
19 | return tag;
20 | }
21 |
22 | const ternary = TernarySyntax.exec(markup.trim());
23 | if (ternary) {
24 | const [, a, b, c] = ternary;
25 | return Dry.Template.parse(`{% if ${a} %}{{ ${b} }}{% else %}{{ ${c} }}{% endif %}`);
26 | }
27 |
28 | return markup;
29 | };
30 |
--------------------------------------------------------------------------------
/lib/shared/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('export-files')(__dirname);
3 |
--------------------------------------------------------------------------------
/lib/shared/parse_with_selected_parser.js:
--------------------------------------------------------------------------------
1 |
2 | const { parse_with_selected_parser } = require('./select-parser');
3 |
4 | module.exports = parse_with_selected_parser;
5 |
--------------------------------------------------------------------------------
/lib/shared/select-parser.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-call */
2 |
3 | const Dry = require('../Dry');
4 |
5 | exports.markup_context = (node, markup = '') => {
6 | return node.markup_context ? node.markup_context(markup) : `in "${markup.trim()}"`;
7 | };
8 |
9 | exports.parse_with_selected_parser = (node, markup) => {
10 | switch (node.error_mode) {
11 | case 'strict': return exports.strict_parse_with_error_context(node, markup);
12 | case 'lax': return node.lax_parse(markup);
13 | case 'warn':
14 | default: {
15 | try {
16 | return exports.strict_parse_with_error_context(node, markup);
17 | } catch (err) {
18 | if (err instanceof Dry.SyntaxError) {
19 | if (node.state?.warnings) node.state.warnings.push(err);
20 | return node.lax_parse.call(node, markup);
21 | }
22 | throw err;
23 | }
24 | }
25 | }
26 | };
27 |
28 | exports.strict_parse_with_error_context = (node, markup) => {
29 | try {
30 | return node.strict_parse(markup);
31 | } catch (err) {
32 | if (err instanceof Dry.SyntaxError) {
33 | err.line_number = node.line_number;
34 | err.markup_context = exports.markup_context(node, markup);
35 | err.message = err.toString(true);
36 | }
37 | throw err;
38 | }
39 | };
40 |
41 | exports.strict_parse_with_error_mode_fallback = (node, markup) => {
42 | try {
43 | return exports.strict_parse_with_error_context(node, markup);
44 | } catch (err) {
45 | if (err instanceof Dry.SyntaxError) {
46 | switch (node.error_mode) {
47 | case 'strict': throw err;
48 | case 'warn': {
49 | if (node.state?.warnings) node.state.warnings.push(err);
50 | break;
51 | }
52 | }
53 | return node.lax_parse.call(node, markup);
54 | }
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/lib/tag/disableable.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Dry => {
3 | Dry.Tag = class extends Dry.Tag {
4 | render_to_output_buffer(context) {
5 | if (context.tag_disabled(this.tag_name)) {
6 | return this.disabled_error(context);
7 | }
8 | return super.render_to_output_buffer(context);
9 | }
10 |
11 | disabled_error(context) {
12 | try {
13 | // raise then rescue the exception so that the Context#exception_renderer can handle it
14 | throw new Dry.DisabledError(`${this.tag_name} ${this.state.locale.t('errors.disabled.tag')}`);
15 | } catch (exc) {
16 | context.handle_error(exc, this.line_number);
17 | }
18 | }
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/lib/tag/disabler.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Dry => {
3 | Dry.Tag = class extends Dry.Tag {
4 | async render_to_output_buffer(context) {
5 | return context.with_disabled_tags(this.constructor.disabled_tags, () => {
6 | return super.render_to_output_buffer(context);
7 | });
8 | }
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/lib/tags/Apply.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * The Apply tag applies filters to a block and renders it in-place.
6 | *
7 | * {% apply upcase | split: '-' %}
8 | * Monkeys!
9 | * {% endapply %}
10 | */
11 |
12 | class Apply extends Dry.BlockTag {
13 | async render(context, output = '') {
14 | this.variable ||= new Dry.Variable(`apply | ${this.match[3]}`, this.state, this);
15 |
16 | await context.stack({ apply: super.render(context) }, async () => {
17 | output = await this.variable.render(context);
18 | });
19 |
20 | return output;
21 | }
22 | }
23 |
24 | module.exports = Apply;
25 |
--------------------------------------------------------------------------------
/lib/tags/Block.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { regex: { QuotedFragment: q }, utils } = Dry;
4 |
5 | class Block extends Dry.BlockTag {
6 | static BlockSyntax = utils.r`(${q}+)(?:\\s+(?:mode=(\\S+)))?(\\s+\\|(.*)$)?`;
7 | static seen = new Set();
8 |
9 | constructor(node, state, parent) {
10 | super(node, state, parent);
11 | this.type = 'block';
12 | }
13 |
14 | push(node) {
15 | super.push(node);
16 |
17 | if (node.type === 'open') {
18 | this.parse();
19 | }
20 | }
21 |
22 | parse() {
23 | this.markup = this.match[3];
24 |
25 | if (this.ParseSyntax(this.markup, Block.BlockSyntax)) {
26 | const template_name = this.last_match[1];
27 | const template_mode = this.last_match[2];
28 |
29 | this.template_name = template_name;
30 | this.template_mode = template_mode;
31 | this.template_name_expr = this.parse_expression(template_name);
32 | this.template_mode_expr = this.parse_expression(template_mode);
33 |
34 | this.set_block(...this.last_match.slice(1));
35 | } else {
36 | this.raise_syntax_error('block');
37 | }
38 | }
39 |
40 | set_block(name, mode, filters = '') {
41 | if (utils.isQuoted(name)) {
42 | this.variable_name = utils.unquote(name);
43 | this.state.set_block(this.variable_name, this);
44 | } else {
45 | this.lazy_set_block(name, mode, filters);
46 | }
47 | }
48 |
49 | lazy_set_block(name, mode, filters) {
50 | const set = async context => {
51 | this.state.queue.delete(set);
52 | const block = this.clone();
53 | block.markup = mode ? `${name} ${filters}`.trim() : this.markup;
54 | const variable = new Dry.nodes.Variable(block, this.state, this);
55 | this.variable_name = await variable.render(context) || name;
56 | this.state.set_block(this.variable_name, this);
57 | };
58 |
59 | this.state.queue.add(set);
60 | }
61 |
62 | async render_block(context) {
63 | const name = await context.evaluate(this.template_name_expr);
64 | const blocks = await context.get('blocks');
65 | const block = blocks[name] || this;
66 |
67 | const mode = await context.evaluate(block.template_mode_expr);
68 | const output = await block.render_inner(context);
69 | const render = () => block !== this ? this.render_inner(context) : '';
70 |
71 | switch (mode) {
72 | case 'append': return await render() + output;
73 | case 'prepend': return output + await render();
74 | case 'replace':
75 | default: {
76 | return output;
77 | }
78 | }
79 | }
80 |
81 | async render(context) {
82 | if (this.rendering) return;
83 | this.rendering = true; // prevent infinite recursion
84 |
85 | // support calling "super()" inside block
86 | const parent = () => this.render_inner(context);
87 | const output = await context.stack({ parent }, () => this.render_block(context));
88 | this.rendering = false;
89 | return output;
90 | }
91 | }
92 |
93 | module.exports = Block;
94 |
--------------------------------------------------------------------------------
/lib/tags/Break.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * The `break` tag is used to break out of a for loop.
6 | *
7 | * == Basic Usage:
8 | * {% for item in collection %}
9 | * {% if item.condition %}
10 | * {% break %}
11 | * {% endif %}
12 | * {% endfor %}
13 | */
14 |
15 | class Break extends Dry.Tag {
16 | static INTERRUPT = new Dry.tags.Interrupts.BreakInterrupt();
17 |
18 | render(context, output = '') {
19 | context.push_interrupt(Break.INTERRUPT);
20 | return output;
21 | }
22 | }
23 |
24 | module.exports = Break;
25 |
--------------------------------------------------------------------------------
/lib/tags/Capture.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | // Capture stores the result of a block into a variable without rendering it inplace.
5 | //
6 | // {% capture heading %}
7 | // Monkeys!
8 | // {% endcapture %}
9 | // ...
10 | // {{ heading }}
11 | //
12 | // Capture is useful for saving content for use later in your template, such as
13 | // in a sidebar or footer.
14 | class Capture extends Dry.BlockTag {
15 | static Syntax = Dry.utils.r`${Dry.regex.VariableSignature}+`;
16 | blank = true;
17 |
18 | push(node) {
19 | super.push(node);
20 | if (node.type === 'close') {
21 | this.markup = this.match[3];
22 | this.parse();
23 | }
24 | }
25 |
26 | parse() {
27 | if (this.ParseSyntax(this.markup, Capture.Syntax)) {
28 | this.to = this.last_match[0];
29 | } else {
30 | this.raise_syntax_error('capture');
31 | }
32 | }
33 |
34 | async render(context) {
35 | await context.resource_limits.with_capture(async () => {
36 | context.set(this.to, await super.render(context));
37 | });
38 |
39 | return '';
40 | }
41 | }
42 |
43 | module.exports = Capture;
44 |
--------------------------------------------------------------------------------
/lib/tags/Comment.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * The Comment tag is used for adding block comments that will
6 | * not be rendered in the output.
7 | *
8 | * {# comment #}
9 | * Monkeys!
10 | * {# endcomment #}
11 | *
12 | * Line comments have the following syntax:
13 | *
14 | * {# this is a "line" comment #}
15 | *
16 | */
17 |
18 | class Comment extends Dry.BlockTag {
19 | blank = true;
20 | render() {
21 | return '';
22 | }
23 | render_to_output_buffer() {
24 | return '';
25 | }
26 | unknown_tag() {}
27 |
28 | static get Line() {
29 | return LineComment;
30 | }
31 | }
32 |
33 | class LineComment extends Dry.Tag {
34 | blank = true;
35 | render() {
36 | return '';
37 | }
38 | render_to_output_buffer() {
39 | return '';
40 | }
41 | unknown_tag() {}
42 | }
43 |
44 | module.exports = Comment;
45 |
--------------------------------------------------------------------------------
/lib/tags/Content.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * The `content` tag is used to render content in a layout.
6 | *
7 | * == Basic Usage:
8 | *
9 | *
10 | *
11 | * {% content %}
12 | *
13 | *
14 | */
15 |
16 | class Content extends Dry.Tag {
17 | async render(context) {
18 | return context.evaluate(this.parse_expression(this.name));
19 | }
20 | }
21 |
22 | module.exports = Content;
23 |
--------------------------------------------------------------------------------
/lib/tags/Continue.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * Continue tag to be used to break out of a for loop.
6 | *
7 | * == Basic Usage:
8 | * {% for item in collection %}
9 | * {% if item.condition %}
10 | * {% continue %}
11 | * {% endif %}
12 | * {% endfor %}
13 | */
14 |
15 | class Continue extends Dry.Tag {
16 | static INTERRUPT = new Dry.tags.Interrupts.ContinueInterrupt();
17 |
18 | render(context, output = '') {
19 | context.push_interrupt(Continue.INTERRUPT);
20 | return output;
21 | }
22 | }
23 |
24 | module.exports = Continue;
25 |
--------------------------------------------------------------------------------
/lib/tags/Cycle.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { regex: { QuotedFragment: q }, utils } = Dry;
4 |
5 | /**
6 | * Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
7 | *
8 | * {% for item in items %}
9 | * {{ item }}
10 | * {% endfor %}
11 | *
12 | * Item one
13 | * Item two
14 | * Item three
15 | * Item four
16 | * Item five
17 | */
18 |
19 | class Cycle extends Dry.Tag {
20 | static SimpleSyntax = utils.r`^${q}+`;
21 | static NamedSyntax = utils.r('m')`^(${q})\\s*\\:\\s*(.*)`;
22 | static StringSyntax = utils.r`\\s*(${q})\\s*`;
23 |
24 | constructor(node, state, parent) {
25 | super(node, state, parent);
26 | this.markup = this.match[3];
27 | this.blank = false;
28 | this.parse(this.markup);
29 | }
30 |
31 | parse(markup) {
32 | if (this.ParseSyntax(markup, Cycle.NamedSyntax)) {
33 | this.variables = this.variables_from_string(this.last_match[2]);
34 | this.name = this.parse_expression(this.last_match[1]);
35 | return;
36 | }
37 |
38 | if (this.ParseSyntax(markup, Cycle.SimpleSyntax)) {
39 | this.variables = this.variables_from_string(markup);
40 | this.name = this.variables.toString();
41 | } else {
42 | this.raise_syntax_error('cycle');
43 | }
44 | }
45 |
46 | async render(context) {
47 | const cycle = context.registers['cycle'] ||= {};
48 | const key = await context.evaluate(this.name);
49 |
50 | let iteration = utils.to_i(cycle[key] || 0);
51 | let output = await context.evaluate(this.variables[iteration]);
52 |
53 | if (Array.isArray(output)) {
54 | output = output.join(' ');
55 | } else if (typeof output !== 'string') {
56 | output = String(output == null ? ' ' : output);
57 | }
58 |
59 | iteration = iteration >= this.variables.length - 1 ? 0 : iteration + 1;
60 | cycle[key] = iteration;
61 | return output;
62 | }
63 |
64 | variables_from_string(markup) {
65 | const variables = markup.split(',').map(variable => {
66 | const match = Cycle.StringSyntax.exec(variable);
67 | return match[1] ? this.parse_expression(match[1]) : null;
68 | });
69 | return variables.filter(v => v != null);
70 | }
71 |
72 | static get ParseTreeVisitor() {
73 | return ParseTreeVisitor;
74 | }
75 | }
76 |
77 | class ParseTreeVisitor extends Dry.ParseTreeVisitor {
78 | get children() {
79 | return Array.from(this.node.variables);
80 | }
81 | }
82 |
83 | module.exports = Cycle;
84 |
--------------------------------------------------------------------------------
/lib/tags/Decrement.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * decrement is used in a place where one needs to insert a counter
6 | * into a template, and needs the counter to survive across
7 | * multiple instantiations of the template.
8 | * NOTE: decrement is a pre-decrement, --i,
9 | * while increment is post: i++.
10 | *
11 | * (To achieve the survival, the application must keep the context)
12 | *
13 | * if the variable does not exist, it is created with value 0.
14 | *
15 | * Hello: {% decrement variable %}
16 | *
17 | * gives you:
18 | *
19 | * Hello: -1
20 | * Hello: -2
21 | * Hello: -3
22 | */
23 |
24 | class Decrement extends Dry.Tag {
25 | render(context) {
26 | this.variable ||= this.match[3];
27 | let value = context.environments[0][this.variable] ||= 0;
28 | value--;
29 | context.environments[0][this.variable] = value;
30 | return String(value);
31 | }
32 | }
33 |
34 | module.exports = Decrement;
35 |
--------------------------------------------------------------------------------
/lib/tags/Echo.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * Echo outputs an expression
6 | *
7 | * {% echo monkey %}
8 | * {% echo user.name %}
9 | *
10 | * This is identical to variable output syntax, like {{ foo }}, but works
11 | * inside {% liquid %} tags. The full syntax is supported, including filters:
12 | *
13 | * {% echo user | link %}
14 | */
15 |
16 | class Echo extends Dry.Tag {
17 | constructor(node, state, parent) {
18 | super(node, state, parent);
19 | node.markup = node.match[3];
20 | this.variable = new Dry.Variable(node, state, this);
21 | }
22 |
23 | render(context) {
24 | return this.variable.render(context);
25 | }
26 |
27 | static get ParseTreeVisitor() {
28 | return class extends Dry.ParseTreeVisitor {
29 | get children() {
30 | return [this.node.variable];
31 | }
32 | };
33 | }
34 | }
35 |
36 | module.exports = Echo;
37 |
--------------------------------------------------------------------------------
/lib/tags/Else.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | class Else extends Dry.Node {
5 | constructor(node, state, parent) {
6 | super(node, state, parent);
7 | this.name = 'else';
8 | this.markup = 'true';
9 | this.trim_left = node.match[1].includes('-');
10 | this.trim_right = node.match[3].includes('-');
11 | }
12 |
13 | async render(context) {
14 | const branch = this.parent.branches.find(b => b.name === 'else');
15 | const output = await Promise.all(branch.body.map(node => node.render(context)));
16 | return output.join('');
17 | }
18 | }
19 |
20 | module.exports = Else;
21 |
--------------------------------------------------------------------------------
/lib/tags/Elsif.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | class Elsif extends Dry.Node {
5 | constructor(node, state, parent) {
6 | super(node, state, parent);
7 | this.name = 'elsif';
8 | this.markup = node.match[3];
9 | this.trim_left = node.match[1].includes('-');
10 | this.trim_right = node.match[4].includes('-');
11 | }
12 | }
13 |
14 | module.exports = Elsif;
15 |
--------------------------------------------------------------------------------
/lib/tags/Embed.js:
--------------------------------------------------------------------------------
1 | const Dry = require('../Dry');
2 |
3 | // The embed tag combines features from includes and extends, allowing you
4 | // to include a template, while also overriding any blocks defined within
5 | // the included template.
6 | //
7 | // {% embed "teasers_skeleton.liquid" %}
8 | // {# These blocks are defined in "teasers_skeleton.liquid" #}
9 | // {# and we override them right here: #}
10 | // {% block left_teaser %}
11 | // Some content for the left teaser box
12 | // {% endblock %}
13 | // {% block right_teaser %}
14 | // Some content for the right teaser box
15 | // {% endblock %}
16 | // {% endembed %}
17 | //
18 | class Embed extends Dry.BlockTag {
19 | async render(context, output = '') {
20 | const template = new Dry.tags.Render(this.nodes[0], this.state, this);
21 |
22 | await context.stack({ blocks: this.state.blocks }, async () => {
23 | output = await template.render(context);
24 | });
25 |
26 | return output;
27 | }
28 | }
29 |
30 | module.exports = Embed;
31 |
--------------------------------------------------------------------------------
/lib/tags/Extends.js:
--------------------------------------------------------------------------------
1 | const Dry = require('../Dry');
2 | const Include = require('./Include');
3 |
4 | // class FileSystem {
5 | // constructor(partials) {
6 | // this.partials = partials;
7 | // }
8 |
9 | // read_template_file(path) {
10 | // return this.partials[path];
11 | // }
12 | // }
13 | // this.partials = Dry.Template.file_system = new FileSystem(files);
14 |
15 | /**
16 | * The "extends" tag allows template inheritance.
17 | *
18 | * {% extends "default.html" %}
19 | *
20 | */
21 |
22 | class Extends extends Include {
23 | async render(context, output = '') {
24 | const extended = new Dry.tags.Render(this, this.state, this);
25 |
26 | await context.stack({ blocks: this.state.blocks }, async () => {
27 | output = await extended.render(context);
28 | });
29 |
30 | context.push_interrupt(new Dry.tags.Interrupts.BreakInterrupt());
31 | return output;
32 | }
33 | }
34 |
35 | module.exports = Extends;
36 |
--------------------------------------------------------------------------------
/lib/tags/From.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { regex, utils } = Dry;
4 | const { QuotedString: q, QuotedFragment: f, VariableSegment: s } = regex;
5 |
6 | /**
7 | * From is similar to `include` but specifically for macros.
8 | *
9 | * {% from "signup" as "form" %}
10 | *
11 | */
12 |
13 | class From extends Dry.BlockTag {
14 | static Syntax = utils.r`(${q}+)\\s+import\\s+(.+)$`;
15 |
16 | constructor(node, state, parent) {
17 | super(node, state, parent);
18 | this.markup = this.match[3];
19 | this.assignments = [];
20 | }
21 |
22 | push(node) {
23 | super.push(node);
24 |
25 | if (node.type === 'open') {
26 | this.markup = node.markup;
27 | this.parse();
28 | }
29 | }
30 |
31 | parse() {
32 | if (this.ParseSyntax(this.markup, From.Syntax)) {
33 | this.template_name = this.last_match[1];
34 | this.assignment_markup = this.last_match[2].trim();
35 | this.template_name_expr = this.parse_expression(this.template_name);
36 | this.parse_assignments(this.assignment_markup);
37 | } else {
38 | this.raise_syntax_error('include');
39 | }
40 | }
41 |
42 | parse_assignments(markup) {
43 | const p = new Dry.expressions.Parser(markup);
44 | while (!p.eos()) {
45 | const name = p.expression();
46 | this.assignments.push({ name, to: p.accept('id', 'as') && p.expression() });
47 | p.accept('comma');
48 | }
49 | p.consume('end_of_string');
50 | }
51 |
52 | render(context, output = '') {
53 | return this.render_to_output_buffer(context, output);
54 | }
55 |
56 | async render_to_output_buffer(context, output = '') {
57 | const node = this.nodes[0];
58 | const imported = new Dry.tags.Render(node, this.state, this);
59 | const { partial } = await imported.find_partial(context);
60 |
61 | if (partial?.options?.registry) {
62 | const registry = partial.options.registry.macros ||= {};
63 | const macros = {};
64 |
65 | for (const { name, to = name } of this.assignments) {
66 | macros[to] = registry[name];
67 | }
68 |
69 | return context.stack(macros, () => this.render_inner(context));
70 | }
71 |
72 | return output;
73 | }
74 | }
75 |
76 | module.exports = From;
77 |
--------------------------------------------------------------------------------
/lib/tags/Ifchanged.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | class IfChanged extends Dry.BlockTag {
5 | async render(context) {
6 | const block_output = await super.render(context);
7 | let output = '';
8 |
9 | if (block_output !== context.registers['ifchanged']) {
10 | context.registers['ifchanged'] = block_output;
11 | output += block_output;
12 | }
13 |
14 | return output;
15 | }
16 | }
17 |
18 | module.exports = IfChanged;
19 |
--------------------------------------------------------------------------------
/lib/tags/Import.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * Import is similar to `include` but specifically for macros.
6 | *
7 | * {% import "signup" as "form" %}
8 | *
9 | */
10 |
11 | class Import extends Dry.Tag {
12 | async render(context, output = '') {
13 | const template = new Dry.tags.Render(this, this.state, this);
14 | const imported = await template.find_partial(context);
15 | context.set(imported.context_variable_name, imported.partial.root.ast.macros);
16 | return '';
17 | }
18 | }
19 |
20 | module.exports = Import;
21 |
--------------------------------------------------------------------------------
/lib/tags/Increment.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | /**
5 | * increment is used in a place where one needs to insert a counter
6 | * into a template, and needs the counter to survive across
7 | * multiple instantiations of the template.
8 | * (To achieve the survival, the application must keep the context)
9 | *
10 | * if (the variable does not exist, it is created with value 0.) {
11 | *
12 | * Hello: {% increment variable %}
13 | *
14 | * gives you:
15 | *
16 | * Hello: 0
17 | * Hello: 1
18 | * Hello: 2
19 | */
20 |
21 | class Increment extends Dry.Tag {
22 | render(context) {
23 | this.variable ||= this.match[3];
24 | const first = context.environments[0];
25 | const value = first[this.variable] ||= 0;
26 | first[this.variable] = value + 1;
27 | return String(value);
28 | }
29 | }
30 |
31 | module.exports = Increment;
32 |
--------------------------------------------------------------------------------
/lib/tags/Interrupts.js:
--------------------------------------------------------------------------------
1 |
2 | // An interrupt is any command that breaks processing of a block (ex: a for loop).
3 | class Interrupt {
4 | constructor(message = null) {
5 | this.message = message || 'interrupt';
6 | }
7 |
8 | static get BreakInterrupt() {
9 | return BreakInterrupt;
10 | }
11 |
12 | static get ContinueInterrupt() {
13 | return ContinueInterrupt;
14 | }
15 | }
16 |
17 | // Interrupt that is thrown whenever a {% break %} is called.
18 | class BreakInterrupt extends Interrupt {}
19 |
20 | // Interrupt that is thrown whenever a {% continue %} is called.
21 | class ContinueInterrupt extends Interrupt {}
22 |
23 | module.exports = Interrupt;
24 |
--------------------------------------------------------------------------------
/lib/tags/Macro.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { regex, utils } = Dry;
4 |
5 | /**
6 | * The Macro tag allows you to define inline functions.
7 | *
8 | * {% macro name(a, b=true, c=variable, d) %}
9 | * a: {{a}}
10 | * b: {{b}}
11 | * c: {{c}}
12 | * d: {{d}}
13 | * {% endmacro %}
14 | *
15 | */
16 |
17 | class Macro extends Dry.BlockTag {
18 | static Syntax = utils.r`(${regex.QuotedFragment}+)\\s*\\((.*?)\\)`;
19 |
20 | push(node) {
21 | super.push(node);
22 |
23 | if (node.type === 'open') {
24 | this.markup = this.match[3];
25 | this.parse();
26 | }
27 | }
28 |
29 | parse() {
30 | if (this.ParseSyntax(this.markup, Macro.Syntax)) {
31 | const template_name = this.last_match[1];
32 | const template_params = this.last_match[2];
33 |
34 | this.template_name = template_name;
35 | this.template_name_expr = this.parse_expression(template_name);
36 | this.params = [];
37 |
38 | this.parse_params(template_params);
39 | this.state.registry.macros[template_name] = this;
40 |
41 | this.path = this.state.path;
42 | this.state.registry.scoped.push(this);
43 |
44 | this.parent.macros ||= {};
45 | this.parent.macros[this.template_name] = this;
46 | } else {
47 | this.raise_syntax_error('macro');
48 | }
49 | }
50 |
51 | parse_params(markup) {
52 | const p = new Dry.expressions.Parser(markup);
53 |
54 | while (!p.eos()) {
55 | const param = this.parse_param(p);
56 | this.params.push(param);
57 | p.accept('comma');
58 | }
59 |
60 | p.consume('end_of_string');
61 | }
62 |
63 | parse_param(p) {
64 | const param = this.parse_expression(p.expression());
65 |
66 | if (p.accept('equal')) {
67 | return { param, fallback: this.parse_expression(p.expression()) };
68 | }
69 |
70 | return { param };
71 | }
72 |
73 | render(context) {
74 | return '';
75 | }
76 |
77 | static get ParseTreeVisitor() {
78 | return ParseTreeVisitor;
79 | }
80 | }
81 |
82 | class ParseTreeVisitor extends Dry.ParseTreeVisitor {
83 | Parent = Macro;
84 | get children() {
85 | return this.node.nodes;
86 | }
87 | }
88 |
89 | module.exports = Macro;
90 |
91 |
--------------------------------------------------------------------------------
/lib/tags/Raw.js:
--------------------------------------------------------------------------------
1 | /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "_.*" }]*/
2 |
3 | const Dry = require('../Dry');
4 | const { regex, utils } = Dry;
5 | const { TagStart, TagEnd } = regex;
6 |
7 | /**
8 | * The `raw` tag preserves sections as raw text that will not be evaluated.
9 | *
10 | * {% raw %}
11 | *
12 | * {% for item in seq %}
13 | * {{ item }}
14 | * {% endfor %}
15 | *
16 | * {% endraw %}
17 | */
18 |
19 | class Raw extends Dry.Tag {
20 | static OnlyWhitespace = /^\s*$/;
21 | static FullTokenPossiblyInvalid = utils.r`^([\\s\\S]*?)${TagStart}-?\\s*(\\w+)\\s*(.*)?-?${TagEnd}$`;
22 |
23 | constructor(node, state, parent) {
24 | super(node, state, parent);
25 | this.name = 'raw';
26 | this.blank = false;
27 | }
28 |
29 | parse(tokenizer) {
30 | this.body = '';
31 |
32 | while (!tokenizer.eos()) {
33 | const token = tokenizer.shift();
34 | const match = Raw.FullTokenPossiblyInvalid.exec(token);
35 | if (match && this.block_delimiter === match[2]) {
36 | if (match[1] !== '') this.body += match[1];
37 | return;
38 | }
39 | if (token) this.body += token;
40 | }
41 |
42 | this.raise_tag_never_closed(this.block_name);
43 | }
44 |
45 | parse_whitespace() {
46 | const [markup, _open, tol, tor, body, _close, tcl, tcr] = this.match;
47 | this.markup = markup;
48 | this.body = body;
49 |
50 | this.trim_open_left = tol === '-';
51 | this.trim_open_right = tor === '-';
52 |
53 | this.trim_close_left = tcl === '-';
54 | this.trim_close_right = tcr === '-';
55 |
56 | if (this.match.length < 8) {
57 | this.ensure_valid_markup();
58 | }
59 |
60 | this.trim_whitespace(true);
61 | }
62 |
63 | render() {
64 | this.trim_whitespace();
65 | return this.body;
66 | }
67 |
68 | trim_whitespace(parse = false) {
69 | const { next, prev, state } = this;
70 |
71 | if (!parse) {
72 | if (this.trim_close_right && next) next.value = next.value.trimStart();
73 | if (this.trim_close_left) this.body = this.body.trimEnd();
74 | if (this.trim_open_right) this.body = this.body.trimStart();
75 | } else if (this.trim_open_left && prev) {
76 | const first_byte = prev.value[0];
77 | prev.value = prev.value.trimEnd();
78 |
79 | if (state.template_options.bug_compatible_whitespace_trimming && prev.value === '') {
80 | prev.value += first_byte;
81 | }
82 | }
83 | }
84 |
85 | ensure_valid_markup() {
86 | const ensure_open = () => {
87 | if (!/{%-?\s*raw\s*-?%}/.test(this.value)) {
88 | this.raise_syntax_error('tag_unexpected_args', this.state, { tag_name: this.name });
89 | }
90 | };
91 |
92 | if (!/{%-?\s*endraw\s*-?%}/.test(this.value)) {
93 | ensure_open();
94 | this.raise_syntax_error('tag_never_closed', this.state, { block_name: this.name });
95 | }
96 |
97 | if (Raw.OnlyWhitespace.test(this.body)) {
98 | ensure_open();
99 | this.raise_syntax_error('tag_unexpected_args', this.state, { tag_name: this.name });
100 | }
101 | }
102 | }
103 |
104 | module.exports = Raw;
105 |
--------------------------------------------------------------------------------
/lib/tags/Set.js:
--------------------------------------------------------------------------------
1 |
2 | const Assign = require('./Assign');
3 |
4 | /**
5 | * Alias for the `assign` tag, for compatibility with `twig` and `jinja`.
6 | *
7 | * {% set foo = 'monkey' %}
8 | *
9 | * You can then use the variable later in the page.
10 | *
11 | * {{ foo }}
12 | */
13 |
14 | class Set extends Assign {}
15 |
16 | module.exports = Set;
17 |
--------------------------------------------------------------------------------
/lib/tags/Switch.js:
--------------------------------------------------------------------------------
1 |
2 | const Case = require('./Case');
3 |
4 | /**
5 | * The `switch` tag works like a switch statement, and the `case` tag
6 | * is used for comparing values.
7 | *
8 | * == Basic Usage:
9 | * {% switch handle %}
10 | * {% case 'cake' %}
11 | * This is a cake
12 | * {% case 'cookie' %}
13 | * This is a cookie
14 | * {% else %}
15 | * This is not a cake nor a cookie
16 | * {% endswitch %}
17 | */
18 |
19 | class Switch extends Case {}
20 |
21 | module.exports = Switch;
22 |
--------------------------------------------------------------------------------
/lib/tags/TableRow.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { regex, utils } = Dry;
4 | const { TagAttributes, QuotedFragment: q, VariableSegment: v } = regex;
5 |
6 | class TableRow extends Dry.BlockTag {
7 | static Syntax = utils.r`(${v}+)\\s+(in|of)\\s+(\\([\\s\\S]*?\\)|${q}+)\\s*(reversed)?`;
8 |
9 | constructor(node, state, parent) {
10 | super(node, state, parent);
11 | this.markup = this.match[3];
12 |
13 | if (this.ParseSyntax(this.markup, TableRow.Syntax)) {
14 | this.variable_name = this.last_match[1];
15 | this.collection_name = this.parse_expression(this.last_match[3]);
16 | this.attributes = {};
17 | utils.scan(this.markup, TagAttributes, (m, key, value) => {
18 | this.attributes[key] = this.parse_expression(value);
19 | });
20 | } else {
21 | this.raise_syntax_error('table_row');
22 | }
23 | }
24 |
25 | async render(context) {
26 | let collection = await context.evaluate(this.collection_name);
27 | if (collection == null) return '';
28 |
29 | const attribs = this.attributes;
30 | const { offset, limit } = attribs;
31 |
32 | const from = !utils.isNil(offset) ? utils.to_i(await context.evaluate(offset)) : 0;
33 | const to = !utils.isNil(limit) ? utils.to_i(from + await context.evaluate(limit)) : null;
34 |
35 | collection = utils.slice_collection(collection, from, to);
36 |
37 | const length = collection.length;
38 | const cols = utils.to_i(await context.evaluate(attribs.cols));
39 |
40 | let output = '\n';
41 |
42 | await context.stack({}, async () => {
43 | const tablerowloop = new Dry.drops.TableRowLoopDrop(length, cols);
44 | context.set('tablerowloop', tablerowloop);
45 |
46 | for (const item of collection) {
47 | context.set(this.variable_name, item);
48 | output += ``;
49 | output += await super.render(context);
50 | output += ' ';
51 |
52 | if (tablerowloop.col_last && !tablerowloop.last) {
53 | output += ` \n`;
54 | }
55 | tablerowloop.increment();
56 | }
57 | });
58 |
59 | output += ' \n';
60 | return output;
61 | }
62 |
63 | static get ParseTreeVisitor() {
64 | return ParseTreeVisitor;
65 | }
66 | }
67 |
68 | class ParseTreeVisitor extends Dry.ParseTreeVisitor {
69 | get children() {
70 | return super.children.concat(Object.values(this.node.attributes), [this.node.collection_name]);
71 | }
72 | }
73 |
74 | module.exports = TableRow;
75 |
--------------------------------------------------------------------------------
/lib/tags/Unless.js:
--------------------------------------------------------------------------------
1 |
2 | const If = require('./If');
3 |
4 | class Unless extends If {
5 | async evaluate_branch(branch, context) {
6 | return !await super.evaluate_branch(branch, context);
7 | }
8 | }
9 |
10 | module.exports = Unless;
11 |
--------------------------------------------------------------------------------
/lib/tags/Verbatim.js:
--------------------------------------------------------------------------------
1 |
2 | const Raw = require('./Raw');
3 |
4 | /**
5 | * The `verbatim` tag is an alias for `raw`, which preserves sections as raw text,
6 | * that will not be evaluated.
7 | *
8 | * {% verbatim %}
9 | *
10 | * {% for item in seq %}
11 | * {{ item }}
12 | * {% endfor %}
13 | *
14 | * {% endverbatim %}
15 | */
16 |
17 | class Verbatim extends Raw {}
18 |
19 | module.exports = Verbatim;
20 |
--------------------------------------------------------------------------------
/lib/tags/When.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 |
4 | class When extends Dry.Tag {
5 | constructor(node, state, parent) {
6 | super(node, state, parent);
7 | this.name = 'when';
8 | this.value = this.match[0];
9 | this.markup = this.match[3];
10 | this.body = [];
11 |
12 | if (this.markup === '') {
13 | this.raise_syntax_error('case_invalid_when');
14 | }
15 | }
16 |
17 | async evaluate(context) {
18 | return this.condition && await this.condition.evaluate(context);
19 | }
20 | }
21 |
22 | module.exports = When;
23 |
--------------------------------------------------------------------------------
/lib/tags/With.js:
--------------------------------------------------------------------------------
1 |
2 | const Dry = require('../Dry');
3 | const { constants } = Dry;
4 |
5 | // {% with %}
6 | // {% assign foo = 42 %}
7 | // {{ foo }} {% comment %}foo is 42 here{% endcomment %}
8 | // {% endwith %}
9 | class With extends Dry.BlockTag {
10 | constructor(node, state, parent) {
11 | super(node, state, parent);
12 | this.markup = this.match[3].trim();
13 | this.only = this.markup.endsWith(' only');
14 | this.parse_variable_name();
15 | }
16 |
17 | parse_variable_name() {
18 | if (this.only) this.markup = this.markup.slice(0, -5);
19 |
20 | // const segs = this.markup.split(/(?<=\.\.)\//);
21 | // const props = segs.map(v => v === '.' ? 'this' : v === '..' ? '@parent' : v);
22 | // let key = props.shift();
23 |
24 | // while (props.length) {
25 | // key += `["${props.shift()}"]`;
26 | // }
27 |
28 | this.variable_name = this.markup;
29 | this.variable_name_expr = this.parse_expression(this.markup);
30 | }
31 |
32 | async evaluate(context) {
33 | const result = await context.get(await context.evaluate(this.variable_name));
34 |
35 | if (result === undefined) {
36 | try {
37 | return JSON.parse(this.variable_name);
38 | } catch (err) {
39 | // do nothing
40 | }
41 | }
42 |
43 | return result;
44 | }
45 |
46 | async render(context) {
47 | const data = await this.evaluate(context);
48 | const scope = this.only ? context.new_isolated_subcontext() : context;
49 | const last = context.scopes[context.scopes.length - 1];
50 |
51 | if (this.variable_name === '.' && context.allow_this_variable) {
52 | scope.merge(last);
53 | }
54 |
55 | scope.set(constants.symbols.kWithParent, [last]);
56 | scope.set('this_value', data);
57 |
58 | scope.inside_with_scope ||= 0;
59 | scope.inside_with_scope++;
60 | const output = await scope.stack(data, () => super.render(scope));
61 | scope.inside_with_scope--;
62 | return output;
63 | }
64 | }
65 |
66 | module.exports = With;
67 |
--------------------------------------------------------------------------------
/lib/tags/index.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('export-files')(__dirname, undefined, { case: ['name', 'lower'] });
3 |
--------------------------------------------------------------------------------
/lib/version.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('../package').version;
3 |
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dry",
3 | "description": "Dry is superset of the Liquid templating language, with first-class support for advanced inheritance features, and more.",
4 | "version": "2.0.0",
5 | "license": "MIT",
6 | "repository": "jonschlinkert/dry",
7 | "homepage": "https://github.com/jonschlinkert/dry",
8 | "author": "Jon Schlinkert (https://github.com/jonschlinkert)",
9 | "bugs": {
10 | "url": "https://github.com/jonschlinkert/dry/issues"
11 | },
12 | "main": "index.js",
13 | "files": [
14 | "index.js",
15 | "lib"
16 | ],
17 | "engines": {
18 | "node": ">=16"
19 | },
20 | "scripts": {
21 | "test": "mocha --recursive --ignore 'test/fixtures/**'",
22 | "test:ci": "nyc --reporter=lcov npm run test",
23 | "cover": "nyc --reporter=text --reporter=html npm run test"
24 | },
25 | "dependencies": {
26 | "ansi-colors": "^4.1.3",
27 | "expand-value": "^1.0.0",
28 | "export-files": "^3.0.2",
29 | "fill-range": "^7.1.1",
30 | "get-value": "^4.0.1",
31 | "hash-sum": "^2.0.0",
32 | "is-number": "^7.0.0",
33 | "kind-of": "^6.0.3",
34 | "set-value": "^4.1.0",
35 | "yaml": "^2.7.0"
36 | },
37 | "devDependencies": {
38 | "@babel/eslint-parser": "^7.26.8",
39 | "@babel/eslint-plugin": "^7.25.9",
40 | "@folder/readdir": "^3.1.0",
41 | "@types/jest": "^29.5.14",
42 | "@types/node": "^22.13.2",
43 | "@typescript-eslint/eslint-plugin": "^8.24.0",
44 | "@typescript-eslint/parser": "^8.24.0",
45 | "eslint": "8.57.1",
46 | "gulp-format-md": "^2.0.0",
47 | "handlebars": "^4.7.8",
48 | "mocha": "^11.1.0",
49 | "nyc": "^17.1.0",
50 | "parser-front-matter": "^1.6.4",
51 | "prettier": "^3.5.1",
52 | "remarkable": "^2.0.1",
53 | "time-require": "github:jonschlinkert/time-require",
54 | "vinyl": "^3.0.0"
55 | },
56 | "verb": {
57 | "toc": false,
58 | "layout": "default",
59 | "tasks": [
60 | "readme"
61 | ],
62 | "plugins": [
63 | "gulp-format-md"
64 | ],
65 | "lint": {
66 | "reflinks": true
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'none',
3 | tabWidth: 2,
4 | semi: true,
5 | singleQuote: true,
6 | printWidth: 90,
7 | arrowParens: 'avoid',
8 | bracketSpacing: true,
9 | jsxBracketSameLine: false,
10 | jsxSingleQuote: false,
11 | quoteProps: 'consistent'
12 | };
13 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "../.eslintrc.js"
4 | ],
5 | "env": {
6 | "mocha": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/expected/accessors.html:
--------------------------------------------------------------------------------
1 | a: "accessors.html"
2 |
3 | a.b: "accessors.html"
4 |
5 | c: "accessors.html"
6 |
--------------------------------------------------------------------------------
/test/expected/append-block-multiple.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple.html"
2 |
3 | header: "blocks/multiple.html"
4 | header: "append-block-multiple.html"
5 |
6 | body: "blocks/multiple.html"
7 | body: "append-block-multiple.html"
8 |
9 | footer: "blocks/multiple.html"
10 | footer: "append-block-multiple.html"
11 |
12 | foot: "blocks/multiple.html"
13 |
--------------------------------------------------------------------------------
/test/expected/append-block.html:
--------------------------------------------------------------------------------
1 | unique: "blocks/basic.html"
2 |
3 | header: "blocks/basic.html"
4 | header: "append-block.html"
5 |
--------------------------------------------------------------------------------
/test/expected/block-body.html:
--------------------------------------------------------------------------------
1 | √ header: default
2 |
3 | √ body: block-body.html
4 |
5 | √ footer: default
6 |
--------------------------------------------------------------------------------
/test/expected/block-file-extends.html:
--------------------------------------------------------------------------------
1 | unique: "blocks/basic.html"
2 |
3 | header: "block.html"
4 |
--------------------------------------------------------------------------------
/test/expected/block-indent.html:
--------------------------------------------------------------------------------
1 | unique: parent
2 |
3 |
4 | header: child
5 |
6 |
7 | middle: child
8 |
9 |
10 | footer: child
11 |
12 |
13 | foot: parent
14 |
15 |
--------------------------------------------------------------------------------
/test/expected/block-multiple.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple.html"
2 |
3 | header: "block-multiple.html"
4 |
5 | body: "block-multiple.html"
6 |
7 | footer: "block-multiple.html"
8 |
9 | foot: "blocks/multiple.html"
10 |
--------------------------------------------------------------------------------
/test/expected/block.html:
--------------------------------------------------------------------------------
1 | unique: "blocks/basic.html"
2 |
3 | header: "block.html"
4 |
--------------------------------------------------------------------------------
/test/expected/blocks-missing.html:
--------------------------------------------------------------------------------
1 | one: "block-missing.html"
2 |
3 | two: "block-missing.html"
4 |
--------------------------------------------------------------------------------
/test/expected/body-tag.html:
--------------------------------------------------------------------------------
1 | √ header: default
2 |
3 | √ this is content
4 |
5 | √ footer: default
6 |
--------------------------------------------------------------------------------
/test/expected/filter.html:
--------------------------------------------------------------------------------
1 | {{title | upper}}
2 |
--------------------------------------------------------------------------------
/test/expected/helpers-extends-args.html:
--------------------------------------------------------------------------------
1 | √ one:parent
2 |
3 | √ two:child
4 |
5 |
6 | √ three.four:child
7 |
8 |
--------------------------------------------------------------------------------
/test/expected/helpers-extends.html:
--------------------------------------------------------------------------------
1 | √ one:parent
2 |
3 | √ two:child
4 |
5 |
6 | √ three.four:child
7 |
8 |
--------------------------------------------------------------------------------
/test/expected/helpers.html:
--------------------------------------------------------------------------------
1 | bar: "helpers.html"
2 |
--------------------------------------------------------------------------------
/test/expected/layout-block-and-text-node.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Body fixture
7 |
8 |
9 |
10 |
11 | This is root-text-node content from "fixtures/layout.html".
12 |
13 |
14 | footer: "fixtures/layout.html"
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/expected/layout-block-outside-body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | head: "layout-block-outside-body.html"
5 |
6 |
7 |
8 | This is root-text-node content from "layout-block-outside-body.html".
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/expected/layout-block.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Body fixture
7 |
8 |
9 |
10 |
11 |
12 |
13 | footer: "fixtures/layout.html"
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/expected/layout-file-property.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Body fixture
7 |
8 |
9 |
10 | This is root-text-node content from "layout-file-property.html".
11 |
12 |
13 | footer: "layout-file-property.html"
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/expected/layout-tag-nested.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | parent: I should be rendered
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Body fixture
13 |
14 |
15 |
16 | This is root-text-node content from "fixtures/layout-nested.html".
17 |
18 | footer: "fixtures/layout-nested.html"
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/expected/layout-tag-replace.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | head: "fixtures/layout-replace.html"
5 |
6 |
7 |
8 | This is root-text-node content from "fixtures/layout-replace.html".
9 |
10 |
11 | footer: "fixtures/layout-replace.html"
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/expected/layout-text-node.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Body fixture
7 |
8 |
9 |
10 |
11 | This is root-text-node content from "fixtures/layout.html".
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/expected/merge-blocks.html:
--------------------------------------------------------------------------------
1 | a: child
2 |
3 | b: child inside
4 |
5 |
6 | c: child
7 |
8 | e: child
9 |
10 | e.f: child outside
11 |
12 | g: child
13 | g: default
14 |
15 | g.h: child outside
16 |
17 | i: child
18 | i: default
19 |
20 | j: default inside block
21 |
22 |
--------------------------------------------------------------------------------
/test/expected/mixed-multiple.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple-mixed.html"
2 |
3 | one: "pages/multiple.html"
4 |
5 | two: "pages/multiple.html"
6 |
7 | three: "blocks/multiple-mixed.html"
8 | three: "pages/multiple.html"
9 |
10 | four: "pages/multiple.html"
11 | four: "blocks/multiple-mixed.html"
12 |
13 | five: "blocks/multiple-mixed.html"
14 |
15 | six: "blocks/multiple-mixed.html"
16 | six: "pages/multiple.html"
17 |
18 | foot: "blocks/multiple-mixed.html"
19 |
--------------------------------------------------------------------------------
/test/expected/nested-blocks-1.html:
--------------------------------------------------------------------------------
1 | a: "nested-blocks-1.html"
2 |
--------------------------------------------------------------------------------
/test/expected/nested-blocks-append-repeat.html:
--------------------------------------------------------------------------------
1 | a: default first
2 |
3 | a.b: "nested-blocks-append-repeat.html"
4 | a: "nested-blocks-append-repeat.html"
5 |
6 | a.b: "nested-blocks-append-repeat.html"
7 |
8 | a.b: "nested-blocks-append-repeat.html"
9 |
10 | a: default second
11 | a: "nested-blocks-append-repeat.html"
12 |
13 | a: default third
14 | a: "nested-blocks-append-repeat.html"
15 |
16 |
17 | a.b: "nested-blocks-append-repeat.html"
18 | a: "nested-blocks-append-repeat.html"
19 |
--------------------------------------------------------------------------------
/test/expected/nested-blocks-append.html:
--------------------------------------------------------------------------------
1 | a: default
2 |
3 | a.b: "nested-blocks-append.html"
4 | a: "nested-blocks-append.html"
5 |
--------------------------------------------------------------------------------
/test/expected/nested-blocks-prepend.html:
--------------------------------------------------------------------------------
1 | a: "nested-blocks-prepend.html"
2 | a: default
3 |
4 | a.b: "nested-blocks-prepend.html"
5 |
--------------------------------------------------------------------------------
/test/expected/nested-blocks.html:
--------------------------------------------------------------------------------
1 | before `a.b`: "nested.blocks.html"
2 | a.b: "nested.blocks.html"
3 | after `a.b`: "nested.blocks.html"
4 |
5 | a.b: "nested.blocks.html"
6 |
7 | b: default
8 |
9 | c: default before
10 | c.d: "nested.blocks.html" outside
11 | c: default after
12 | c: "nested.blocks.html"
13 |
--------------------------------------------------------------------------------
/test/expected/nested-extends-append-stacked.html:
--------------------------------------------------------------------------------
1 | one: "blocks/parent-append-stacked.html"
2 | one: "blocks/nested-extends-append-stacked.html"
3 | one: "nested-extends-append-stacked.html"
4 |
--------------------------------------------------------------------------------
/test/expected/nested-extends-append.html:
--------------------------------------------------------------------------------
1 | one: "blocks/parent-append.html"
2 | one: "nested-extends-append.html"
3 |
4 | two: "nested-extends-append.html"
5 |
6 | three: "blocks/parent-append.html"
7 | three: "blocks/nested-extends-append.html"
8 |
9 | four: "blocks/parent-append.html"
10 | four: "blocks/nested-extends-append.html"
11 |
12 | five: "nested-extends-append.html"
13 |
14 | six: "nested-extends-append.html"
15 |
16 | seven: "blocks/parent-append.html"
17 | seven: "blocks/nested-extends-append.html"
18 | seven: "nested-extends-append.html"
19 |
--------------------------------------------------------------------------------
/test/expected/nested-extends-mixed.html:
--------------------------------------------------------------------------------
1 | one: "nested-extends-mixed.html"
2 | one: "blocks/parent-mixed.html"
3 |
4 | two: "blocks/parent-mixed.html"
5 | two: "blocks/nested-extends-mixed.html"
6 | two: "nested-extends-mixed.html"
7 |
8 | three: "blocks/nested-extends-mixed.html"
9 | three: "blocks/parent-mixed.html"
10 |
11 | four: "blocks/parent-mixed.html"
12 | four: "blocks/nested-extends-mixed.html"
13 |
14 | five: "blocks/nested-extends-mixed.html"
15 | five: "nested-extends-mixed.html"
16 |
17 | six: "nested-extends-mixed.html"
18 |
19 | seven: "nested-extends-mixed.html"
20 | seven: "blocks/parent-mixed.html"
21 | seven: "blocks/nested-extends-mixed.html"
22 |
--------------------------------------------------------------------------------
/test/expected/nested-extends-mixed2.html:
--------------------------------------------------------------------------------
1 | one: "nested-extends-mixed2.html"
2 | one: "blocks/parent-mixed2.html"
3 |
4 | two: "blocks/nested-extends-mixed2.html"
5 | two: "blocks/parent-mixed2.html"
6 | two: "nested-extends-mixed2.html"
7 |
8 | three: "blocks/nested-extends-mixed2.html"
9 | three: "blocks/parent-mixed2.html"
10 |
11 | four: "blocks/nested-extends-mixed2.html"
12 | four: "blocks/parent-mixed2.html"
13 |
14 | five: "blocks/nested-extends-mixed2.html"
15 | five: "nested-extends-mixed2.html"
16 |
17 | six: "nested-extends-mixed2.html"
18 |
19 | seven: "nested-extends-mixed2.html"
20 | seven: "blocks/nested-extends-mixed2.html"
21 | seven: "blocks/parent-mixed2.html"
22 |
--------------------------------------------------------------------------------
/test/expected/nested-extends-prepend.html:
--------------------------------------------------------------------------------
1 | one: "nested-extends-prepend.html"
2 | one: "blocks/parent-prepend.html"
3 |
4 | two: "nested-extends-prepend.html"
5 |
6 | three: "blocks/nested-extends-prepend.html"
7 | three: "blocks/parent-prepend.html"
8 |
9 | four: "blocks/nested-extends-prepend.html"
10 | four: "blocks/parent-prepend.html"
11 |
12 | five: "nested-extends-prepend.html"
13 |
14 | six: "nested-extends-prepend.html"
15 |
16 | seven: "nested-extends-prepend.html"
17 | seven: "blocks/nested-extends-prepend.html"
18 | seven: "blocks/parent-prepend.html"
19 |
--------------------------------------------------------------------------------
/test/expected/nested-extends.html:
--------------------------------------------------------------------------------
1 | head: "blocks/parent.html"
2 |
3 | header: "nested-extends.html"
4 |
5 | body: "blocks/nested-extends.html"
6 |
7 | footer: "nested-extends.html"
8 |
9 | foot: "blocks/parent.html"
10 |
--------------------------------------------------------------------------------
/test/expected/options-trim.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple.html"
2 |
3 | header: "block-multiple.html"
4 |
5 | body: "block-multiple.html"
6 |
7 | footer: "block-multiple.html"
8 |
9 | foot: "blocks/multiple.html"
10 |
--------------------------------------------------------------------------------
/test/expected/other-blocks.html:
--------------------------------------------------------------------------------
1 | {% fake "nothing" %}
2 | header: "other-blocks.html"
3 | {% endfake %}
4 |
5 | header: "other-blocks.html"
6 |
7 | {% bar %}
8 | header: "other-blocks.html"
9 | {% endbar %}
10 |
--------------------------------------------------------------------------------
/test/expected/prepend-block-multiple.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple.html"
2 |
3 | header: "prepend-block-multiple.html"
4 | header: "blocks/multiple.html"
5 |
6 | body: "prepend-block-multiple.html"
7 | body: "blocks/multiple.html"
8 |
9 | footer: "prepend-block-multiple.html"
10 | footer: "blocks/multiple.html"
11 |
12 | foot: "blocks/multiple.html"
13 |
--------------------------------------------------------------------------------
/test/expected/prepend-block.html:
--------------------------------------------------------------------------------
1 | unique: "blocks/basic.html"
2 |
3 | header: "prepend-block.html"
4 | header: "blocks/basic.html"
5 |
--------------------------------------------------------------------------------
/test/expected/repeat.html:
--------------------------------------------------------------------------------
1 | foo: "repeat.html"
2 |
3 | foo: "repeat.html"
4 |
5 | foo: "repeat.html"
6 |
7 | foo: "repeat.html"
8 |
9 | foo: "repeat.html"
10 |
11 | foo: "repeat.html"
12 |
--------------------------------------------------------------------------------
/test/expected/replace-block-multiple.html:
--------------------------------------------------------------------------------
1 | head: "blocks/multiple.html"
2 |
3 | header: "replace-block-multiple.html"
4 |
5 | body: "replace-block-multiple.html"
6 |
7 | footer: "replace-block-multiple.html"
8 |
9 | foot: "blocks/multiple.html"
10 |
--------------------------------------------------------------------------------
/test/expected/replace-block.html:
--------------------------------------------------------------------------------
1 | unique: "blocks/basic.html"
2 |
3 | header: "replace-block.html"
4 |
--------------------------------------------------------------------------------
/test/expected/text-nodes.html:
--------------------------------------------------------------------------------
1 | header: from text-nodes.html
2 |
3 | text node from "blocks/basic.html"
4 |
--------------------------------------------------------------------------------
/test/fixtures/_macros/fields.html:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type = "text", size = 20) %}
2 |
3 | {% endmacro %}
4 |
--------------------------------------------------------------------------------
/test/fixtures/_macros/multiple.html:
--------------------------------------------------------------------------------
1 | {% macro one() %}
2 | This is macro "one"
3 | {% endmacro %}
4 |
5 | {% macro two() %}
6 | This is macro "two"
7 | {% endmacro %}
8 |
9 | {% macro three() %}
10 | This is macro "three"
11 | {% endmacro %}
12 |
13 | {% macro four(name="four") %}
14 | This is macro "{{ name }}"
15 | {% endmacro %}
16 |
17 | {% macro hello(name="friend") %}
18 | Hello, {{ name }}!
19 | {% endmacro %}
20 |
--------------------------------------------------------------------------------
/test/fixtures/_macros/signup.html:
--------------------------------------------------------------------------------
1 | {% macro input(name, value, type = "text", size = 20) %}
2 |
3 | {% endmacro %}
4 |
5 | {% macro textarea(name, value, rows = 10, cols = 40) %}
6 |
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/test/fixtures/_macros/simple.html:
--------------------------------------------------------------------------------
1 | {% macro hello(name) %}Hello, {{ name }}!{% endmacro %}
--------------------------------------------------------------------------------
/test/fixtures/_macros/test.html:
--------------------------------------------------------------------------------
1 | Test
2 |
--------------------------------------------------------------------------------
/test/fixtures/_macros/textarea.html:
--------------------------------------------------------------------------------
1 | {% macro textarea(name, value, rows = 10, cols = 40) %}
2 |
3 | {% endmacro %}
4 |
--------------------------------------------------------------------------------
/test/fixtures/en_locale.yml:
--------------------------------------------------------------------------------
1 | ---
2 | simple: "less is more"
3 | whatever: "something %{something}"
4 | errors:
5 | i18n:
6 | undefined_interpolation: "undefined key %{key}"
7 | unknown_translation: "translation '%{name}' wasn't found"
8 | syntax:
9 | oops: "something wasn't right"
10 |
--------------------------------------------------------------------------------
/test/integration/block_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { with_custom_tag } = require('../test_helpers');
4 | const Dry = require('../..');
5 |
6 | describe('block_test', () => {
7 | it('test_unexpected_end_tag', () => {
8 | assert.throws(() => Dry.Template.parse('{% if true %}{% endunless %}'));
9 |
10 | try {
11 | Dry.Template.parse('{% if true %}{% endunless %}');
12 | } catch (err) {
13 | assert.equal(err.message, "Dry syntax error: 'endunless' is not a valid delimiter for if tags. use endif");
14 | }
15 | });
16 |
17 | it('test_with_custom_tag', () => {
18 | with_custom_tag('testtag', class extends Dry.BlockTag {}, () => {
19 | assert(Dry.Template.parse('{% testtag %} {% endtesttag %}'));
20 | });
21 | });
22 |
23 | it('test_custom_block_tags_rendering', async () => {
24 | class klass1 extends Dry.BlockTag {
25 | render() {
26 | return 'hello';
27 | }
28 | }
29 |
30 | await with_custom_tag('blabla', klass1, async () => {
31 | const template = Dry.Template.parse('{% blabla %} bla {% endblabla %}');
32 | assert.equal('hello', await template.render());
33 | assert.equal('prefix+hello', await template.render({}, { output: 'prefix+' }));
34 | });
35 |
36 | class klass2 extends klass1 {
37 | render() {
38 | return 'foo' + super.render() + 'bar';
39 | }
40 | }
41 |
42 | await with_custom_tag('blabla', klass2, async () => {
43 | const template = Dry.Template.parse('{% blabla %} foo {% endblabla %}');
44 | assert.equal('foohellobar', await template.render());
45 | assert.equal('prefix+foohellobar', await template.render({}, { output: 'prefix+' }));
46 | });
47 | });
48 | });
49 |
50 |
--------------------------------------------------------------------------------
/test/integration/expression_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Parser = require('../../lib/expressions/Parser');
4 | const Dry = require('../..');
5 | const { Context, Expression, Template } = Dry;
6 |
7 | const parse_and_eval = (markup, assigns) => {
8 | if (Template.error_mode === 'strict') {
9 | const parser = new Parser(markup);
10 | markup = parser.expression();
11 | parser.consume('end_of_string');
12 | }
13 |
14 | const expression = Expression.parse(markup);
15 | const context = new Context(assigns);
16 | return context.evaluate(expression);
17 | };
18 |
19 | describe('expression_test', () => {
20 | it('test_keyword_literals', async () => {
21 | assert.equal(true, await parse_and_eval('true'));
22 | assert.equal(true, await parse_and_eval(' true '));
23 | });
24 |
25 | it('test_string', async () => {
26 | assert.equal('single quoted', await parse_and_eval("'single quoted'"));
27 | assert.equal('double quoted', await parse_and_eval('"double quoted"'));
28 | assert.equal('spaced', await parse_and_eval(" 'spaced' "));
29 | assert.equal('spaced2', await parse_and_eval(' "spaced2" '));
30 | });
31 |
32 | it('test_int', async () => {
33 | assert.equal(123, await parse_and_eval('123'));
34 | assert.equal(456, await parse_and_eval(' 456 '));
35 | assert.equal(12, await parse_and_eval('012'));
36 | });
37 |
38 | it('test_float', async () => {
39 | assert.equal(1.5, await parse_and_eval('1.5'));
40 | assert.equal(2.5, await parse_and_eval(' 2.5 '));
41 | });
42 |
43 | it('test_range', async () => {
44 | assert.deepEqual([1, 2], await parse_and_eval('(1..2)'));
45 | assert.deepEqual([3, 4, 5], await parse_and_eval(' ( 3 .. 5 ) '));
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/integration/hash_ordering_test.js:
--------------------------------------------------------------------------------
1 |
2 | const util = require('util');
3 | const assert = require('node:assert/strict');
4 | const { with_global_filter } = require('../test_helpers');
5 | const Dry = require('../..');
6 |
7 | const MoneyFilter = {
8 | money(input) {
9 | return util.format(' %d$ ', input);
10 | }
11 | };
12 |
13 | const CanadianMoneyFilter = {
14 | money(input) {
15 | return util.format(' %d$ CAD ', input);
16 | }
17 | };
18 |
19 | describe('hash_ordering_test', () => {
20 | it('test_global_register_order', async () => {
21 | await with_global_filter([MoneyFilter, CanadianMoneyFilter], async () => {
22 | assert.equal(' 1000$ CAD ', await Dry.Template.parse('{{1000 | money}}').render(null, null));
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/integration/tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { with_custom_tag } = require('../test_helpers');
4 | const Dry = require('../..');
5 |
6 | describe('tag_test', () => {
7 | it('test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility', async () => {
8 | const klass1 = class extends Dry.Tag {
9 | render() {
10 | return 'hello';
11 | }
12 | };
13 |
14 | await with_custom_tag('blabla', klass1, async () => {
15 | const template = Dry.Template.parse('{% blabla %}');
16 | assert.equal('hello', await template.render());
17 | });
18 |
19 | const klass2 = class extends klass1 {
20 | render() {
21 | return 'foo' + super.render() + 'bar';
22 | }
23 | };
24 |
25 | await with_custom_tag('blabla', klass2, async () => {
26 | const template = Dry.Template.parse('{% blabla %}');
27 | assert.equal('foohellobar', await template.render());
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/integration/tags/break_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('break_tag_test', () => {
5 | // tests that no weird errors are raised if (break is called outside of a) {
6 | // block
7 | it('test_break_with_no_block', async () => {
8 | const assigns = { i: 1 };
9 | const markup = '{% break %}';
10 | const expected = '';
11 |
12 | await assert_template_result(expected, markup, assigns);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/integration/tags/capture_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { assert_template_result } = require('../../test_helpers');
4 | const { Template } = require('../../..');
5 |
6 | describe('capture_tag_test', () => {
7 | it('test_captures_block_content_in_variable', async () => {
8 | await assert_template_result('test string', "{% capture 'var' %}test string{% endcapture %}{{var}}", {});
9 | });
10 |
11 | it('test_capture_with_hyphen_in_variable_name', async () => {
12 | const template_source = `
13 | {% capture this-thing %}Print this-thing{% endcapture %}
14 | {{ this-thing }}
15 | `;
16 | const template = Template.parse(template_source);
17 | const rendered = await template.render_strict();
18 | assert.equal('Print this-thing', rendered.trim());
19 | });
20 |
21 | it('test_capture_to_variable_from_outer_scope_if_existing', async () => {
22 | const template_source = `
23 | {% assign var = '' %}
24 | {% if true %}
25 | {% capture var %}first-block-string{% endcapture %}
26 | {% endif %}
27 | {% if true %}
28 | {% capture var %}test-string{% endcapture %}
29 | {% endif %}
30 | {{var}}
31 | `;
32 | const template = Template.parse(template_source);
33 | const rendered = await template.render_strict();
34 | assert.equal('test-string', rendered.replace(/\s/g, ''));
35 | });
36 |
37 | it('test_assigning_from_capture', async () => {
38 | const template_source = `
39 | {% assign first = '' %}
40 | {% assign second = '' %}
41 | {% for number in (1..3) %}
42 | {% capture first %}{{number}}{% endcapture %}
43 | {% assign second = first %}
44 | {% endfor %}
45 | {{ first }}-{{ second }}
46 | `;
47 | const template = Template.parse(template_source);
48 | const rendered = await template.render_strict();
49 | assert.equal('3-3', rendered.replace(/\s/g, ''));
50 | });
51 |
52 | it('test_assigned_variable_from_outer_scope_inside_capture', async () => {
53 | const template_source = `
54 | {% assign foo = "assigned" %}
55 | {% capture bar %}
56 | Inner content
57 | {{ foo }}
58 | {% endcapture %}
59 | {{ bar }}
60 | `;
61 |
62 | const template = Template.parse(template_source);
63 | const rendered = await template.render_strict();
64 | assert.equal('Innercontentassigned', rendered.replace(/\s/g, ''));
65 | });
66 |
67 | it('test_increment_assign_score_by_bytes_not_characters', async () => {
68 | const t = Template.parse('{% capture foo %}すごい{% endcapture %}');
69 | await t.render_strict();
70 | assert.equal(9, t.resource_limits.assign_score);
71 | });
72 | });
73 |
74 |
--------------------------------------------------------------------------------
/test/integration/tags/comment_tag.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('comment_tag_test', () => {
5 | describe('block_comments', () => {
6 | it('test_block_comment_does_not_render_contents', async () => {
7 | await assert_template_result('the comment block should be removed .. right?',
8 | 'the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?');
9 |
10 | await assert_template_result('', '{%comment%}{%endcomment%}');
11 | await assert_template_result('', '{%comment%}{% endcomment %}');
12 | await assert_template_result('', '{% comment %}{%endcomment%}');
13 | await assert_template_result('', '{% comment %}{% endcomment %}');
14 | await assert_template_result('', '{%comment%}comment{%endcomment%}');
15 | await assert_template_result('', '{% comment %}comment{% endcomment %}');
16 |
17 | await assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar');
18 | await assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar');
19 | await assert_template_result('foobar', 'foo{%comment%} comment {%endcomment%}bar');
20 | await assert_template_result('foobar', 'foo{% comment %} comment {% endcomment %}bar');
21 |
22 | await assert_template_result('foo bar', 'foo {%comment%} {%endcomment%} bar');
23 | await assert_template_result('foo bar', 'foo {%comment%}comment{%endcomment%} bar');
24 | await assert_template_result('foo bar', 'foo {%comment%} comment {%endcomment%} bar');
25 |
26 | await assert_template_result('foobar', 'foo{%comment%} {%endcomment%}bar');
27 | });
28 |
29 | it('test_block_comments_may_contain_other_tags', async () => {
30 | await assert_template_result('', '{% comment %}{{ name }}{% endcomment %}', { name: 'doowb' });
31 | await assert_template_result('', '{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}');
32 | });
33 |
34 | it('test_block_comments_may_contain_invalid_tags', async () => {
35 | await assert_template_result('', '{%comment%}{% endif %}{%endcomment%}');
36 | await assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}');
37 | await assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}');
38 | });
39 | });
40 |
41 | describe('line_comments', () => {
42 | it('test_line_comment_does_not_render_contents', async () => {
43 | await assert_template_result('', '{# comment #}');
44 | await assert_template_result('', '{# this is a comment #}');
45 | await assert_template_result('', '{# this is {% tag %} #}');
46 | await assert_template_result('', '{# this is "quoted string" #}');
47 | });
48 |
49 | it('test_text_renders_before_and_after_line_comment', async () => {
50 | await assert_template_result('foo bar', 'foo {# comment #} bar');
51 | await assert_template_result('foo bar', 'foo {# this is a comment #} bar');
52 | await assert_template_result('foo bar', 'foo {# this is {% tag %} #} bar');
53 | await assert_template_result('foo bar', 'foo {# this is "quoted string" #} bar');
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/integration/tags/content_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('content_tag_test', () => {
5 | it('test_content_variable', async () => {
6 | const assigns = { content: 'The content' };
7 | const fixture = `
8 |
9 |
10 |
11 | {% content %}
12 |
13 |
14 | `;
15 |
16 | const expected = `
17 |
18 |
19 |
20 | The content
21 |
22 |
23 | `;
24 | await assert_template_result(expected, fixture, assigns);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/integration/tags/continue_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('continue_tag_test', () => {
5 | // tests that no weird errors are raised if (continue is called outside of a) {
6 | // block
7 | it('test_continue_with_no_block', async () => {
8 | const assigns = {};
9 | const markup = '{% continue %}';
10 | const expected = '';
11 |
12 | await assert_template_result(expected, markup, assigns);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/integration/tags/cycle_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('cycle_tag_test', () => {
5 | it('test_cycle', async () => {
6 | await assert_template_result('one', '{%cycle "one", "two"%}');
7 | await assert_template_result('one two', '{%cycle "one", "two"%} {%cycle "one", "two"%}');
8 | await assert_template_result(' two', '{%cycle "", "two"%} {%cycle "", "two"%}');
9 |
10 | await assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}');
11 |
12 | await assert_template_result('text-align: left text-align: right',
13 | '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}');
14 | });
15 |
16 | it('test_multiple_cycles', async () => {
17 | await assert_template_result('1 2 1 1 2 3 1',
18 | '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}');
19 | });
20 |
21 | it('test_multiple_named_cycles', async () => {
22 | await assert_template_result('one one two two one one',
23 | '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}');
24 | });
25 |
26 | it('test_multiple_named_cycles_with_names_from_context', async () => {
27 | const assigns = { 'var1': 1, 'var2': 2 };
28 | await assert_template_result('one one two two one one',
29 | '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/test/integration/tags/echo_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('echo_test', () => {
5 | it('test_echo_outputs_its_input', async () => {
6 | await assert_template_result('BAR', '{%- echo variable-name | upcase -%}', { 'variable-name': 'bar' });
7 | });
8 | });
9 |
10 |
--------------------------------------------------------------------------------
/test/integration/tags/embed_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result, StubFileSystem } = require('../../test_helpers');
3 | const { State, Template } = require('../../..');
4 |
5 | describe('embed_tag_test', () => {
6 | beforeEach(() => {
7 | State.blocks = {};
8 | Template.file_system = new StubFileSystem({
9 | source: '{% block "top" %}default top{% endblock %}{% block "bottom" %}default bottom{% endblock %}'
10 | });
11 | });
12 |
13 | it('test_renders_and_empty_string_when_template_not_found', async () => {
14 | await assert_template_result('', "{% embed 'not-found' %}test string{% endembed %}", {});
15 | });
16 |
17 | it('test_embeds_content', async () => {
18 | Template.file_system = new StubFileSystem({ source: 'test string' });
19 | await assert_template_result('test string', "{% embed 'source' %}{% endembed %}", {});
20 | });
21 |
22 | it('test_embeds_content_from_blocks', async () => {
23 | await assert_template_result('default topdefault bottom', "{% embed 'source' %}{% endembed %}", {});
24 | });
25 |
26 | it('test_overrides_content_from_blocks', async () => {
27 | await assert_template_result('overriddendefault bottom', "{% embed 'source' %}{% block 'top' %}overridden{% endblock %}{% endembed %}", {});
28 | });
29 |
30 | it('test_assign_inside_block', async () => {
31 | const fixture = '{% embed \'source\' %}{% block "top" %}{% assign var = \'_assigned\' %}overridden{{var}}{% endblock %}{% endembed %}';
32 | await assert_template_result('overridden_assigneddefault bottom', fixture, {});
33 | });
34 |
35 | it('test_assign_outside_block', async () => {
36 | const fixture = '{% embed "source" %}{% assign var = "_assigned" %}{% block "top" %}overridden{{var}}{% endblock %}{% endembed %}';
37 | await assert_template_result('overriddendefault bottom', fixture, {});
38 | });
39 |
40 | it('test_embed_with', async () => {
41 | const fixture = '{% embed "source" with a %}{% block "top" %}{{b}}{% endblock %}{% endembed %}';
42 | await assert_template_result('new topdefault bottom', fixture, { a: { b: 'new top' } });
43 | });
44 | });
45 |
46 |
--------------------------------------------------------------------------------
/test/integration/tags/increment_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('increment tag tests', () => {
5 | it('inc', async () => {
6 | await assert_template_result('0', '{%increment port %}', {});
7 | await assert_template_result('0 1', '{%increment port %} {%increment port%}', {});
8 | await assert_template_result('0 0 1 2 1', '{%increment port %} {%increment starboard%} {%increment port %} {%increment port%} {%increment starboard %}', {});
9 | });
10 |
11 | it('dec', async () => {
12 | // await assert_template_result('9', '{%decrement port %}', { port: 10 });
13 | await assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {});
14 | await assert_template_result('1 5 2 2 5', '{%increment port %} {%increment starboard%} {%increment port %} {%decrement port%} {%decrement starboard %}', { port: 1, starboard: 5 });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/integration/tags/raw_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { assert_template_result } = require('../../test_helpers');
4 |
5 | describe('raw_tag_tests', () => {
6 | it('test_tag_in_raw', async () => {
7 | await assert_template_result('{% comment %} test {% endcomment %}', '{% raw %}{% comment %} test {% endcomment %}{% endraw %}');
8 | });
9 |
10 | it('test_trim_mode_in_raw', async () => {
11 | await assert_template_result('{% comment %} test {% endcomment %}', '{%- raw -%} {% comment %} test {% endcomment %} {%- endraw -%}');
12 | });
13 |
14 | it('test_output_in_raw', async () => {
15 | await assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}');
16 | });
17 |
18 | it('test_open_tag_in_raw', async () => {
19 | await assert_template_result(' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}');
20 | await assert_template_result(' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}');
21 | await assert_template_result(' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}');
22 | await assert_template_result(' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}');
23 | await assert_template_result(' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}');
24 | await assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}');
25 | await assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}');
26 | await assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}');
27 | });
28 |
29 | it('test_invalid_raw', async () => {
30 | await assert.rejects(() => assert_template_result('', '{% raw %} foo'), /tag was never closed/);
31 | await assert.rejects(() => assert_template_result('', '{% raw } foo {% endraw %}'), /Syntax Error/);
32 | await assert.rejects(() => assert_template_result('', '{% raw } foo %}{% endraw %}'), /Syntax Error/);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/integration/tags/unless_else_tag_test.js:
--------------------------------------------------------------------------------
1 |
2 | const { assert_template_result } = require('../../test_helpers');
3 |
4 | describe('unless_else_tag_test', () => {
5 | it('test_unless', async () => {
6 | await assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ');
7 | await assert_template_result(' this text should go into the output ', ' {% unless false %} this text should go into the output {% endunless %} ');
8 | await assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?');
9 | });
10 |
11 | it('test_unless_else', async () => {
12 | await assert_template_result(' YES ', '{% unless true %} NO {% else %} YES {% endunless %}');
13 | await assert_template_result(' YES ', '{% unless false %} YES {% else %} NO {% endunless %}');
14 | await assert_template_result(' YES ', '{% unless "foo" %} NO {% else %} YES {% endunless %}');
15 | });
16 |
17 | it('test_unless_in_loop', async () => {
18 | await assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', { choices: [1, null, false] });
19 | });
20 |
21 | it('test_unless_else_in_loop', async () => {
22 | await assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', { choices: [1, null, false] });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/support/themes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const readdir = require('@folder/readdir');
6 | const Dry = require('../..');
7 |
8 | const themes = async (dir, options) => {
9 | const opts = { recursive: true, ...options, objects: true };
10 | const onFile = async file => {
11 | if (file.name.endsWith('.md')) {
12 | const contents = fs.readFileSync(file.path);
13 | const template = Dry.Template.parse(contents);
14 | const output = template.render();
15 | // console.log(output);
16 | // console.log(file.path);
17 | // if (file.name === 'abs.md') {
18 | // }
19 | }
20 | };
21 |
22 | await readdir(dir, { ...opts, onFile });
23 | };
24 |
25 | // themes(path.join(__dirname, '../fixtures/filters'));
26 | // console.log(Dry.Parser.all_tags);
27 |
--------------------------------------------------------------------------------
/test/unit/block_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { Variable, nodes, tags } = Dry;
5 | const { Comment } = tags;
6 | const { Text } = nodes;
7 |
8 | const block_types = nodes => {
9 | return nodes.map(node => node.constructor);
10 | };
11 |
12 | describe('block_unit_test', () => {
13 | it('test_blankspace', () => {
14 | const template = Dry.Template.parse(' ');
15 | assert.equal(' ', template.root.ast.nodes[0].value);
16 | });
17 |
18 | it('test_variable_beginning', () => {
19 | const template = Dry.Template.parse('{{funk}} ');
20 | assert.equal(2, template.root.ast.nodes.length);
21 | assert.equal(Variable, template.root.ast.nodes[0].constructor);
22 | assert.equal(Text, template.root.ast.nodes[1].constructor);
23 | });
24 |
25 | it('test_variable_end', () => {
26 | const template = Dry.Template.parse(' {{funk}}');
27 | assert.equal(2, template.root.ast.nodes.length);
28 | assert.equal(Text, template.root.ast.nodes[0].constructor);
29 | assert.equal(Variable, template.root.ast.nodes[1].constructor);
30 | });
31 |
32 | it('test_variable_middle', () => {
33 | const template = Dry.Template.parse(' {{funk}} ');
34 | assert.equal(3, template.root.ast.nodes.length);
35 | assert.equal(Text, template.root.ast.nodes[0].constructor);
36 | assert.equal(Variable, template.root.ast.nodes[1].constructor);
37 | assert.equal(Text, template.root.ast.nodes[2].constructor);
38 | });
39 |
40 | it('test_variable_many_embedded_fragments', () => {
41 | const template = Dry.Template.parse(' {{funk}} {{so}} {{brother}} ');
42 | assert.equal(7, template.root.ast.nodes.length);
43 | assert.deepEqual([Text, Variable, Text, Variable, Text, Variable, Text], block_types(template.root.ast.nodes));
44 | });
45 |
46 | it('test_with_block', () => {
47 | const template = Dry.Template.parse(' {% comment %} {% endcomment %} ');
48 | assert.equal(3, template.root.ast.nodes.length);
49 | assert.deepEqual([Text, Comment, Text], block_types(template.root.ast.nodes));
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/unit/expression_lexer_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { expressions: { Lexer } } = Dry;
5 |
6 | describe('lexer_unit_test', () => {
7 | it('test_strings', () => {
8 | const tokens = new Lexer(' \'this is a test""\' "wat \'lol\'"').tokenize();
9 | assert.deepEqual([['string', '\'this is a test""\''], ['string', '"wat \'lol\'"'], ['end_of_string']], tokens);
10 | });
11 |
12 | it('test_integer', () => {
13 | const tokens = new Lexer('hi 50').tokenize();
14 | assert.deepEqual([['id', 'hi'], ['number', '50'], ['end_of_string']], tokens);
15 | });
16 |
17 | it('test_float', () => {
18 | const tokens = new Lexer('hi 5.0').tokenize();
19 | assert.deepEqual([['id', 'hi'], ['number', '5.0'], ['end_of_string']], tokens);
20 | });
21 |
22 | it('test_comparison', () => {
23 | const tokens = new Lexer('== <> contains ').tokenize();
24 | assert.deepEqual(
25 | [['comparison', '=='], ['comparison', '<>'], ['comparison', 'contains'], ['end_of_string']],
26 | tokens
27 | );
28 | });
29 |
30 | it('test_specials', () => {
31 | let tokens = new Lexer('| .:').tokenize();
32 | assert.deepEqual([['pipe', '|'], ['dot', '.'], ['colon', ':'], ['end_of_string']], tokens);
33 | tokens = new Lexer('[,]').tokenize();
34 | assert.deepEqual([['open_square', '['], ['comma', ','], ['close_square', ']'], ['end_of_string']], tokens);
35 | });
36 |
37 | it('test_fancy_identifiers', () => {
38 | let tokens = new Lexer('hi five?').tokenize();
39 | assert.deepEqual([['id', 'hi'], ['id', 'five?'], ['end_of_string']], tokens);
40 |
41 | tokens = new Lexer('2foo').tokenize();
42 | assert.deepEqual([['number', '2'], ['id', 'foo'], ['end_of_string']], tokens);
43 | });
44 |
45 | it('test_whitespace', () => {
46 | const tokens = new Lexer('five|\n\t ==').tokenize();
47 | assert.deepEqual([['id', 'five'], ['pipe', '|'], ['comparison', '=='], ['end_of_string']], tokens);
48 | });
49 |
50 | it('test_unexpected_character', () => {
51 | assert.throws(() => new Lexer('@').tokenize(), Dry.SyntaxError);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/test/unit/expression_parser_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { expressions: { Parser } } = Dry;
5 |
6 | describe('parser_unit_test', () => {
7 | it('test_consume', () => {
8 | const p = new Parser('wat: 7');
9 | assert.equal('wat', p.consume('id'));
10 | assert.equal(':', p.consume('colon'));
11 | assert.equal('7', p.consume('number'));
12 | });
13 |
14 | it('test_jump', () => {
15 | const p = new Parser('wat: 7');
16 | p.jump(2);
17 | assert.equal('7', p.consume('number'));
18 | });
19 |
20 | it('test_consume_', () => {
21 | const p = new Parser('wat: 7');
22 | assert.equal('wat', p.accept('id'));
23 | assert.equal(false, p.accept('dot'));
24 | assert.equal(':', p.consume('colon'));
25 | assert.equal('7', p.accept('number'));
26 | });
27 |
28 | it('test_id_', () => {
29 | const p = new Parser('wat 6 Peter Hegemon');
30 | assert.equal('wat', p.id('wat'));
31 | assert.equal(false, p.id('endgame'));
32 | assert.equal('6', p.consume('number'));
33 | assert.equal('Peter', p.id('Peter'));
34 | assert.equal(false, p.id('Achilles'));
35 | });
36 |
37 | it('test_look', () => {
38 | const p = new Parser('wat 6 Peter Hegemon');
39 | assert.equal(true, p.look('id'));
40 | assert.equal('wat', p.consume('id'));
41 | assert.equal(false, p.look('comparison'));
42 | assert.equal(true, p.look('number'));
43 | assert.equal(true, p.look('id', 1));
44 | assert.equal(false, p.look('number', 1));
45 | });
46 |
47 | it('test_expressions', () => {
48 | let p = new Parser('hi.there hi?[5].there? hi.there.bob');
49 | assert.equal('hi.there', p.expression());
50 | assert.equal('hi?[5].there?', p.expression());
51 | assert.equal('hi.there.bob', p.expression());
52 |
53 | p = new Parser("567 6.0 'lol' \"wut\"");
54 | assert.equal('567', p.expression());
55 | assert.equal('6.0', p.expression());
56 | assert.equal("'lol'", p.expression());
57 | assert.equal('"wut"', p.expression());
58 | });
59 |
60 | it('test_ranges', () => {
61 | const p = new Parser('(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)');
62 | assert.equal('(5..7)', p.expression());
63 | assert.equal('(1.5..9.6)', p.expression());
64 | assert.equal('(young..old)', p.expression());
65 | assert.equal('(hi[5].wat..old)', p.expression());
66 | });
67 |
68 | it('test_arguments', () => {
69 | const p = new Parser('filter: hi.there[5], keyarg: 7');
70 | assert.equal('filter', p.consume('id'));
71 | assert.equal(':', p.consume('colon'));
72 | assert.equal('hi.there[5]', p.argument());
73 | assert.equal(',', p.consume('comma'));
74 | assert.equal('keyarg: 7', p.argument());
75 | });
76 |
77 | it('test_invalid_expression', () => {
78 | assert.throws(() => {
79 | const p = new Parser('==');
80 | p.expression();
81 | }, Dry.SyntaxError);
82 | });
83 | });
84 |
85 |
--------------------------------------------------------------------------------
/test/unit/file_system_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { FileSystem, FileSystemError } = Dry;
5 | const { BlankFileSystem, LocalFileSystem } = FileSystem;
6 |
7 | const p = filepath => {
8 | return process.platform === 'win32' ? filepath.slice(2).split('\\').join('/') : filepath;
9 | };
10 |
11 | describe('file_system_unit_test', () => {
12 | it('test_default', () => {
13 | assert.throws(() => new BlankFileSystem().read_template_file('dummy'), FileSystemError);
14 | });
15 |
16 | it('test_local', () => {
17 | const file_system = new LocalFileSystem('/some/path');
18 | assert.equal('/some/path/_mypartial.liquid', p(file_system.full_path('mypartial')));
19 | assert.equal('/some/path/dir/_mypartial.liquid', p(file_system.full_path('dir/mypartial')));
20 |
21 | assert.throws(() => p(file_system.full_path('../dir/mypartial')), FileSystemError);
22 | assert.throws(() => p(file_system.full_path('/dir/../../dir/mypartial')), FileSystemError);
23 | assert.throws(() => p(file_system.full_path('/etc/passwd')), FileSystemError);
24 | });
25 |
26 | it('test_custom_template_filename_patterns', () => {
27 | const file_system = new LocalFileSystem('/some/path', '%s.html');
28 | assert.equal('/some/path/mypartial.html', p(file_system.full_path('mypartial')));
29 | assert.equal('/some/path/dir/mypartial.html', p(file_system.full_path('dir/mypartial')));
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/test/unit/i18n_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { fixture } = require('../test_helpers');
4 | const Dry = require('../..');
5 | const { I18n } = Dry;
6 |
7 | describe('i18n_unit_test', () => {
8 | let i18n;
9 |
10 | before(() => {
11 | i18n = new I18n(fixture('en_locale.yml'));
12 | });
13 |
14 | it('test_simple_translate_string', () => {
15 | assert.equal('less is more', i18n.translate('simple'));
16 | });
17 |
18 | it('test_nested_translate_string', () => {
19 | assert.equal("something wasn't right", i18n.translate('errors.syntax.oops'));
20 | });
21 |
22 | it('test_single_string_interpolation', () => {
23 | assert.equal('something different', i18n.translate('whatever', { something: 'different' }));
24 | });
25 |
26 | it('test_raises_unknown_translation', () => {
27 | assert.throws(() => i18n.translate('doesnt_exist'), I18n.TranslationError);
28 | });
29 |
30 | it('test_sets_default_path_to_en', () => {
31 | assert.equal(I18n.DEFAULT_LOCALE, new I18n().path);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/test/unit/lexer_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { expressions } = require('../..');
4 | const { Lexer } = expressions;
5 |
6 | describe('lexer_unit_test', () => {
7 | let tokens;
8 |
9 | it('test_strings', () => {
10 | tokens = new Lexer(' \'this is a test""\' "wat \'lol\'"').tokenize();
11 | assert.deepEqual([['string', '\'this is a test""\''], ['string', '"wat \'lol\'"'], ['end_of_string']], tokens);
12 | });
13 |
14 | it('test_integer', () => {
15 | tokens = new Lexer('hi 50').tokenize();
16 | assert.deepEqual([['id', 'hi'], ['number', '50'], ['end_of_string']], tokens);
17 | });
18 |
19 | it('test_float', () => {
20 | tokens = new Lexer('hi 5.0').tokenize();
21 | assert.deepEqual([['id', 'hi'], ['number', '5.0'], ['end_of_string']], tokens);
22 | });
23 |
24 | it('test_comparison', () => {
25 | tokens = new Lexer('== <> contains ').tokenize();
26 | assert.deepEqual([['comparison', '=='], ['comparison', '<>'], ['comparison', 'contains'], ['end_of_string']], tokens);
27 | });
28 |
29 | it('test_specials', () => {
30 | tokens = new Lexer('| .:').tokenize();
31 | assert.deepEqual([['pipe', '|'], ['dot', '.'], ['colon', ':'], ['end_of_string']], tokens);
32 | tokens = new Lexer('[,]').tokenize();
33 | assert.deepEqual([['open_square', '['], ['comma', ','], ['close_square', ']'], ['end_of_string']], tokens);
34 | });
35 |
36 | it('test_fancy_identifiers', () => {
37 | tokens = new Lexer('hi five?').tokenize();
38 | assert.deepEqual([['id', 'hi'], ['id', 'five?'], ['end_of_string']], tokens);
39 |
40 | tokens = new Lexer('2foo').tokenize();
41 | assert.deepEqual([['number', '2'], ['id', 'foo'], ['end_of_string']], tokens);
42 | });
43 |
44 | it('test_whitespace', () => {
45 | tokens = new Lexer('five|\n\t ==').tokenize();
46 | assert.deepEqual([['id', 'five'], ['pipe', '|'], ['comparison', '=='], ['end_of_string']], tokens);
47 | });
48 |
49 | it('test_unexpected_character', () => {
50 | assert.throws(() => {
51 | new Lexer('#').tokenize();
52 | });
53 | });
54 | });
55 |
56 |
--------------------------------------------------------------------------------
/test/unit/parse_append_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { Template } = Dry;
5 |
6 | describe('parse_append_unit_test', () => {
7 | it('test_blank_space', () => {
8 | const template = Template.parse(' ');
9 | assert.equal(' ', template.root.ast.value);
10 | });
11 |
12 | it('test_variable', () => {
13 | const template = Template.parse('{{funk}} ');
14 | assert.equal('{{funk}} ', template.root.ast.value);
15 | });
16 |
17 | it('test_block', () => {
18 | const fixture = ' {% comment %} {% endcomment %} ';
19 | const template = Template.parse(fixture);
20 | console.log([template.root.ast.value]);
21 | assert.equal(fixture, template.root.ast.value);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/unit/parser_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { expressions: { Parser } } = Dry;
5 |
6 | describe('parser_unit_test', () => {
7 | it('test_consume', () => {
8 | const p = new Parser('wat: 7');
9 | assert.equal('wat', p.consume('id'));
10 | assert.equal(':', p.consume('colon'));
11 | assert.equal('7', p.consume('number'));
12 | });
13 |
14 | it('test_jump', () => {
15 | const p = new Parser('wat: 7');
16 | p.jump(2);
17 | assert.equal('7', p.consume('number'));
18 | });
19 |
20 | it('test_consume_', () => {
21 | const p = new Parser('wat: 7');
22 | assert.equal('wat', p.accept('id'));
23 | assert.equal(false, p.accept('dot'));
24 | assert.equal(':', p.consume('colon'));
25 | assert.equal('7', p.accept('number'));
26 | });
27 |
28 | it('test_id_', () => {
29 | const p = new Parser('wat 6 Peter Hegemon');
30 | assert.equal('wat', p.id('wat'));
31 | assert.equal(false, p.id('endgame'));
32 | assert.equal('6', p.consume('number'));
33 | assert.equal('Peter', p.id('Peter'));
34 | assert.equal(false, p.id('Achilles'));
35 | });
36 |
37 | it('test_look', () => {
38 | const p = new Parser('wat 6 Peter Hegemon');
39 | assert.equal(true, p.look('id'));
40 | assert.equal('wat', p.consume('id'));
41 | assert.equal(false, p.look('comparison'));
42 | assert.equal(true, p.look('number'));
43 | assert.equal(true, p.look('id', 1));
44 | assert.equal(false, p.look('number', 1));
45 | });
46 |
47 | it('test_expressions', () => {
48 | let p = new Parser('hi.there hi?[5].there? hi.there.bob');
49 | assert.equal('hi.there', p.expression());
50 | assert.equal('hi?[5].there?', p.expression());
51 | assert.equal('hi.there.bob', p.expression());
52 |
53 | p = new Parser("567 6.0 'lol' \"wut\"");
54 | assert.equal('567', p.expression());
55 | assert.equal('6.0', p.expression());
56 | assert.equal("'lol'", p.expression());
57 | assert.equal('"wut"', p.expression());
58 | });
59 |
60 | it('test_ranges', () => {
61 | const p = new Parser('(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)');
62 | assert.equal('(5..7)', p.expression());
63 | assert.equal('(1.5..9.6)', p.expression());
64 | assert.equal('(young..old)', p.expression());
65 | assert.equal('(hi[5].wat..old)', p.expression());
66 | });
67 |
68 | it('test_arguments', () => {
69 | const p = new Parser('filter: hi.there[5], keyarg: 7');
70 | assert.equal('filter', p.consume('id'));
71 | assert.equal(':', p.consume('colon'));
72 | assert.equal('hi.there[5]', p.argument());
73 | assert.equal(',', p.consume('comma'));
74 | assert.equal('keyarg: 7', p.argument());
75 | });
76 |
77 | it('test_invalid_expression', () => {
78 | assert.throws(() => {
79 | const p = new Parser('==');
80 | p.expression();
81 | }, Dry.SyntaxError);
82 | });
83 | });
84 |
85 |
--------------------------------------------------------------------------------
/test/unit/regexp_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { QuotedFragment, VariableParser } = Dry.regex;
5 |
6 | describe('regexp_unit_test', () => {
7 | it('test_empty', () => {
8 | assert.deepEqual([], (''.match(QuotedFragment) || []).slice());
9 | });
10 |
11 | it('test_quote', () => {
12 | assert.deepEqual(['"arg 1"'], '"arg 1"'.match(QuotedFragment).slice());
13 | });
14 |
15 | it('test_words', () => {
16 | assert.deepEqual(['arg1', 'arg2'], 'arg1 arg2'.match(QuotedFragment).slice());
17 | });
18 |
19 | it('test_tags', () => {
20 | assert.deepEqual(['', ' '], ' '.match(QuotedFragment).slice());
21 | assert.deepEqual([' '], ' '.match(QuotedFragment).slice());
22 | assert.deepEqual([''], ''.match(QuotedFragment).slice());
23 | });
24 |
25 | it('test_double_quoted_words', () => {
26 | assert.deepEqual(['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.match(QuotedFragment).slice());
27 | });
28 |
29 | it('test_single_quoted_words', () => {
30 | assert.deepEqual(['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.match(QuotedFragment).slice());
31 | });
32 |
33 | it('test_quoted_words_in_the_middle', () => {
34 | assert.deepEqual(['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.match(QuotedFragment).slice());
35 | });
36 |
37 | it('test_VariableParser', () => {
38 | assert.deepEqual(['var'], 'var'.match(VariableParser).slice());
39 | assert.deepEqual(['var', 'method'], 'var.method'.match(VariableParser).slice());
40 | assert.deepEqual(['var', '[method]'], 'var[method]'.match(VariableParser).slice());
41 | assert.deepEqual(['var', '[method]', '[0]'], 'var[method][0]'.match(VariableParser).slice());
42 | assert.deepEqual(['var', '["method"]', '[0]'], 'var["method"][0]'.match(VariableParser).slice());
43 | assert.deepEqual(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.match(VariableParser).slice());
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/unit/strainer_factory_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { Context, StrainerFactory, StrainerTemplate } = Dry;
5 |
6 | const accessScopeFilters = {
7 | public_filter() {
8 | return 'public';
9 | },
10 | private_filter() {
11 | return 'private';
12 | }
13 | };
14 |
15 | const lateAddedFilter = {
16 | late_added_filter(_keep /* don't delete unused param, it's tested for arity*/) {
17 | return 'filtered';
18 | }
19 | };
20 |
21 | describe('strainer factory unit tests', () => {
22 | let context;
23 |
24 | before(() => {
25 | StrainerFactory.add_global_filter(accessScopeFilters);
26 | });
27 |
28 | beforeEach(() => {
29 | context = new Context();
30 | });
31 |
32 | it('strainer', () => {
33 | const strainer = StrainerFactory.create(context);
34 | assert.equal(5, strainer.invoke('size', 'input'));
35 | assert.equal('public', strainer.invoke('public_filter'));
36 | });
37 |
38 | it('strainer_argument_error_contains_backtrace', () => {
39 | const strainer = StrainerFactory.create(context);
40 | const errorRe = /wrong number of arguments \((1 for 0|given 1, expected 0)\)/;
41 |
42 | try {
43 | strainer.invoke('public_filter', 1);
44 | } catch (err) {
45 | assert(err instanceof Dry.ArgumentError);
46 | assert(errorRe.test(err.message));
47 | }
48 | });
49 |
50 | it('strainer_only_invokes_public_filter_methods', () => {
51 | const strainer = StrainerFactory.create(context);
52 | assert.equal(false, strainer.constructor.invokable('__proto__'));
53 | assert.equal(false, strainer.constructor.invokable('test'));
54 | assert.equal(false, strainer.constructor.invokable('eval'));
55 | assert.equal(false, strainer.constructor.invokable('call'));
56 | assert.equal(true, strainer.constructor.invokable('size')); // from the standard lib
57 | });
58 |
59 | it('strainer_returns_nil_if_no_filter_method_found', () => {
60 | const strainer = StrainerFactory.create(context);
61 | assert(!strainer.invoke('undef_the_filter'));
62 | });
63 |
64 | it('strainer_returns_first_argument_if_no_method_and_arguments_given', () => {
65 | const strainer = StrainerFactory.create(context);
66 | assert.equal('password', strainer.invoke('undef_the_method', 'password'));
67 | });
68 |
69 | it('strainer_only_allows_methods_defined_in_filters', () => {
70 | const strainer = StrainerFactory.create(context);
71 | assert.equal('1 + 1', strainer.invoke('instance_eval', '1 + 1'));
72 | assert.equal('puts', strainer.invoke('__send__', 'puts', 'Hi Mom'));
73 | assert.equal('has_method?', strainer.invoke('invoke', 'has_method?', 'invoke'));
74 | });
75 |
76 | it('strainer_uses_a_class_cache_to_avoid_method_cache_invalidation', () => {
77 | const strainer = StrainerFactory.create(context);
78 | assert(strainer instanceof StrainerTemplate);
79 | });
80 |
81 | it('add_global_filter_clears_cache', () => {
82 | assert.equal('input', StrainerFactory.create(context).invoke('late_added_filter', 'input'));
83 | StrainerFactory.add_global_filter(lateAddedFilter);
84 | assert.equal('filtered', StrainerFactory.create(null).invoke('late_added_filter', 'input'));
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/unit/strainer_template_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | require('mocha');
3 | const assert = require('node:assert/strict');
4 | const { assert_raises, with_global_filter } = require('../test_helpers');
5 | const Dry = require('../..');
6 | const { Context } = Dry;
7 |
8 | const ProtectedMethodOverrideFilter = {
9 | constructor() {
10 | return 'overriden as protected';
11 | }
12 | };
13 |
14 | const PrivateMethodOverrideFilter = {
15 | assign() {
16 | return 'overriden as private';
17 | }
18 | };
19 |
20 | const PublicMethodOverrideFilter = {
21 | public_filter() {
22 | return 'public';
23 | }
24 | };
25 |
26 | describe('strainer template unit tests', () => {
27 | it('test_add_filter_when_wrong_filter_class', async () => {
28 | const c = new Context();
29 | const s = c.strainer;
30 | const wrong_filter = v => v.reverse();
31 |
32 | const error = await assert_raises(Dry.TypeError, () => {
33 | return s.constructor.add_filter(wrong_filter);
34 | });
35 |
36 | assert.equal(error.message, 'Dry error: wrong argument type "function" (expected an object)');
37 | });
38 |
39 | it('test_add_filter_raises_when_module_privately_overrides_registered_public_methods', async () => {
40 | const strainer = new Context().strainer;
41 |
42 | const error = await assert_raises(Dry.MethodOverrideError, () => {
43 | return strainer.constructor.add_filter(PrivateMethodOverrideFilter);
44 | });
45 |
46 | assert.equal('Dry error: Filter overrides registered public methods as non public: assign', error.message);
47 | });
48 |
49 | it('test_add_filter_raises_when_module_overrides_registered_public_method_as_protected', async () => {
50 | const strainer = new Context().strainer;
51 |
52 | const error = await assert_raises(Dry.MethodOverrideError, () => {
53 | return strainer.constructor.add_filter(ProtectedMethodOverrideFilter);
54 | });
55 |
56 | assert.equal('Dry error: Filter overrides registered public methods as non public: constructor', error.message);
57 | });
58 |
59 | it('test_add_filter_does_not_raise_when_module_overrides_previously_registered_method', async () => {
60 | const strainer = new Context().strainer;
61 |
62 | with_global_filter(() => {
63 | strainer.constructor.add_filter(PublicMethodOverrideFilter);
64 | assert(strainer.constructor.filter_methods.has('public_filter'));
65 | });
66 | });
67 |
68 | it('test_add_filter_does_not_include_already_included_module', async () => {
69 | let count = 0;
70 | const mod = {
71 | included(_mod) {
72 | count += 1;
73 | }
74 | };
75 |
76 | const strainer = new Context().strainer;
77 | strainer.constructor.add_filter(mod);
78 | strainer.constructor.add_filter(mod);
79 | assert.equal(count, 1);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/unit/tag_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { Context, State, Tag, Tokenizer } = Dry;
5 |
6 | describe('tag_unit_test', () => {
7 | it('test_tag', async () => {
8 | const state = new State();
9 | const tag = Tag.parse({ name: 'tag', value: '' }, new Tokenizer('', state), state);
10 | assert.equal('tag', tag.name);
11 | assert.equal('', await tag.render(new Context()));
12 | });
13 |
14 | it('test_return_raw_text_of_tag', () => {
15 | const state = new State();
16 | const node = { name: 'long_tag', value: 'param1, param2, param3' };
17 | const tag = Tag.parse(node, new Tokenizer('', state), state);
18 | assert.equal('long_tag param1, param2, param3', tag.raw);
19 | });
20 |
21 | it('test_tag_name_should_return_name_of_the_tag', () => {
22 | const state = new State();
23 | const node = { name: 'some_tag', value: '' };
24 | const tag = Tag.parse(node, new Tokenizer('', state), state);
25 | assert.equal('some_tag', tag.name);
26 | });
27 | });
28 |
29 |
--------------------------------------------------------------------------------
/test/unit/tags/case_tag_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../../..');
4 |
5 | describe('case_tag_unit_test', () => {
6 | it('test_case_nodelist', () => {
7 | const template = Dry.Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}');
8 | assert.deepEqual(['WHEN', 'ELSE'], template.root.ast.nodes[0].nodelist.flat());
9 | });
10 | });
11 |
12 |
--------------------------------------------------------------------------------
/test/unit/tags/for_tag_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../../..');
4 |
5 | describe('for_tag_unit_test', () => {
6 | it('test_for_nodelist', () => {
7 | const template = Dry.Template.parse('{% for item in items %}FOR{% endfor %}');
8 | assert.deepEqual(['FOR'], template.root.ast.nodelist[0].nodelist.flat());
9 | });
10 |
11 | it('test_for_else_nodelist', () => {
12 | const template = Dry.Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}');
13 | assert.deepEqual(['FOR', 'ELSE'], template.root.ast.nodelist[0].nodelist);
14 | });
15 | });
16 |
17 |
--------------------------------------------------------------------------------
/test/unit/tags/if_tag_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../../..');
4 |
5 | describe('if_tag_unit_test', () => {
6 | it('test_if_nodelist', () => {
7 | const template = Dry.Template.parse('{% if true %}IF{% else %}ELSE{% endif %}');
8 | assert.deepEqual(['IF', 'ELSE'], template.root.ast.nodelist[0].nodelist);
9 | });
10 | });
11 |
12 |
--------------------------------------------------------------------------------
/test/unit/template_factory_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 |
5 | describe('template_factory_unit_test', () => {
6 | it('test_for_returns_liquid_template_instance', () => {
7 | const template = new Dry.TemplateFactory().for('anything');
8 | assert(template instanceof Dry.Template);
9 | });
10 | });
11 |
12 |
--------------------------------------------------------------------------------
/test/unit/template_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const { fixture } = require('../test_helpers');
4 | const Dry = require('../..');
5 | const { I18n, Template } = Dry;
6 |
7 | describe('template unit tests', () => {
8 | it('sets_default_localization_in_document', () => {
9 | const template = new Template();
10 | template.parse('{%comment%}{%endcomment%}');
11 | assert(template.root.ast.nodes[0].state.locale instanceof I18n);
12 | });
13 |
14 | it('sets_default_localization_in_context_with_quick_initialization', () => {
15 | const template = new Template();
16 | template.parse('{%comment%}{%endcomment%}', { locale: new I18n(fixture('en_locale.yml')) });
17 |
18 | const locale = template.root.ast.nodes[0].state.locale;
19 | assert(locale instanceof I18n);
20 | assert.equal(fixture('en_locale.yml'), locale.path);
21 | });
22 |
23 | it('tags_delete', () => {
24 | class FakeTag {}
25 | Template.register_tag('fake', FakeTag);
26 | assert.equal(FakeTag, Template.tags.get('fake'));
27 |
28 | Template.tags.delete('fake');
29 | assert(!Template.tags.get('fake'));
30 | });
31 |
32 | it('tags_can_be_looped_over', cb => {
33 | class FakeTag {}
34 | try {
35 | Template.tags.clear();
36 | Template.register_tag('fake', FakeTag);
37 | assert([...Template.tags.keys()].includes('fake'));
38 | cb();
39 | } catch (err) {
40 | cb(err);
41 | } finally {
42 | Template.tags.delete('fake');
43 | }
44 | });
45 |
46 | it('template_inheritance', async () => {
47 | class TemplateSubclass extends Dry.Template {}
48 | assert.equal('foo', await TemplateSubclass.parse('foo').render());
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/unit/tokenizer_unit_test.js:
--------------------------------------------------------------------------------
1 |
2 | const assert = require('node:assert/strict');
3 | const Dry = require('../..');
4 | const { State } = Dry;
5 |
6 | const new_tokenizer = (source, { state = new State(), start_line_number = null } = {}) => {
7 | return state.new_tokenizer(source, { start_line_number });
8 | };
9 |
10 | const tokenize = source => {
11 | const tokenizer = new_tokenizer(source);
12 | return tokenizer.tokens.filter(Boolean);
13 | };
14 |
15 | const tokenize_line_numbers = source => {
16 | const tokenizer = new_tokenizer(source, { start_line_number: 1 });
17 | const line_numbers = [];
18 | let line_number;
19 |
20 | do {
21 | if (line_number) line_numbers.push(line_number);
22 | line_number = tokenizer.line_number;
23 | } while (tokenizer.shift());
24 | return line_numbers;
25 | };
26 |
27 | describe('tokenizer_test', () => {
28 | it('test_tokenize_strings', () => {
29 | assert.deepEqual([' '], tokenize(' '));
30 | assert.deepEqual(['hello world'], tokenize('hello world'));
31 | });
32 |
33 | it('test_tokenize_variables', () => {
34 | assert.deepEqual(['{{funk}}'], tokenize('{{funk}}'));
35 | assert.deepEqual([' ', '{{funk}}', ' '], tokenize(' {{funk}} '));
36 | assert.deepEqual([' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} '));
37 | assert.deepEqual([' ', '{{ funk }}', ' '], tokenize(' {{ funk }} '));
38 | });
39 |
40 | it('test_tokenize_blocks', () => {
41 | assert.deepEqual(['{%comment%}'], tokenize('{%comment%}'));
42 | assert.deepEqual([' ', '{%comment%}', ' '], tokenize(' {%comment%} '));
43 |
44 | assert.deepEqual([' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} '));
45 | assert.deepEqual([' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(' {% comment %} {% endcomment %} '));
46 | });
47 |
48 | it('test_calculate_line_numbers_per_token_with_profiling', () => {
49 | assert.deepEqual([1], tokenize_line_numbers('{{funk}}'));
50 | assert.deepEqual([1, 1, 1], tokenize_line_numbers(' {{funk}} '));
51 | assert.deepEqual([1, 2, 2], tokenize_line_numbers('\n{{funk}}\n'));
52 | assert.deepEqual([1, 1, 3], tokenize_line_numbers(' {{\n funk \n}} '));
53 | });
54 | });
55 |
56 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "allowJs": true,
5 | "allowSyntheticDefaultImports": true,
6 | "checkJs": false,
7 | "esModuleInterop": true,
8 | "isolatedModules": true,
9 | "moduleResolution": "node",
10 | "noEmit": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "lib": [
13 | "ES2022"
14 | ],
15 | "resolveJsonModule": true,
16 | "strict": true,
17 | "target": "ES2022",
18 | "types": [
19 | "node"
20 | ]
21 | },
22 | "ts-node": {
23 | "transpileOnly": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------