mirror of
https://github.com/zotero/zotero.git
synced 2026-10-09 03:18:01 +00:00
Move templating engine to a standalone, lazy-loaded module (#5989)
This commit is contained in:
parent
dc6d55a137
commit
efcfaec5d4
6 changed files with 497 additions and 448 deletions
250
chrome/content/zotero/modules/templates.mjs
Normal file
250
chrome/content/zotero/modules/templates.mjs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2026 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
/**
|
||||
* Splits a string by outer-most brackets (`{{` and '}}' by default, configurable).
|
||||
*
|
||||
* @param {string} input - The input string to split.
|
||||
* @returns {string[]} An array of strings split by outer-most brackets.
|
||||
*/
|
||||
export function splitByOuterBrackets(input, left = '{{', right = '}}') {
|
||||
const result = [];
|
||||
let startIndex = 0;
|
||||
let depth = 0;
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
if (input.slice(i, i + 2) === left) {
|
||||
if (depth === 0) {
|
||||
result.push(input.slice(startIndex, i));
|
||||
startIndex = i;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
else if (input.slice(i, i + 2) === right) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
result.push(input.slice(startIndex, i + 2));
|
||||
startIndex = i + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex < input.length) {
|
||||
result.push(input.slice(startIndex));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic templating engine
|
||||
*
|
||||
* - 'if' statement does case-insensitive string comparison
|
||||
* - functions can be called from if statements but must be wrapped in {{}} if arguments are passed (e.g. {{myFunction arg1="foo" arg2="bar"}})
|
||||
*
|
||||
* Vars example:
|
||||
* {
|
||||
* color: '#ff6666',
|
||||
* highlight: '<span class="highlight">This is a highlight</span>,
|
||||
* comment: 'This is a comment',
|
||||
* citation: '<span class="citation">(Author, 1900)</citation>',
|
||||
* image: '<img src="…"/>',
|
||||
* tags: (attrs) => ['tag1', 'tag2'].map(tag => tag.name).join(attrs.join || ' ')
|
||||
* }
|
||||
*
|
||||
* Template example:
|
||||
* {{if color == '#ff6666'}}
|
||||
* <h2>{{highlight}}</h2>
|
||||
* {{elseif color == '#2ea8e5'}}
|
||||
* {{if comment}}<p>{{comment}}:</p>{{endif}}<blockquote>{{highlight}}</blockquote><p>{{citation}}</p>
|
||||
* {{else}}
|
||||
* <p>{{highlight}} {{citation}} {{comment}} {{if tags}} #{{tags join=' #'}}{{endif}}</p>
|
||||
* {{endif}}
|
||||
*
|
||||
* @param {String} template
|
||||
* @param {Object} vars
|
||||
* @returns {String} HTML
|
||||
*/
|
||||
export function generateHTMLFromTemplate(template, vars) {
|
||||
const hyphenToCamel = varName => varName.replace(/-(.)/g, (_, g1) => g1.toUpperCase());
|
||||
|
||||
const getAttributes = (part) => {
|
||||
let attrsRegexp = new RegExp(/(([\w-]*) *=+ *(['"])((\\\3|[^\3])*?)\3)/g);
|
||||
let attrs = {};
|
||||
let match;
|
||||
while ((match = attrsRegexp.exec(part))) {
|
||||
if (match[1]) { // if first alternative (i.e. argument with value wrapped in " or ') matched, even if value is empty
|
||||
attrs[hyphenToCamel(match[2])] = match[4];
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
|
||||
|
||||
const evaluateIdentifier = (ident, args = {}) => {
|
||||
ident = hyphenToCamel(ident);
|
||||
|
||||
if (!(ident in vars)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const identValue = typeof vars[ident] === 'function' ? vars[ident](args) : vars[ident];
|
||||
|
||||
if (Array.isArray(identValue)) {
|
||||
return identValue.length ? identValue.join(',') : '';
|
||||
}
|
||||
|
||||
if (typeof identValue !== 'string') {
|
||||
throw new Error(`Identifier "${ident}" does not evaluate to a string`);
|
||||
}
|
||||
|
||||
return identValue;
|
||||
};
|
||||
|
||||
// evaluates extracted (i.e. without brackets) statement (e.g. `sum a="1" b="2"`) into a string value
|
||||
const evaluateStatement = (statement) => {
|
||||
statement = statement.trim();
|
||||
const operator = statement.split(' ', 1)[0].trim();
|
||||
const args = statement.slice(operator.length).trim();
|
||||
|
||||
return evaluateIdentifier(operator, getAttributes(args));
|
||||
};
|
||||
|
||||
// splits raw (i.e. bracketed) statement (e.g. `{{ sum a="1" b="2" }}) into operator and arguments (e.g. ['sum', 'a="1" b="2"'])
|
||||
const splitStatement = (statement) => {
|
||||
statement = statement.slice(2, -2).trim();
|
||||
const operator = statement.split(' ', 1)[0].trim();
|
||||
const args = statement.slice(operator.length).trim();
|
||||
return [operator, args];
|
||||
};
|
||||
|
||||
// We allow unquoted numbers in conditions, e.g. `a == 1` or `a == 1.0` but not `a == 1.0.0` or `a == 1st edition`
|
||||
const asNumber = (string) => {
|
||||
if (typeof string === 'number') {
|
||||
return string;
|
||||
}
|
||||
const number = parseFloat(string);
|
||||
if (!Number.isNaN(number) && string?.trim().match(/^[+-]?\d+(\.\d+)?$/)) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// evaluates a condition (e.g. `a == "b"`) into a boolean value
|
||||
const evaluateCondition = (condition) => {
|
||||
const comparators = ['==', '!=', "<=", ">=", '<', '>'];
|
||||
condition = condition.trim();
|
||||
|
||||
// Regular expression breakdown for condition matching:
|
||||
// - `match[1]`: Left operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[3]`: Left operand if it's a string literal, extracted without quotes.
|
||||
// - `match[4]`: Left operand if it's a standalone identifier (e.g., a variable) or a number
|
||||
// - `match[6]`: Right operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[8]`: Right operand if it's a string literal, extracted without quotes.
|
||||
// - `match[9]`: Right operand if it's a standalone identifier or a number.
|
||||
// - `match[2]` and `match[7]`: Captured quotes around string literals, used to ensure matching pairs.
|
||||
// - `match[5]`: The comparator (e.g., `==`, `!=`, `<`, `>`, etc.), extracted from `comparators.join('|')`.
|
||||
const match = condition.match(new RegExp(String.raw`(?:{{(.*?)}}|(?:(['"])(.*?)\2)|([^ ]+)) *(${comparators.join('|')}) *(?:{{(.*?)}}|(?:(['"])(.*?)\7)|([^ ]+))`));
|
||||
|
||||
if (!match) {
|
||||
// condition is a statement or identifier without a comparator
|
||||
if (condition.startsWith('{{')) {
|
||||
const [operator, args] = splitStatement(condition);
|
||||
return !!evaluateIdentifier(operator, getAttributes(args));
|
||||
}
|
||||
return !!evaluateIdentifier(condition);
|
||||
}
|
||||
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? asNumber(match[4]) ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const comparator = match[5];
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? asNumber(match[9]) ?? evaluateIdentifier(match[9]) ?? '';
|
||||
|
||||
switch (comparator) {
|
||||
default:
|
||||
case '==':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() == right.toLowerCase() : asNumber(left) == asNumber(right);
|
||||
case '!=':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() != right.toLowerCase() : asNumber(left) != asNumber(right);
|
||||
case ">=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() >= right.toLowerCase() : asNumber(left) >= asNumber(right);
|
||||
case "<=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() <= right.toLowerCase() : asNumber(left) <= asNumber(right);
|
||||
case '>':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() > right.toLowerCase() : asNumber(left) > asNumber(right);
|
||||
case '<':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() < right.toLowerCase() : asNumber(left) < asNumber(right);
|
||||
}
|
||||
};
|
||||
|
||||
let html = '';
|
||||
const levels = [{ condition: true }];
|
||||
const parts = splitByOuterBrackets(template);
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let part = parts[i];
|
||||
let level = levels[levels.length - 1];
|
||||
|
||||
if (part.startsWith('{{')) {
|
||||
const [operator, args] = splitStatement(part);
|
||||
|
||||
if (operator === 'if') {
|
||||
level = { condition: false, executed: false, parentCondition: levels[levels.length - 1].condition };
|
||||
levels.push(level);
|
||||
}
|
||||
if (['if', 'elseif'].includes(operator)) {
|
||||
if (!level.executed) {
|
||||
level.condition = level.parentCondition && evaluateCondition(args);
|
||||
level.executed = level.condition;
|
||||
}
|
||||
else {
|
||||
level.condition = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (operator === 'else') {
|
||||
level.condition = level.parentCondition && !level.executed;
|
||||
level.executed = level.condition;
|
||||
continue;
|
||||
}
|
||||
else if (operator === 'endif') {
|
||||
// Don't pop the base level for an unbalanced {{endif}} (i.e. one without a matching
|
||||
// {{if}}), which would leave `levels` empty and cause `level` to be undefined on the
|
||||
// next iteration
|
||||
if (levels.length > 1) {
|
||||
levels.pop();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (level.condition) {
|
||||
const attrs = getAttributes(part);
|
||||
html += evaluateIdentifier(operator, attrs);
|
||||
}
|
||||
}
|
||||
else if (level.condition) {
|
||||
html += part;
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
|
@ -25,6 +25,10 @@
|
|||
|
||||
Zotero.Attachments = new function () {
|
||||
const { HiddenBrowser } = ChromeUtils.importESModule("chrome://zotero/content/HiddenBrowser.mjs");
|
||||
let lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
generateHTMLFromTemplate: "chrome://zotero/content/modules/templates.mjs",
|
||||
});
|
||||
|
||||
// Keep in sync with Zotero.Schema.integrityCheck() and this.linkModeToName()
|
||||
this.LINK_MODE_IMPORTED_FILE = 0;
|
||||
|
|
@ -2548,7 +2552,7 @@ Zotero.Attachments = new function () {
|
|||
// Final name is generated twice. In the first pass we collect all affixed values and determine protected literals.
|
||||
// This is done in order to remove repeated suffixes, except if these appear in the value or the format string itself.
|
||||
// See "should suppress suffixes where they would create a repeat character" test for edge cases.
|
||||
let formatted = Zotero.Utilities.Internal.generateHTMLFromTemplate(formatString, vars);
|
||||
let formatted = lazy.generateHTMLFromTemplate(formatString, vars);
|
||||
|
||||
let replacePairs = new Map();
|
||||
for (let chunk of chunks) {
|
||||
|
|
@ -2571,7 +2575,7 @@ Zotero.Attachments = new function () {
|
|||
);
|
||||
}
|
||||
|
||||
formatted = Zotero.Utilities.Internal.generateHTMLFromTemplate(formatString, vars);
|
||||
formatted = lazy.generateHTMLFromTemplate(formatString, vars);
|
||||
if (replacePairs.size > 0) {
|
||||
formatted = formatted.replace(
|
||||
new RegExp(`(${Array.from(replacePairs.keys()).map(replace => `(?<!\\\\)${replace}(?!//)`).join('|')})`, 'g'),
|
||||
|
|
|
|||
|
|
@ -23,9 +23,15 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var { InlineSpellChecker } = ChromeUtils.importESModule("resource://gre/modules/InlineSpellChecker.sys.mjs");
|
||||
var { FilePicker } = ChromeUtils.importESModule('chrome://zotero/content/modules/filePicker.mjs');
|
||||
|
||||
let lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
generateHTMLFromTemplate: "chrome://zotero/content/modules/templates.mjs",
|
||||
});
|
||||
|
||||
// Note: TinyMCE is automatically doing some meaningless corrections to
|
||||
// note-editor produced HTML. Which might result to more
|
||||
// conflicts, especially in group libraries
|
||||
|
|
@ -1495,7 +1501,7 @@ class EditorInstance {
|
|||
title: Zotero.getString('reader-annotations'),
|
||||
date: new Date().toLocaleString()
|
||||
};
|
||||
html = Zotero.Utilities.Internal.generateHTMLFromTemplate(Zotero.Prefs.get('annotations.noteTemplates.title'), vars);
|
||||
html = lazy.generateHTMLFromTemplate(Zotero.Prefs.get('annotations.noteTemplates.title'), vars);
|
||||
// New line is needed for note title parser
|
||||
html += '\n';
|
||||
}
|
||||
|
|
@ -1730,7 +1736,7 @@ class EditorInstanceUtilities {
|
|||
tags: (attrs) => (annotation.tags && annotation.tags.map(tag => tag.name) || []).join(attrs.join || ' ')
|
||||
};
|
||||
|
||||
let templateHTML = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
let templateHTML = lazy.generateHTMLFromTemplate(template, vars);
|
||||
// Remove some spaces at the end of paragraph
|
||||
templateHTML = templateHTML.replace(/([\s]*)(<\/p)/g, '$2');
|
||||
// Remove multiple spaces
|
||||
|
|
@ -1890,3 +1896,4 @@ class EditorInstanceUtilities {
|
|||
|
||||
Zotero.EditorInstance = EditorInstance;
|
||||
Zotero.EditorInstanceUtilities = new EditorInstanceUtilities();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2201,229 +2201,12 @@ Zotero.Utilities.Internal = {
|
|||
|
||||
|
||||
/**
|
||||
* Splits a string by outer-most brackets (`{{` and '}}' by default, configurable).
|
||||
*
|
||||
* @param {string} input - The input string to split.
|
||||
* @returns {string[]} An array of strings split by outer-most brackets.
|
||||
*/
|
||||
splitByOuterBrackets: function (input, left = '{{', right = '}}') {
|
||||
const result = [];
|
||||
let startIndex = 0;
|
||||
let depth = 0;
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
if (input.slice(i, i + 2) === left) {
|
||||
if (depth === 0) {
|
||||
result.push(input.slice(startIndex, i));
|
||||
startIndex = i;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
else if (input.slice(i, i + 2) === right) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
result.push(input.slice(startIndex, i + 2));
|
||||
startIndex = i + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex < input.length) {
|
||||
result.push(input.slice(startIndex));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* A basic templating engine
|
||||
*
|
||||
* - 'if' statement does case-insensitive string comparison
|
||||
* - functions can be called from if statements but must be wrapped in {{}} if arguments are passed (e.g. {{myFunction arg1="foo" arg2="bar"}})
|
||||
*
|
||||
* Vars example:
|
||||
* {
|
||||
* color: '#ff6666',
|
||||
* highlight: '<span class="highlight">This is a highlight</span>,
|
||||
* comment: 'This is a comment',
|
||||
* citation: '<span class="citation">(Author, 1900)</citation>',
|
||||
* image: '<img src="…"/>',
|
||||
* tags: (attrs) => ['tag1', 'tag2'].map(tag => tag.name).join(attrs.join || ' ')
|
||||
* }
|
||||
*
|
||||
* Template example:
|
||||
* {{if color == '#ff6666'}}
|
||||
* <h2>{{highlight}}</h2>
|
||||
* {{elseif color == '#2ea8e5'}}
|
||||
* {{if comment}}<p>{{comment}}:</p>{{endif}}<blockquote>{{highlight}}</blockquote><p>{{citation}}</p>
|
||||
* {{else}}
|
||||
* <p>{{highlight}} {{citation}} {{comment}} {{if tags}} #{{tags join=' #'}}{{endif}}</p>
|
||||
* {{endif}}
|
||||
*
|
||||
* @param {String} template
|
||||
* @param {Object} vars
|
||||
* @returns {String} HTML
|
||||
* @deprecated Use generateHTMLFromTemplate() from modules/templates.mjs
|
||||
*/
|
||||
generateHTMLFromTemplate: function (template, vars) {
|
||||
const hyphenToCamel = varName => varName.replace(/-(.)/g, (_, g1) => g1.toUpperCase());
|
||||
|
||||
const getAttributes = (part) => {
|
||||
let attrsRegexp = new RegExp(/(([\w-]*) *=+ *(['"])((\\\3|[^\3])*?)\3)/g);
|
||||
let attrs = {};
|
||||
let match;
|
||||
while ((match = attrsRegexp.exec(part))) {
|
||||
if (match[1]) { // if first alternative (i.e. argument with value wrapped in " or ') matched, even if value is empty
|
||||
attrs[hyphenToCamel(match[2])] = match[4];
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
|
||||
|
||||
const evaluateIdentifier = (ident, args = {}) => {
|
||||
ident = hyphenToCamel(ident);
|
||||
|
||||
if (!(ident in vars)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const identValue = typeof vars[ident] === 'function' ? vars[ident](args) : vars[ident];
|
||||
|
||||
if (Array.isArray(identValue)) {
|
||||
return identValue.length ? identValue.join(',') : '';
|
||||
}
|
||||
|
||||
if (typeof identValue !== 'string') {
|
||||
throw new Error(`Identifier "${ident}" does not evaluate to a string`);
|
||||
}
|
||||
|
||||
return identValue;
|
||||
};
|
||||
|
||||
// evaluates extracted (i.e. without brackets) statement (e.g. `sum a="1" b="2"`) into a string value
|
||||
const evaluateStatement = (statement) => {
|
||||
statement = statement.trim();
|
||||
const operator = statement.split(' ', 1)[0].trim();
|
||||
const args = statement.slice(operator.length).trim();
|
||||
|
||||
return evaluateIdentifier(operator, getAttributes(args));
|
||||
};
|
||||
|
||||
// splits raw (i.e. bracketed) statement (e.g. `{{ sum a="1" b="2" }}) into operator and arguments (e.g. ['sum', 'a="1" b="2"'])
|
||||
const splitStatement = (statement) => {
|
||||
statement = statement.slice(2, -2).trim();
|
||||
const operator = statement.split(' ', 1)[0].trim();
|
||||
const args = statement.slice(operator.length).trim();
|
||||
return [operator, args];
|
||||
};
|
||||
|
||||
// We allow unquoted numbers in conditions, e.g. `a == 1` or `a == 1.0` but not `a == 1.0.0` or `a == 1st edition`
|
||||
const asNumber = (string) => {
|
||||
if (typeof string === 'number') {
|
||||
return string;
|
||||
}
|
||||
const number = parseFloat(string);
|
||||
if (!Number.isNaN(number) && string?.trim().match(/^[+-]?\d+(\.\d+)?$/)) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// evaluates a condition (e.g. `a == "b"`) into a boolean value
|
||||
const evaluateCondition = (condition) => {
|
||||
const comparators = ['==', '!=', "<=", ">=", '<', '>'];
|
||||
condition = condition.trim();
|
||||
|
||||
// Regular expression breakdown for condition matching:
|
||||
// - `match[1]`: Left operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[3]`: Left operand if it's a string literal, extracted without quotes.
|
||||
// - `match[4]`: Left operand if it's a standalone identifier (e.g., a variable) or a number
|
||||
// - `match[6]`: Right operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[8]`: Right operand if it's a string literal, extracted without quotes.
|
||||
// - `match[9]`: Right operand if it's a standalone identifier or a number.
|
||||
// - `match[2]` and `match[7]`: Captured quotes around string literals, used to ensure matching pairs.
|
||||
// - `match[5]`: The comparator (e.g., `==`, `!=`, `<`, `>`, etc.), extracted from `comparators.join('|')`.
|
||||
const match = condition.match(new RegExp(String.raw`(?:{{(.*?)}}|(?:(['"])(.*?)\2)|([^ ]+)) *(${comparators.join('|')}) *(?:{{(.*?)}}|(?:(['"])(.*?)\7)|([^ ]+))`));
|
||||
|
||||
if (!match) {
|
||||
// condition is a statement or identifier without a comparator
|
||||
if (condition.startsWith('{{')) {
|
||||
const [operator, args] = splitStatement(condition);
|
||||
return !!evaluateIdentifier(operator, getAttributes(args));
|
||||
}
|
||||
return !!evaluateIdentifier(condition);
|
||||
}
|
||||
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? asNumber(match[4]) ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const comparator = match[5];
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? asNumber(match[9]) ?? evaluateIdentifier(match[9]) ?? '';
|
||||
|
||||
switch (comparator) {
|
||||
default:
|
||||
case '==':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() == right.toLowerCase() : asNumber(left) == asNumber(right);
|
||||
case '!=':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() != right.toLowerCase() : asNumber(left) != asNumber(right);
|
||||
case ">=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() >= right.toLowerCase() : asNumber(left) >= asNumber(right);
|
||||
case "<=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() <= right.toLowerCase() : asNumber(left) <= asNumber(right);
|
||||
case '>':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() > right.toLowerCase() : asNumber(left) > asNumber(right);
|
||||
case '<':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() < right.toLowerCase() : asNumber(left) < asNumber(right);
|
||||
}
|
||||
};
|
||||
|
||||
let html = '';
|
||||
const levels = [{ condition: true }];
|
||||
const parts = this.splitByOuterBrackets(template);
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let part = parts[i];
|
||||
let level = levels[levels.length - 1];
|
||||
|
||||
if (part.startsWith('{{')) {
|
||||
const [operator, args] = splitStatement(part);
|
||||
|
||||
if (operator === 'if') {
|
||||
level = { condition: false, executed: false, parentCondition: levels[levels.length - 1].condition };
|
||||
levels.push(level);
|
||||
}
|
||||
if (['if', 'elseif'].includes(operator)) {
|
||||
if (!level.executed) {
|
||||
level.condition = level.parentCondition && evaluateCondition(args);
|
||||
level.executed = level.condition;
|
||||
}
|
||||
else {
|
||||
level.condition = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (operator === 'else') {
|
||||
level.condition = level.parentCondition && !level.executed;
|
||||
level.executed = level.condition;
|
||||
continue;
|
||||
}
|
||||
else if (operator === 'endif') {
|
||||
// Don't pop the base level for an unbalanced {{endif}} (i.e. one without a matching
|
||||
// {{if}}), which would leave `levels` empty and cause `level` to be undefined on the
|
||||
// next iteration
|
||||
if (levels.length > 1) {
|
||||
levels.pop();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (level.condition) {
|
||||
const attrs = getAttributes(part);
|
||||
html += evaluateIdentifier(operator, attrs);
|
||||
}
|
||||
}
|
||||
else if (level.condition) {
|
||||
html += part;
|
||||
}
|
||||
}
|
||||
return html;
|
||||
Zotero.debug("Zotero.Utilities.Internal.generateHTMLFromTemplate() is deprecated -- use modules/templates.mjs");
|
||||
let { generateHTMLFromTemplate } = ChromeUtils.importESModule("chrome://zotero/content/modules/templates.mjs");
|
||||
return generateHTMLFromTemplate(template, vars);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
228
test/tests/templatesTest.js
Normal file
228
test/tests/templatesTest.js
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
var {
|
||||
generateHTMLFromTemplate,
|
||||
} = 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 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 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -581,229 +581,6 @@ describe("Zotero.Utilities.Internal", 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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
assert.equal(out, '"" has a length of 0 and "hello" has a length of 5');
|
||||
});
|
||||
|
||||
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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
assert.equal(out, '1 + 2 = 3');
|
||||
|
||||
const template2 = '{{if false != "false"}}no{{elseif false == "false"}}yes{{else}}no{{endif}}';
|
||||
const out2 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template2, vars);
|
||||
assert.equal(out2, 'yes');
|
||||
|
||||
const template3 = '{{ if twoWords == "two words" }}yes{{else}}no{{endif}}';
|
||||
const out3 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template3, vars);
|
||||
assert.equal(out3, 'yes');
|
||||
|
||||
const template4 = '{{ if onlyOne == \'actually == 1\' }}yes{{else}}no{{endif}}';
|
||||
const out4 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template4, vars);
|
||||
assert.equal(out4, 'yes');
|
||||
|
||||
const template5 = '{{ if "3" == {{ sum a="1" b="2" }} }}yes{{else}}no{{endif}}';
|
||||
const out5 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template5, vars);
|
||||
assert.equal(out5, 'yes');
|
||||
|
||||
const template6 = '{{ if {{ sum a="1" b="2" }} }}yes{{else}}no{{endif}}';
|
||||
const out6 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template6, vars);
|
||||
assert.equal(out6, 'yes');
|
||||
|
||||
const template7 = '{{ if {{ twoWords }} }}yes{{else}}no{{endif}}';
|
||||
const out7 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template7, vars);
|
||||
assert.equal(out7, 'yes');
|
||||
|
||||
const template8 = '{{ if twoWords }}yes{{else}}no{{endif}}';
|
||||
const out8 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template8, vars);
|
||||
assert.equal(out8, 'yes');
|
||||
|
||||
const template9 = '{{ if missing }}no{{else}}yes{{endif}}';
|
||||
const out9 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template9, vars);
|
||||
assert.equal(out9, 'yes');
|
||||
|
||||
const template10 = '{{ if {{ missing foo="bar" }} }}no{{else}}yes{{endif}}';
|
||||
const out10 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template10, vars);
|
||||
assert.equal(out10, 'yes');
|
||||
|
||||
const template11 = '{{ if {{ missing foo="bar" }} == "" }}yes{{else}}no{{endif}}';
|
||||
const out11 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template11, vars);
|
||||
assert.equal(out11, 'yes');
|
||||
|
||||
const template12 = '{{ if fooBar == "bar" }}yes{{else}}no{{endif}}';
|
||||
const out12 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template12, vars);
|
||||
assert.equal(out12, 'yes');
|
||||
|
||||
const template13 = '{{ if {{ fooBar }} == "bar" }}yes{{else}}no{{endif}}';
|
||||
const out13 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template14, vars);
|
||||
assert.equal(out14, '1 + 2 = 3');
|
||||
|
||||
const template15 = `{{if "two words"==twoWords}}yes{{else}}no{{endif}}`;
|
||||
const out15 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template2, vars);
|
||||
assert.equal(out2, 'more than π');
|
||||
|
||||
const template3 = `{{if 3.14 >= π}}more than or equal to π{{else}}less than π{{endif}}`;
|
||||
const out3 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template5, vars);
|
||||
assert.equal(out5, '"test" > "foo"');
|
||||
|
||||
const template6 = `{{if "bar" < v2 }}"bar" < "foo"{{else}}no{{endif}}`;
|
||||
const out6 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template6, vars);
|
||||
assert.equal(out6, '"bar" < "foo"');
|
||||
});
|
||||
|
||||
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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
assert.equal(out, 'foo');
|
||||
|
||||
const template2 = 'test: {{ if bar == "" }}yes{{else}}no{{endif}}';
|
||||
const out2 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, {});
|
||||
assert.equal(out, template);
|
||||
const vars = {
|
||||
space: ' ',
|
||||
spaceFn: () => ' ',
|
||||
};
|
||||
|
||||
const whitespace = ' {{if spaceFn}}{{else}} {{endif}}{{space}} {{space-fn}}';
|
||||
const out2 = Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
assert.equal(out, '#foo #bar');
|
||||
|
||||
someTags = [];
|
||||
const out2 = Zotero.Utilities.Internal.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(() => Zotero.Utilities.Internal.generateHTMLFromTemplate('{{ number }}', vars), /Identifier "number" does not evaluate to a string/);
|
||||
assert.throws(() => Zotero.Utilities.Internal.generateHTMLFromTemplate('{{ logic }}', vars), /Identifier "logic" does not evaluate to a string/);
|
||||
assert.throws(() => Zotero.Utilities.Internal.generateHTMLFromTemplate('{{ if fn }}no{{endif}}', vars), /Identifier "fn" does not evaluate to a string/);
|
||||
assert.throws(() => Zotero.Utilities.Internal.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 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template, vars);
|
||||
assert.equal(html, 'yes1yes2 yes3');
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenURL", function () {
|
||||
var item;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue