mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Some checks are pending
- Fix multiple potential scenarios causing a template engine crash
- Add support for specifying string literals in the template engine
- Validate `if/else/elseif` order and matching clause closures
- Validate to ensure every `{{` is properly closed with a matching `}}`
- When a template is invalid, display a warning, do not offer batch-renaming tools, do not update synced setting
- When a template is invalid, prompt the user to fix or reset the template when
Closes #5965
545 lines
23 KiB
JavaScript
545 lines
23 KiB
JavaScript
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}}'));
|
||
});
|
||
});
|
||
});
|