var {
generateHTMLFromTemplate,
parseTemplateBrackets,
isTemplateValid,
} = ChromeUtils.importESModule("chrome://zotero/content/modules/templates.mjs");
describe("Templates", function () {
describe("#generateHTMLFromTemplate()", function () {
it("should support variables with attributes", function () {
var vars = {
v1: '1',
v2: pars => `${pars.a1 ?? ''}${pars.a2 ?? ''}${pars.a3 ?? ''}`,
v3: () => '',
v5: () => 'something',
ar1: [],
ar2: [1, 2]
};
var template = `{{ v1}}{{v2 a1= "1" a2 =' 2' a3 = "3 "}}{{v3}}{{v4}}{{if ar1}}ar1{{endif}}{{if ar2}}{{ar2}}{{endif}}{{if v5}}yes{{endif}}{{if v3}}no1{{endif}}{{if v2}}{{v2}}{{endif}}`;
var html = generateHTMLFromTemplate(template, vars);
assert.equal(html, '11 23 1,2yes');
});
it("should support empty string as attribute value and correctly render returned false-ish values", function () {
const vars = {
length: ({ string }) => string.length.toString(),
};
const template = `"" has a length of {{ length string="" }} and "hello" has a length of {{ length string="hello" }}`;
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, '"" has a length of 0 and "hello" has a length of 5');
});
it("should parse attributes using the shared string-literal semantics", function () {
const vars = { echo: attrs => JSON.stringify(attrs) };
// Both quote styles, an empty value, and a hyphenated (camelCased) name
assert.equal(
generateHTMLFromTemplate('{{ echo a="x" b=\'y\' c="" my-attr="z" }}', vars),
JSON.stringify({ a: 'x', b: 'y', c: '', myAttr: 'z' })
);
// A backslash is an ordinary character; a `"` always closes the value
assert.equal(
generateHTMLFromTemplate('{{ echo a="p\\q" }}', vars),
JSON.stringify({ a: 'p\\q' })
);
// A value may contain the opposite quote by delimiting with the other one
assert.equal(
generateHTMLFromTemplate(`{{ echo a='"' b="it's" }}`, vars),
JSON.stringify({ a: '"', b: "it's" })
);
// A newline inside a value is allowed
assert.equal(
generateHTMLFromTemplate('{{ echo a="p\nq" }}', vars),
JSON.stringify({ a: 'p\nq' })
);
});
it("should support functions in comparison statements", function () {
const vars = {
sum: ({ a, b }) => (parseInt(a) + parseInt(b)).toString(),
fooBar: ({ isFoo }) => (isFoo === 'true' ? 'foo' : 'bar'),
false: 'false',
twoWords: 'two words',
onlyOne: 'actually == 1'
};
const template = `{{if {{ sum a="1" b="2" }} == "3"}}1 + 2 = {{sum a="1" b="2"}}{{else}}no speak math{{endif}}`;
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, '1 + 2 = 3');
const template2 = '{{if false != "false"}}no{{elseif false == "false"}}yes{{else}}no{{endif}}';
const out2 = generateHTMLFromTemplate(template2, vars);
assert.equal(out2, 'yes');
const template3 = '{{ if twoWords == "two words" }}yes{{else}}no{{endif}}';
const out3 = generateHTMLFromTemplate(template3, vars);
assert.equal(out3, 'yes');
const template4 = '{{ if onlyOne == \'actually == 1\' }}yes{{else}}no{{endif}}';
const out4 = generateHTMLFromTemplate(template4, vars);
assert.equal(out4, 'yes');
const template5 = '{{ if "3" == {{ sum a="1" b="2" }} }}yes{{else}}no{{endif}}';
const out5 = generateHTMLFromTemplate(template5, vars);
assert.equal(out5, 'yes');
const template6 = '{{ if {{ sum a="1" b="2" }} }}yes{{else}}no{{endif}}';
const out6 = generateHTMLFromTemplate(template6, vars);
assert.equal(out6, 'yes');
const template7 = '{{ if {{ twoWords }} }}yes{{else}}no{{endif}}';
const out7 = generateHTMLFromTemplate(template7, vars);
assert.equal(out7, 'yes');
const template8 = '{{ if twoWords }}yes{{else}}no{{endif}}';
const out8 = generateHTMLFromTemplate(template8, vars);
assert.equal(out8, 'yes');
const template9 = '{{ if missing }}no{{else}}yes{{endif}}';
const out9 = generateHTMLFromTemplate(template9, vars);
assert.equal(out9, 'yes');
const template10 = '{{ if {{ missing foo="bar" }} }}no{{else}}yes{{endif}}';
const out10 = generateHTMLFromTemplate(template10, vars);
assert.equal(out10, 'yes');
const template11 = '{{ if {{ missing foo="bar" }} == "" }}yes{{else}}no{{endif}}';
const out11 = generateHTMLFromTemplate(template11, vars);
assert.equal(out11, 'yes');
const template12 = '{{ if fooBar == "bar" }}yes{{else}}no{{endif}}';
const out12 = generateHTMLFromTemplate(template12, vars);
assert.equal(out12, 'yes');
const template13 = '{{ if {{ fooBar }} == "bar" }}yes{{else}}no{{endif}}';
const out13 = generateHTMLFromTemplate(template13, vars);
assert.equal(out13, 'yes');
const template14 = `{{if {{ sum a="1" b="2" }}=="3"}}1 + 2 = {{sum a="1" b="2"}}{{else}}no{{endif}}`;
const out14 = generateHTMLFromTemplate(template14, vars);
assert.equal(out14, '1 + 2 = 3');
const template15 = `{{if "two words"==twoWords}}yes{{else}}no{{endif}}`;
const out15 = generateHTMLFromTemplate(template15, vars);
assert.equal(out15, 'yes');
});
it("should support relational operators", function () {
const vars = {
sum: ({ a, b }) => (parseInt(a) + parseInt(b)).toString(),
v1: '1',
v2: 'foo',
v3: '100',
v4: '99',
π: '3.14',
};
const template1 = `{{if v1 > π}}more than π{{elseif v1 <= π}}less or equal to π{{endif}}`;
const out1 = generateHTMLFromTemplate(template1, vars);
assert.equal(out1, 'less or equal to π');
const template2 = `{{if {{ sum a="2" b="3" }} > π}}more than π{{else}}less or equal to π{{endif}}`;
const out2 = generateHTMLFromTemplate(template2, vars);
assert.equal(out2, 'more than π');
const template3 = `{{if 3.14 >= π}}more than or equal to π{{else}}less than π{{endif}}`;
const out3 = generateHTMLFromTemplate(template3, vars);
assert.equal(out3, 'more than or equal to π');
const template4 = `{{if v3 > v4}}100 is more than 99{{else}}string "100" would be sorted before "99"{{endif}}`;
const out4 = generateHTMLFromTemplate(template4, vars);
assert.equal(out4, '100 is more than 99');
// This is undocumented and unsupported behavior, but comparing strings should work
const template5 = `{{if "test" > v2}}"test" > "foo"{{else}}no{{endif}}`;
const out5 = generateHTMLFromTemplate(template5, vars);
assert.equal(out5, '"test" > "foo"');
const template6 = `{{if "bar" < v2 }}"bar" < "foo"{{else}}no{{endif}}`;
const out6 = generateHTMLFromTemplate(template6, vars);
assert.equal(out6, '"bar" < "foo"');
});
it("should compare as strings when a bare number is compared with a non-numeric value", function () {
const vars = {
edition: '2nd ed.',
year: '2020',
};
const template1 = '{{if edition == 2}}second{{else}}other{{endif}}';
const out1 = generateHTMLFromTemplate(template1, vars);
assert.equal(out1, 'other');
const template2 = '{{if edition != 2}}other{{else}}second{{endif}}';
const out2 = generateHTMLFromTemplate(template2, vars);
assert.equal(out2, 'other');
const template3 = '{{if 2 == edition}}second{{else}}other{{endif}}';
const out3 = generateHTMLFromTemplate(template3, vars);
assert.equal(out3, 'other');
const template4 = '{{if edition < 5}}less{{else}}more{{endif}}';
const out4 = generateHTMLFromTemplate(template4, vars);
assert.equal(out4, 'less');
// The same comparison still evaluates numerically when the value is numeric
const template5 = '{{if year == 2020}}yes{{else}}no{{endif}}';
const out5 = generateHTMLFromTemplate(template5, vars);
assert.equal(out5, 'yes');
});
it("should accept hyphen-case variables and attributes", function () {
const vars = {
fooBar: ({ isFoo }) => (isFoo === 'true' ? 'foo' : 'bar'),
};
const template = '{{ foo-bar is-foo="true" }}{{ if {{ foo-bar is-foo="false" }} == "bar" }}{{ foo-bar is-foo="false" }}{{ endif }}';
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, 'foobar');
});
it("should not throw on an unbalanced {{endif}}", function () {
const vars = {
v1: '1',
};
// An {{endif}} without a matching {{if}} shouldn't pop the base level and crash
const template = 'foo{{endif}}bar{{if v1 == "1"}}baz{{endif}}';
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, 'foobarbaz');
});
it("should work with a condition in the middle", function () {
const vars = {
v1: '1',
};
const template = 'test {{ if v1 == "1" }}yes{{ else }}no{{ endif }} foobar';
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, 'test yes foobar');
});
it("missing identifiers are evaluted as empty string", function () {
const vars = {
foo: 'foo',
};
const template = '{{bar}}{{ if foo == "" }}no{{elseif foo}}{{foo}}{{else}}no{{endif}}';
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, 'foo');
const template2 = 'test: {{ if bar == "" }}yes{{else}}no{{endif}}';
const out2 = generateHTMLFromTemplate(template2, vars);
assert.equal(out2, 'test: yes');
});
it("should preserve whitespace outside of brackets", function () {
const template = ' starts }} with {{ whitespace {"test"} == \'foobar\' ';
const out = generateHTMLFromTemplate(template, {});
assert.equal(out, template);
const vars = {
space: ' ',
spaceFn: () => ' ',
};
const whitespace = ' {{if spaceFn}}{{else}} {{endif}}{{space}} {{space-fn}}';
const out2 = generateHTMLFromTemplate(whitespace, vars);
assert.equal(out2, ' ');
});
it("should accept array values in logic statements", function () {
let someTags = ['foo', 'bar'];
const vars = {
tags: ({ join }) => (join ? someTags.join(join) : someTags),
};
const template = '{{ if tags }}#{{ tags join=" #" }}{{else}}no tags{{endif}}';
const out = generateHTMLFromTemplate(template, vars);
assert.equal(out, '#foo #bar');
someTags = [];
const out2 = generateHTMLFromTemplate(template, vars);
assert.equal(out2, 'no tags');
});
it("should throw if function returns anything else than a string (or an array which is always joined into string)", function () {
const vars = {
number: () => 1,
logic: () => true,
array: () => [],
fn: () => 1,
};
assert.throws(() => generateHTMLFromTemplate('{{ number }}', vars), /Identifier "number" does not evaluate to a string/);
assert.throws(() => generateHTMLFromTemplate('{{ logic }}', vars), /Identifier "logic" does not evaluate to a string/);
assert.throws(() => generateHTMLFromTemplate('{{ if fn }}no{{endif}}', vars), /Identifier "fn" does not evaluate to a string/);
assert.throws(() => generateHTMLFromTemplate('{{ if {{ fn foo="bar" }} }}no{{endif}}', vars), /Identifier "fn" does not evaluate to a string/);
});
it("should support nested 'if' statements", function () {
var vars = {
v1: '1',
v2: 'H',
};
var template = `{{if v1 == '1'}}yes1{{if x}}no{{elseif v2 == "h" }}yes2{{endif}}{{elseif v2 == "2"}}no{{else}}no{{endif}} {{if v2 == "1"}}not{{elseif x}}not{{else}}yes3{{ endif}}`;
var html = generateHTMLFromTemplate(template, vars);
assert.equal(html, 'yes1yes2 yes3');
});
it("should render a string literal", function () {
assert.equal(generateHTMLFromTemplate('{{ "literal" }}', {}), 'literal');
assert.equal(generateHTMLFromTemplate("{{ 'literal' }}", {}), 'literal');
});
it("should render brace characters via string literals", function () {
assert.equal(generateHTMLFromTemplate('{{ "{" }}', {}), '{');
assert.equal(generateHTMLFromTemplate('{{ "{{" }}', {}), '{{');
assert.equal(generateHTMLFromTemplate('{{ "}}" }}', {}), '}}');
assert.equal(generateHTMLFromTemplate('{{ "}" }}', {}), '}');
});
it("should render string literals alongside text and variables", function () {
const vars = { v1: 'X' };
assert.equal(generateHTMLFromTemplate('a{{ "{{" }}{{v1}}{{ "}}" }}c', vars), 'a{{X}}c');
});
it("should render a string literal that spans multiple lines", function () {
assert.equal(generateHTMLFromTemplate('{{ "a\nb" }}', {}), 'a\nb');
assert.equal(generateHTMLFromTemplate("{{ 'a\nb' }}", {}), 'a\nb');
});
it("should not treat a quoted keyword as a control statement", function () {
assert.equal(generateHTMLFromTemplate('{{ "if" }}', {}), 'if');
assert.equal(generateHTMLFromTemplate('{{ "endif" }}', {}), 'endif');
});
it("should not treat a malformed quoted statement as a string literal", function () {
assert.equal(generateHTMLFromTemplate('{{ "x" y="z" }}', {}), '');
assert.equal(generateHTMLFromTemplate('{{ "a" "b" }}', {}), '');
// A `"` inside a value closes the string, so an embedded quote makes the statement malformed
assert.equal(generateHTMLFromTemplate('{{ "a\\"b" }}', {}), '');
});
it("should treat an empty or bare string-literal condition as false, forcing {{else}}", function () {
assert.equal(generateHTMLFromTemplate('{{if}}Y{{else}}N{{endif}}', {}), 'N');
assert.equal(generateHTMLFromTemplate('{{if "foo"}}Y{{else}}N{{endif}}', {}), 'N');
});
it("should treat any whitespace after a control keyword as a separator", function () {
// split(' ') only splits on an ASCII space; a tab/NBSP/newline after `if` must still
// parse as the `if` keyword rather than a mangled `if\ttitle` identifier
for (let ws of ['\t', ' ', '\n']) {
let tmpl = `{{if${ws}v}}KEEP{{endif}}`;
assert.equal(generateHTMLFromTemplate(tmpl, { v: '1' }), 'KEEP', JSON.stringify(ws));
assert.equal(generateHTMLFromTemplate(tmpl, {}), '', JSON.stringify(ws));
}
});
it("should treat an empty {{}} comparison operand as an empty value instead of throwing", function () {
const vars = { color: 'red', empty: '' };
// Empty operand on either side of any comparator must not crash the engine
assert.equal(generateHTMLFromTemplate('{{if {{}} == color}}Y{{else}}N{{endif}}', vars), 'N');
assert.equal(generateHTMLFromTemplate('{{if color == {{}}}}Y{{else}}N{{endif}}', vars), 'N');
assert.equal(generateHTMLFromTemplate('{{if {{}} != color}}Y{{else}}N{{endif}}', vars), 'Y');
// An empty operand compares equal to an empty variable
assert.equal(generateHTMLFromTemplate('{{if {{}} == empty}}Y{{else}}N{{endif}}', vars), 'Y');
});
it("should not treat a }} or comparator inside a nested operand's quoted argument as structure", function () {
const vars = { echo: attrs => attrs.x ?? '' };
// The `}}` and `==` inside x="…" must not be read as the operand end or the comparator
assert.equal(
generateHTMLFromTemplate('{{if {{ echo x="}} ==" }} == "}} =="}}Y{{else}}N{{endif}}', vars),
'Y'
);
// The same nested call compared with a non-matching literal takes the else branch
assert.equal(
generateHTMLFromTemplate('{{if {{ echo x="}} ==" }} == "other"}}Y{{else}}N{{endif}}', vars),
'N'
);
});
it("should evaluate a nested {{ }} string literal to its contents, like at the top level", function () {
// `{{ "x" }}` renders `x` at the top level, so as a nested operand it must evaluate to `x`
// (not be treated as a missing identifier), so the comparison behaves consistently
assert.equal(generateHTMLFromTemplate('{{if {{ "x" }} == "x"}}Y{{else}}N{{endif}}', {}), 'Y');
assert.equal(generateHTMLFromTemplate("{{if {{ 'x' }} == 'x'}}Y{{else}}N{{endif}}", {}), 'Y');
// ...on either side of the comparator
assert.equal(generateHTMLFromTemplate('{{if "x" == {{ "x" }}}}Y{{else}}N{{endif}}', {}), 'Y');
// A nested literal used directly as a condition is evaluated too: non-empty is truthy,
// empty is falsy (a bare, unbracketed literal remains an identifier ref -- see the
// "bare string-literal condition as false" test above)
assert.equal(generateHTMLFromTemplate('{{if {{ "foo" }}}}Y{{else}}N{{endif}}', {}), 'Y');
assert.equal(generateHTMLFromTemplate('{{if {{ "" }}}}Y{{else}}N{{endif}}', {}), 'N');
});
});
describe("#parseTemplateBrackets()", function () {
it("should split text and bracketed statements", function () {
assert.deepEqual(parseTemplateBrackets('a{{b}}c'), ['a', '{{b}}', 'c']);
});
it("should return a single part for text without brackets", function () {
assert.deepEqual(parseTemplateBrackets('abc'), ['abc']);
});
it("should keep nested brackets within a single outer part", function () {
assert.deepEqual(
parseTemplateBrackets('{{ if {{ x }} }}'),
['', '{{ if {{ x }} }}']
);
});
it("should treat braces inside string literals as literal characters", function () {
assert.deepEqual(parseTemplateBrackets('{{ "{{" }}'), ['', '{{ "{{" }}']);
assert.deepEqual(parseTemplateBrackets('{{ "}}" }}'), ['', '{{ "}}" }}']);
});
it("should not close a statement early when a nested {{}} is adjacent to the outer }}", function () {
// The trailing `}}}}` must be read as the inner `}}` then the outer `}}`, not overlapping
assert.deepEqual(
parseTemplateBrackets('{{if color == {{}}}}'),
['', '{{if color == {{}}}}']
);
assert.deepEqual(
parseTemplateBrackets('{{if {{}}}}X{{endif}}'),
['', '{{if {{}}}}', 'X', '{{endif}}']
);
});
it("should treat `}}` inside a string literal as an ordinary character, not a delimiter", function () {
assert.deepEqual(
parseTemplateBrackets('{{ "a}}b" }}X'),
['', '{{ "a}}b" }}', 'X']
);
assert.deepEqual(
parseTemplateBrackets('{{ title x="}}" }}TAIL'),
['', '{{ title x="}}" }}', 'TAIL']
);
});
it("should not let a trailing backslash before a closing quote swallow a later statement", function () {
// A literal-backslash suffix followed by another quoted statement must stay two
// statements; the `\` before the closing quote must not pair with the later quote
assert.deepEqual(
parseTemplateBrackets('{{ title suffix="\\" }}{{ title prefix="x" }}'),
['', '{{ title suffix="\\" }}', '', '{{ title prefix="x" }}']
);
assert.isTrue(isTemplateValid('{{ title suffix="\\" }}{{ title prefix="x" }}'));
});
});
describe("#isTemplateValid()", function () {
it("should return false for a template with mismatched if/endif", function () {
// The stray {{endif}} that used to crash the engine (see #generateHTMLFromTemplate() tests)
const template = 'foo{{endif}}bar{{if v1 == "1"}}baz{{endif}}';
assert.isFalse(isTemplateValid(template));
});
it("should return false for an {{if}} without a matching {{endif}}", function () {
assert.isFalse(isTemplateValid('{{if v1 == "1"}}baz'));
});
it("should return true for balanced if/endif", function () {
assert.isTrue(isTemplateValid('foo{{if v1 == "1"}}baz{{endif}}'));
});
it("should treat any whitespace after a control keyword as a separator", function () {
// Must agree with the engine: a tab/newline after `if` still parses as the `if` keyword,
// so the block is balanced rather than being read as a mangled `if\ttitle` identifier
for (let ws of ['\t', ' ', '\n']) {
assert.isTrue(isTemplateValid(`{{if${ws}v}}KEEP{{endif}}`), JSON.stringify(ws));
}
});
it("should return false for a non-string template", function () {
assert.isFalse(isTemplateValid(null));
assert.isFalse(isTemplateValid(undefined));
assert.isFalse(isTemplateValid(42));
});
it("should return false for an unmatched opening {{", function () {
assert.isFalse(isTemplateValid('{{ title'));
assert.isFalse(isTemplateValid('foo {{ title }} bar {{ baz'));
});
it("should return false for a stray, non-literal }}", function () {
assert.isFalse(isTemplateValid('title }}'));
assert.isFalse(isTemplateValid('foo }} bar'));
});
it("should treat braces inside string literals as valid", function () {
assert.isTrue(isTemplateValid('{{ "{{" }}'));
assert.isTrue(isTemplateValid('{{ "}}" }}'));
assert.isTrue(isTemplateValid('a{{ "{{" }}b{{ "}}" }}c'));
});
it("should return false for a malformed quoted statement", function () {
assert.isFalse(isTemplateValid('{{ "x" y="z" }}'));
assert.isFalse(isTemplateValid('{{ "a" "b" }}'));
});
it("should return false for a statement with an unterminated quote", function () {
assert.isFalse(isTemplateValid('{{ title truncate="50 }}'));
assert.isFalse(isTemplateValid("{{ title truncate='50 }}"));
assert.isFalse(isTemplateValid('{{if title == "x }}a{{endif}}'));
});
it("should reject an unclosed {{ that a nested }} makes look closed", function () {
// parseTemplateBrackets folds an unclosed outer `{{` and its nested `}}` into one part
// that ends in `}}`; without a balance check the engine would keep it and silently
// drop `year` at render time
assert.isFalse(isTemplateValid('{{ firstCreator {{ year }}'));
assert.isFalse(isTemplateValid('foo {{ bar {{ baz }}'));
});
it("should accept a balanced nested {{ }} operand", function () {
assert.isTrue(isTemplateValid('{{if {{ authorsCount }} == "2"}}a{{endif}}'));
});
it("should return false when an unterminated quote swallows a following statement", function () {
// The unterminated quote makes the tokenizer fold `{{ year }}` into one part, which
// would otherwise validate as a lone statement and silently drop `year` at render time
assert.isFalse(isTemplateValid('{{ title x="a }}{{ year }}'));
});
it("should return true for a well-formed string literal statement", function () {
assert.isTrue(isTemplateValid('{{ "literal" }}'));
assert.isTrue(isTemplateValid('{{ "a}}b" }}'));
});
it("should accept a string literal that spans multiple lines", function () {
assert.isTrue(isTemplateValid('{{ "a\nb" }}'));
assert.isTrue(isTemplateValid("{{ 'a\nb' }}"));
});
it("should reject an attribute value with an unescaped closing quote", function () {
// Backslashes are literal, so a `"` always closes the value; `x="a\""` leaves a stray
// quote that never closes the statement
assert.isFalse(isTemplateValid('{{ title x="a\\"" }}TAIL'));
});
it("should allow single brace characters in text", function () {
assert.isTrue(isTemplateValid('a { b } c'));
});
it("should return false for an {{elseif}} after an {{else}}", function () {
// Renders two branches in the engine (e.g. "ac"), so reject it
assert.isFalse(isTemplateValid('{{if title}}a{{else}}b{{elseif year}}c{{endif}}'));
});
it("should return false for a second {{else}} in the same block", function () {
assert.isFalse(isTemplateValid('{{if title}}a{{else}}b{{else}}c{{endif}}'));
});
it("should accept an {{if}} with an empty condition", function () {
// An empty condition renders as false (forcing the {{else}} branch), which the engine
// has always handled; keep it valid so existing templates don't fall back to the default
assert.isTrue(isTemplateValid('{{if}}a{{endif}}'));
assert.isTrue(isTemplateValid('{{if }}a{{endif}}'));
assert.isTrue(isTemplateValid('{{if}}a{{else}}b{{endif}}'));
});
it("should accept an {{elseif}} with an empty condition", function () {
assert.isTrue(isTemplateValid('{{if title}}a{{elseif}}b{{endif}}'));
});
it("should return true for a valid if/elseif/else chain", function () {
assert.isTrue(isTemplateValid('{{if a}}1{{elseif b}}2{{else}}3{{endif}}'));
assert.isTrue(isTemplateValid('{{if a}}1{{if b}}2{{else}}3{{endif}}{{else}}4{{endif}}'));
});
});
});