liquidjs
Version:
A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.
1,660 lines (1,607 loc) • 96.3 kB
JavaScript
/*
* liquidjs@9.24.2, https://github.com/harttle/liquidjs
* (c) 2016-2021 harttle
* Released under the MIT License.
*/
import { extname, resolve as resolve$1 } from 'path';
import { statSync, readFileSync as readFileSync$1, stat, readFile as readFile$1 } from 'fs';
class Drop {
valueOf() {
return undefined;
}
liquidMethodMissing(key) {
return undefined;
}
}
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
const toStr = Object.prototype.toString;
const toLowerCase = String.prototype.toLowerCase;
/*
* Checks if value is classified as a String primitive or object.
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is a string, else false.
*/
function isString(value) {
return toStr.call(value) === '[object String]';
}
function isFunction(value) {
return typeof value === 'function';
}
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
err ? reject(err) : resolve(result);
});
});
};
}
function stringify(value) {
value = toValue(value);
return isNil(value) ? '' : String(value);
}
function toValue(value) {
return value instanceof Drop ? value.valueOf() : value;
}
function isNumber(value) {
return typeof value === 'number';
}
function toLiquid(value) {
if (value && isFunction(value.toLiquid))
return toLiquid(value.toLiquid());
return value;
}
function isNil(value) {
return value === null || value === undefined;
}
function isArray(value) {
// be compatible with IE 8
return toStr.call(value) === '[object Array]';
}
/*
* Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
* The iteratee is invoked with three arguments: (value, key, object).
* Iteratee functions may exit iteration early by explicitly returning false.
* @param {Object} object The object to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @return {Object} Returns object.
*/
function forOwn(object, iteratee) {
object = object || {};
for (const k in object) {
if (object.hasOwnProperty(k)) {
if (iteratee(object[k], k, object) === false)
break;
}
}
return object;
}
function last(arr) {
return arr[arr.length - 1];
}
/*
* Checks if value is the language type of Object.
* (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
* @param {any} value The value to check.
* @return {Boolean} Returns true if value is an object, else false.
*/
function isObject(value) {
const type = typeof value;
return value !== null && (type === 'object' || type === 'function');
}
function range(start, stop, step = 1) {
const arr = [];
for (let i = start; i < stop; i += step) {
arr.push(i);
}
return arr;
}
function padStart(str, length, ch = ' ') {
return pad(str, length, ch, (str, ch) => ch + str);
}
function padEnd(str, length, ch = ' ') {
return pad(str, length, ch, (str, ch) => str + ch);
}
function pad(str, length, ch, add) {
str = String(str);
let n = length - str.length;
while (n-- > 0)
str = add(str, ch);
return str;
}
function identify(val) {
return val;
}
function snakeCase(str) {
return str.replace(/(\w?)([A-Z])/g, (_, a, b) => (a ? a + '_' : '') + b.toLowerCase());
}
function changeCase(str) {
const hasLowerCase = [...str].some(ch => ch >= 'a' && ch <= 'z');
return hasLowerCase ? str.toUpperCase() : str.toLowerCase();
}
function ellipsis(str, N) {
return str.length > N ? str.substr(0, N - 3) + '...' : str;
}
// compare string in case-insensitive way, undefined values to the tail
function caseInsensitiveCompare(a, b) {
if (a == null && b == null)
return 0;
if (a == null)
return 1;
if (b == null)
return -1;
a = toLowerCase.call(a);
b = toLowerCase.call(b);
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
}
class Node {
constructor(key, value, next, prev) {
this.key = key;
this.value = value;
this.next = next;
this.prev = prev;
}
}
class LRU {
constructor(limit, size = 0) {
this.limit = limit;
this.size = size;
this.cache = {};
this.head = new Node('HEAD', null, null, null);
this.tail = new Node('TAIL', null, null, null);
this.head.next = this.tail;
this.tail.prev = this.head;
}
write(key, value) {
if (this.cache[key]) {
this.cache[key].value = value;
}
else {
const node = new Node(key, value, this.head.next, this.head);
this.head.next.prev = node;
this.head.next = node;
this.cache[key] = node;
this.size++;
this.ensureLimit();
}
}
read(key) {
if (!this.cache[key])
return;
const { value } = this.cache[key];
this.remove(key);
this.write(key, value);
return value;
}
remove(key) {
const node = this.cache[key];
node.prev.next = node.next;
node.next.prev = node.prev;
delete this.cache[key];
this.size--;
}
clear() {
this.head.next = this.tail;
this.tail.prev = this.head;
this.size = 0;
this.cache = {};
}
ensureLimit() {
if (this.size > this.limit)
this.remove(this.tail.prev.key);
}
}
const statAsync = promisify(stat);
const readFileAsync = promisify(readFile$1);
function exists(filepath) {
return statAsync(filepath).then(() => true).catch(() => false);
}
function readFile(filepath) {
return readFileAsync(filepath, 'utf8');
}
function existsSync(filepath) {
try {
statSync(filepath);
return true;
}
catch (err) {
return false;
}
}
function readFileSync(filepath) {
return readFileSync$1(filepath, 'utf8');
}
function resolve(root, file, ext) {
if (!extname(file))
file += ext;
return resolve$1(root, file);
}
function fallback(file) {
try {
return require.resolve(file);
}
catch (e) { }
}
var fs = /*#__PURE__*/Object.freeze({
exists: exists,
readFile: readFile,
existsSync: existsSync,
readFileSync: readFileSync,
resolve: resolve,
fallback: fallback
});
function isComparable(arg) {
return arg && isFunction(arg.equals);
}
function isTruthy(val, ctx) {
return !isFalsy(val, ctx);
}
function isFalsy(val, ctx) {
if (ctx.opts.jsTruthy) {
return !val;
}
else {
return val === false || undefined === val || val === null;
}
}
const defaultOperators = {
'==': (l, r) => {
if (isComparable(l))
return l.equals(r);
if (isComparable(r))
return r.equals(l);
return l === r;
},
'!=': (l, r) => {
if (isComparable(l))
return !l.equals(r);
if (isComparable(r))
return !r.equals(l);
return l !== r;
},
'>': (l, r) => {
if (isComparable(l))
return l.gt(r);
if (isComparable(r))
return r.lt(l);
return l > r;
},
'<': (l, r) => {
if (isComparable(l))
return l.lt(r);
if (isComparable(r))
return r.gt(l);
return l < r;
},
'>=': (l, r) => {
if (isComparable(l))
return l.geq(r);
if (isComparable(r))
return r.leq(l);
return l >= r;
},
'<=': (l, r) => {
if (isComparable(l))
return l.leq(r);
if (isComparable(r))
return r.geq(l);
return l <= r;
},
'contains': (l, r) => {
return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false;
},
'and': (l, r, ctx) => isTruthy(l, ctx) && isTruthy(r, ctx),
'or': (l, r, ctx) => isTruthy(l, ctx) || isTruthy(r, ctx)
};
// **DO NOT CHANGE THIS FILE**
//
// This file is generated by bin/character-gen.js
// bitmask character types to boost performance
const TYPES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 4, 4, 4, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 2, 8, 0, 0, 0, 0, 8, 0, 0, 0, 64, 0, 65, 0, 0, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 0, 0, 2, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0];
const IDENTIFIER = 1;
const BLANK = 4;
const QUOTE = 8;
const INLINE_BLANK = 16;
const NUMBER = 32;
const SIGN = 64;
TYPES[160] = TYPES[5760] = TYPES[6158] = TYPES[8192] = TYPES[8193] = TYPES[8194] = TYPES[8195] = TYPES[8196] = TYPES[8197] = TYPES[8198] = TYPES[8199] = TYPES[8200] = TYPES[8201] = TYPES[8202] = TYPES[8232] = TYPES[8233] = TYPES[8239] = TYPES[8287] = TYPES[12288] = BLANK;
function createTrie(operators) {
const trie = {};
for (const [name, handler] of Object.entries(operators)) {
let node = trie;
for (let i = 0; i < name.length; i++) {
const c = name[i];
node[c] = node[c] || {};
if (i === name.length - 1 && (TYPES[name.charCodeAt(i)] & IDENTIFIER)) {
node[c].needBoundary = true;
}
node = node[c];
}
node.handler = handler;
node.end = true;
}
return trie;
}
const defaultOptions = {
root: ['.'],
cache: undefined,
extname: '',
fs: fs,
dynamicPartials: true,
jsTruthy: false,
trimTagRight: false,
trimTagLeft: false,
trimOutputRight: false,
trimOutputLeft: false,
greedy: true,
tagDelimiterLeft: '{%',
tagDelimiterRight: '%}',
outputDelimiterLeft: '{{',
outputDelimiterRight: '}}',
preserveTimezones: false,
strictFilters: false,
strictVariables: false,
lenientIf: false,
globals: {},
keepOutputType: false,
operators: defaultOperators,
operatorsTrie: createTrie(defaultOperators)
};
function normalize(options) {
options = options || {};
if (options.hasOwnProperty('root')) {
options.root = normalizeStringArray(options.root);
}
if (options.hasOwnProperty('cache')) {
let cache;
if (typeof options.cache === 'number')
cache = options.cache > 0 ? new LRU(options.cache) : undefined;
else if (typeof options.cache === 'object')
cache = options.cache;
else
cache = options.cache ? new LRU(1024) : undefined;
options.cache = cache;
}
if (options.hasOwnProperty('operators')) {
options.operatorsTrie = createTrie(options.operators);
}
return options;
}
function applyDefault(options) {
return Object.assign({}, defaultOptions, options);
}
function normalizeStringArray(value) {
if (isArray(value))
return value;
if (isString(value))
return [value];
return [];
}
class LiquidError extends Error {
constructor(err, token) {
super(err.message);
this.originalError = err;
this.token = token;
this.context = '';
}
update() {
const err = this.originalError;
this.context = mkContext(this.token);
this.message = mkMessage(err.message, this.token);
this.stack = this.message + '\n' + this.context +
'\n' + this.stack + '\nFrom ' + err.stack;
}
}
class TokenizationError extends LiquidError {
constructor(message, token) {
super(new Error(message), token);
this.name = 'TokenizationError';
super.update();
}
}
class ParseError extends LiquidError {
constructor(err, token) {
super(err, token);
this.name = 'ParseError';
this.message = err.message;
super.update();
}
}
class RenderError extends LiquidError {
constructor(err, tpl) {
super(err, tpl.token);
this.name = 'RenderError';
this.message = err.message;
super.update();
}
static is(obj) {
return obj.name === 'RenderError';
}
}
class UndefinedVariableError extends LiquidError {
constructor(err, token) {
super(err, token);
this.name = 'UndefinedVariableError';
this.message = err.message;
super.update();
}
}
// only used internally; raised where we don't have token information,
// so it can't be an UndefinedVariableError.
class InternalUndefinedVariableError extends Error {
constructor(variableName) {
super(`undefined variable: ${variableName}`);
this.name = 'InternalUndefinedVariableError';
this.variableName = variableName;
}
}
class AssertionError extends Error {
constructor(message) {
super(message);
this.name = 'AssertionError';
this.message = message + '';
}
}
function mkContext(token) {
const [line] = token.getPosition();
const lines = token.input.split('\n');
const begin = Math.max(line - 2, 1);
const end = Math.min(line + 3, lines.length);
const context = range(begin, end + 1)
.map(lineNumber => {
const indicator = (lineNumber === line) ? '>> ' : ' ';
const num = padStart(String(lineNumber), String(end).length);
const text = lines[lineNumber - 1];
return `${indicator}${num}| ${text}`;
})
.join('\n');
return context;
}
function mkMessage(msg, token) {
if (token.file)
msg += `, file:${token.file}`;
const [line, col] = token.getPosition();
msg += `, line:${line}, col:${col}`;
return msg;
}
class Context {
constructor(env = {}, opts = defaultOptions, sync = false) {
this.scopes = [{}];
this.registers = {};
this.sync = sync;
this.opts = opts;
this.globals = opts.globals;
this.environments = env;
}
getRegister(key, defaultValue = {}) {
return (this.registers[key] = this.registers[key] || defaultValue);
}
setRegister(key, value) {
return (this.registers[key] = value);
}
saveRegister(...keys) {
return keys.map(key => [key, this.getRegister(key)]);
}
restoreRegister(keyValues) {
return keyValues.forEach(([key, value]) => this.setRegister(key, value));
}
getAll() {
return [this.globals, this.environments, ...this.scopes]
.reduce((ctx, val) => __assign(ctx, val), {});
}
get(paths) {
const scope = this.findScope(paths[0]);
return this.getFromScope(scope, paths);
}
getFromScope(scope, paths) {
if (typeof paths === 'string')
paths = paths.split('.');
return paths.reduce((scope, path) => {
scope = readProperty(scope, path);
if (isNil(scope) && this.opts.strictVariables) {
throw new InternalUndefinedVariableError(path);
}
return scope;
}, scope);
}
push(ctx) {
return this.scopes.push(ctx);
}
pop() {
return this.scopes.pop();
}
bottom() {
return this.scopes[0];
}
findScope(key) {
for (let i = this.scopes.length - 1; i >= 0; i--) {
const candidate = this.scopes[i];
if (key in candidate)
return candidate;
}
if (key in this.environments)
return this.environments;
return this.globals;
}
}
function readProperty(obj, key) {
if (isNil(obj))
return obj;
obj = toLiquid(obj);
if (isFunction(obj[key]))
return obj[key]();
if (obj instanceof Drop) {
if (obj.hasOwnProperty(key))
return obj[key];
return obj.liquidMethodMissing(key);
}
if (key === 'size')
return readSize(obj);
if (key === 'first')
return readFirst(obj);
if (key === 'last')
return readLast(obj);
return obj[key];
}
function readFirst(obj) {
if (isArray(obj))
return obj[0];
return obj['first'];
}
function readLast(obj) {
if (isArray(obj))
return obj[obj.length - 1];
return obj['last'];
}
function readSize(obj) {
if (isArray(obj) || isString(obj))
return obj.length;
return obj['size'];
}
var TokenKind;
(function (TokenKind) {
TokenKind[TokenKind["Number"] = 1] = "Number";
TokenKind[TokenKind["Literal"] = 2] = "Literal";
TokenKind[TokenKind["Tag"] = 4] = "Tag";
TokenKind[TokenKind["Output"] = 8] = "Output";
TokenKind[TokenKind["HTML"] = 16] = "HTML";
TokenKind[TokenKind["Filter"] = 32] = "Filter";
TokenKind[TokenKind["Hash"] = 64] = "Hash";
TokenKind[TokenKind["PropertyAccess"] = 128] = "PropertyAccess";
TokenKind[TokenKind["Word"] = 256] = "Word";
TokenKind[TokenKind["Range"] = 512] = "Range";
TokenKind[TokenKind["Quoted"] = 1024] = "Quoted";
TokenKind[TokenKind["Operator"] = 2048] = "Operator";
TokenKind[TokenKind["Delimited"] = 12] = "Delimited";
})(TokenKind || (TokenKind = {}));
function isDelimitedToken(val) {
return !!(getKind(val) & TokenKind.Delimited);
}
function isOperatorToken(val) {
return getKind(val) === TokenKind.Operator;
}
function isHTMLToken(val) {
return getKind(val) === TokenKind.HTML;
}
function isOutputToken(val) {
return getKind(val) === TokenKind.Output;
}
function isTagToken(val) {
return getKind(val) === TokenKind.Tag;
}
function isQuotedToken(val) {
return getKind(val) === TokenKind.Quoted;
}
function isLiteralToken(val) {
return getKind(val) === TokenKind.Literal;
}
function isNumberToken(val) {
return getKind(val) === TokenKind.Number;
}
function isPropertyAccessToken(val) {
return getKind(val) === TokenKind.PropertyAccess;
}
function isWordToken(val) {
return getKind(val) === TokenKind.Word;
}
function isRangeToken(val) {
return getKind(val) === TokenKind.Range;
}
function getKind(val) {
return val ? val.kind : -1;
}
var typeGuards = /*#__PURE__*/Object.freeze({
isDelimitedToken: isDelimitedToken,
isOperatorToken: isOperatorToken,
isHTMLToken: isHTMLToken,
isOutputToken: isOutputToken,
isTagToken: isTagToken,
isQuotedToken: isQuotedToken,
isLiteralToken: isLiteralToken,
isNumberToken: isNumberToken,
isPropertyAccessToken: isPropertyAccessToken,
isWordToken: isWordToken,
isRangeToken: isRangeToken
});
function whiteSpaceCtrl(tokens, options) {
let inRaw = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (!isDelimitedToken(token))
continue;
if (!inRaw && token.trimLeft) {
trimLeft(tokens[i - 1], options.greedy);
}
if (isTagToken(token)) {
if (token.name === 'raw')
inRaw = true;
else if (token.name === 'endraw')
inRaw = false;
}
if (!inRaw && token.trimRight) {
trimRight(tokens[i + 1], options.greedy);
}
}
}
function trimLeft(token, greedy) {
if (!token || !isHTMLToken(token))
return;
const mask = greedy ? BLANK : INLINE_BLANK;
while (TYPES[token.input.charCodeAt(token.end - 1 - token.trimRight)] & mask)
token.trimRight++;
}
function trimRight(token, greedy) {
if (!token || !isHTMLToken(token))
return;
const mask = greedy ? BLANK : INLINE_BLANK;
while (TYPES[token.input.charCodeAt(token.begin + token.trimLeft)] & mask)
token.trimLeft++;
if (token.input.charAt(token.begin + token.trimLeft) === '\n')
token.trimLeft++;
}
class Token {
constructor(kind, input, begin, end, file) {
this.kind = kind;
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
}
getText() {
return this.input.slice(this.begin, this.end);
}
getPosition() {
let [row, col] = [1, 1];
for (let i = 0; i < this.begin; i++) {
if (this.input[i] === '\n') {
row++;
col = 1;
}
else
col++;
}
return [row, col];
}
size() {
return this.end - this.begin;
}
}
class NumberToken extends Token {
constructor(whole, decimal) {
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file);
this.whole = whole;
this.decimal = decimal;
}
}
class IdentifierToken extends Token {
constructor(input, begin, end, file) {
super(TokenKind.Word, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
this.content = this.getText();
}
isNumber(allowSign = false) {
const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
? this.begin + 1
: this.begin;
for (let i = begin; i < this.end; i++) {
if (!(TYPES[this.input.charCodeAt(i)] & NUMBER))
return false;
}
return true;
}
}
class NullDrop extends Drop {
equals(value) {
return isNil(toValue(value));
}
gt() {
return false;
}
geq() {
return false;
}
lt() {
return false;
}
leq() {
return false;
}
valueOf() {
return null;
}
}
class EmptyDrop extends Drop {
equals(value) {
if (value instanceof EmptyDrop)
return false;
value = toValue(value);
if (isString(value) || isArray(value))
return value.length === 0;
if (isObject(value))
return Object.keys(value).length === 0;
return false;
}
gt() {
return false;
}
geq() {
return false;
}
lt() {
return false;
}
leq() {
return false;
}
valueOf() {
return '';
}
}
class BlankDrop extends EmptyDrop {
equals(value) {
if (value === false)
return true;
if (isNil(toValue(value)))
return true;
if (isString(value))
return /^\s*$/.test(value);
return super.equals(value);
}
}
const nil = new NullDrop();
const literalValues = {
'true': true,
'false': false,
'nil': nil,
'null': nil,
'empty': new EmptyDrop(),
'blank': new BlankDrop()
};
class LiteralToken extends Token {
constructor(input, begin, end, file) {
super(TokenKind.Literal, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
this.literal = this.getText();
}
}
const precedence = {
'==': 1,
'!=': 1,
'>': 1,
'<': 1,
'>=': 1,
'<=': 1,
'contains': 1,
'and': 0,
'or': 0
};
class OperatorToken extends Token {
constructor(input, begin, end, file) {
super(TokenKind.Operator, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
this.operator = this.getText();
}
getPrecedence() {
const key = this.getText();
return key in precedence ? precedence[key] : 1;
}
}
const rHex = /[\da-fA-F]/;
const rOct = /[0-7]/;
const escapeChar = {
b: '\b',
f: '\f',
n: '\n',
r: '\r',
t: '\t',
v: '\x0B'
};
function hexVal(c) {
const code = c.charCodeAt(0);
if (code >= 97)
return code - 87;
if (code >= 65)
return code - 55;
return code - 48;
}
function parseStringLiteral(str) {
let ret = '';
for (let i = 1; i < str.length - 1; i++) {
if (str[i] !== '\\') {
ret += str[i];
continue;
}
if (escapeChar[str[i + 1]] !== undefined) {
ret += escapeChar[str[++i]];
}
else if (str[i + 1] === 'u') {
let val = 0;
let j = i + 2;
while (j <= i + 5 && rHex.test(str[j])) {
val = val * 16 + hexVal(str[j++]);
}
i = j - 1;
ret += String.fromCharCode(val);
}
else if (!rOct.test(str[i + 1])) {
ret += str[++i];
}
else {
let j = i + 1;
let val = 0;
while (j <= i + 3 && rOct.test(str[j])) {
val = val * 8 + hexVal(str[j++]);
}
i = j - 1;
ret += String.fromCharCode(val);
}
}
return ret;
}
class PropertyAccessToken extends Token {
constructor(variable, props, end) {
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file);
this.variable = variable;
this.props = props;
}
getVariableAsText() {
if (this.variable instanceof IdentifierToken) {
return this.variable.getText();
}
else {
return parseStringLiteral(this.variable.getText());
}
}
}
function assert(predicate, message) {
if (!predicate) {
const msg = message ? message() : `expect ${predicate} to be true`;
throw new AssertionError(msg);
}
}
class FilterToken extends Token {
constructor(name, args, input, begin, end, file) {
super(TokenKind.Filter, input, begin, end, file);
this.name = name;
this.args = args;
}
}
class HashToken extends Token {
constructor(input, begin, end, name, value, file) {
super(TokenKind.Hash, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.name = name;
this.value = value;
this.file = file;
}
}
class QuotedToken extends Token {
constructor(input, begin, end, file) {
super(TokenKind.Quoted, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
}
}
class HTMLToken extends Token {
constructor(input, begin, end, file) {
super(TokenKind.HTML, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.file = file;
this.trimLeft = 0;
this.trimRight = 0;
}
getContent() {
return this.input.slice(this.begin + this.trimLeft, this.end - this.trimRight);
}
}
class DelimitedToken extends Token {
constructor(kind, content, input, begin, end, trimLeft, trimRight, file) {
super(kind, input, begin, end, file);
this.trimLeft = false;
this.trimRight = false;
this.content = this.getText();
const tl = content[0] === '-';
const tr = last(content) === '-';
this.content = content
.slice(tl ? 1 : 0, tr ? -1 : content.length)
.trim();
this.trimLeft = tl || trimLeft;
this.trimRight = tr || trimRight;
}
}
class TagToken extends DelimitedToken {
constructor(input, begin, end, options, file) {
const { trimTagLeft, trimTagRight, tagDelimiterLeft, tagDelimiterRight } = options;
const value = input.slice(begin + tagDelimiterLeft.length, end - tagDelimiterRight.length);
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file);
const tokenizer = new Tokenizer(this.content, options.operatorsTrie);
this.name = tokenizer.readIdentifier().getText();
if (!this.name)
throw new TokenizationError(`illegal tag syntax`, this);
tokenizer.skipBlank();
this.args = tokenizer.remaining();
}
}
class RangeToken extends Token {
constructor(input, begin, end, lhs, rhs, file) {
super(TokenKind.Range, input, begin, end, file);
this.input = input;
this.begin = begin;
this.end = end;
this.lhs = lhs;
this.rhs = rhs;
this.file = file;
}
}
class OutputToken extends DelimitedToken {
constructor(input, begin, end, options, file) {
const { trimOutputLeft, trimOutputRight, outputDelimiterLeft, outputDelimiterRight } = options;
const value = input.slice(begin + outputDelimiterLeft.length, end - outputDelimiterRight.length);
super(TokenKind.Output, value, input, begin, end, trimOutputLeft, trimOutputRight, file);
}
}
function matchOperator(str, begin, trie, end = str.length) {
let node = trie;
let i = begin;
let info;
while (node[str[i]] && i < end) {
node = node[str[i++]];
if (node['end'])
info = node;
}
if (!info)
return -1;
if (info['needBoundary'] && (TYPES[str.charCodeAt(i)] & IDENTIFIER))
return -1;
return i;
}
class Expression {
constructor(tokens) {
this.postfix = [...toPostfix(tokens)];
}
*evaluate(ctx, lenient) {
assert(ctx, () => 'unable to evaluate: context not defined');
const operands = [];
for (const token of this.postfix) {
if (isOperatorToken(token)) {
const r = yield operands.pop();
const l = yield operands.pop();
const result = evalOperatorToken(ctx.opts.operators, token, l, r, ctx);
operands.push(result);
}
else {
operands.push(yield evalToken(token, ctx, lenient && this.postfix.length === 1));
}
}
return operands[0];
}
}
function evalToken(token, ctx, lenient = false) {
if (isPropertyAccessToken(token))
return evalPropertyAccessToken(token, ctx, lenient);
if (isRangeToken(token))
return evalRangeToken(token, ctx);
if (isLiteralToken(token))
return evalLiteralToken(token);
if (isNumberToken(token))
return evalNumberToken(token);
if (isWordToken(token))
return token.getText();
if (isQuotedToken(token))
return evalQuotedToken(token);
}
function evalPropertyAccessToken(token, ctx, lenient) {
const variable = token.getVariableAsText();
const props = token.props.map(prop => evalToken(prop, ctx, false));
try {
return ctx.get([variable, ...props]);
}
catch (e) {
if (lenient && e.name === 'InternalUndefinedVariableError')
return null;
throw (new UndefinedVariableError(e, token));
}
}
function evalNumberToken(token) {
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '');
return Number(str);
}
function evalQuotedToken(token) {
return parseStringLiteral(token.getText());
}
function evalOperatorToken(operators, token, lhs, rhs, ctx) {
const impl = operators[token.operator];
return impl(lhs, rhs, ctx);
}
function evalLiteralToken(token) {
return literalValues[token.literal];
}
function evalRangeToken(token, ctx) {
const low = evalToken(token.lhs, ctx);
const high = evalToken(token.rhs, ctx);
return range(+low, +high + 1);
}
function* toPostfix(tokens) {
const ops = [];
for (const token of tokens) {
if (isOperatorToken(token)) {
while (ops.length && ops[ops.length - 1].getPrecedence() > token.getPrecedence()) {
yield ops.pop();
}
ops.push(token);
}
else
yield token;
}
while (ops.length) {
yield ops.pop();
}
}
class Tokenizer {
constructor(input, trie, file = '') {
this.input = input;
this.trie = trie;
this.file = file;
this.p = 0;
this.rawBeginAt = -1;
this.N = input.length;
}
readExpression() {
return new Expression(this.readExpressionTokens());
}
*readExpressionTokens() {
const operand = this.readValue();
if (!operand)
return;
yield operand;
while (this.p < this.N) {
const operator = this.readOperator();
if (!operator)
return;
const operand = this.readValue();
if (!operand)
return;
yield operator;
yield operand;
}
}
readOperator() {
this.skipBlank();
const end = matchOperator(this.input, this.p, this.trie, this.p + 8);
if (end === -1)
return;
return new OperatorToken(this.input, this.p, (this.p = end), this.file);
}
readFilters() {
const filters = [];
while (true) {
const filter = this.readFilter();
if (!filter)
return filters;
filters.push(filter);
}
}
readFilter() {
this.skipBlank();
if (this.end())
return null;
assert(this.peek() === '|', () => `unexpected token at ${this.snapshot()}`);
this.p++;
const begin = this.p;
const name = this.readIdentifier();
if (!name.size())
return null;
const args = [];
this.skipBlank();
if (this.peek() === ':') {
do {
++this.p;
const arg = this.readFilterArg();
arg && args.push(arg);
while (this.p < this.N && this.peek() !== ',' && this.peek() !== '|')
++this.p;
} while (this.peek() === ',');
}
return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file);
}
readFilterArg() {
const key = this.readValue();
if (!key)
return;
this.skipBlank();
if (this.peek() !== ':')
return key;
++this.p;
const value = this.readValue();
return [key.getText(), value];
}
readTopLevelTokens(options = defaultOptions) {
const tokens = [];
while (this.p < this.N) {
const token = this.readTopLevelToken(options);
tokens.push(token);
}
whiteSpaceCtrl(tokens, options);
return tokens;
}
readTopLevelToken(options) {
const { tagDelimiterLeft, outputDelimiterLeft } = options;
if (this.rawBeginAt > -1)
return this.readEndrawOrRawContent(options);
if (this.match(tagDelimiterLeft))
return this.readTagToken(options);
if (this.match(outputDelimiterLeft))
return this.readOutputToken(options);
return this.readHTMLToken(options);
}
readHTMLToken(options) {
const begin = this.p;
while (this.p < this.N) {
const { tagDelimiterLeft, outputDelimiterLeft } = options;
if (this.match(tagDelimiterLeft))
break;
if (this.match(outputDelimiterLeft))
break;
++this.p;
}
return new HTMLToken(this.input, begin, this.p, this.file);
}
readTagToken(options = defaultOptions) {
const { file, input } = this;
const begin = this.p;
if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
throw this.mkError(`tag ${this.snapshot(begin)} not closed`, begin);
}
const token = new TagToken(input, begin, this.p, options, file);
if (token.name === 'raw')
this.rawBeginAt = begin;
return token;
}
readToDelimiter(delimiter) {
while (this.p < this.N) {
if ((this.peekType() & QUOTE)) {
this.readQuoted();
continue;
}
++this.p;
if (this.rmatch(delimiter))
return this.p;
}
return -1;
}
readOutputToken(options = defaultOptions) {
const { file, input } = this;
const { outputDelimiterRight } = options;
const begin = this.p;
if (this.readToDelimiter(outputDelimiterRight) === -1) {
throw this.mkError(`output ${this.snapshot(begin)} not closed`, begin);
}
return new OutputToken(input, begin, this.p, options, file);
}
readEndrawOrRawContent(options) {
const { tagDelimiterLeft, tagDelimiterRight } = options;
const begin = this.p;
let leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length;
while (this.p < this.N) {
if (this.readIdentifier().getText() !== 'endraw') {
leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length;
continue;
}
while (this.p <= this.N) {
if (this.rmatch(tagDelimiterRight)) {
const end = this.p;
if (begin === leftPos) {
this.rawBeginAt = -1;
return new TagToken(this.input, begin, end, options, this.file);
}
else {
this.p = leftPos;
return new HTMLToken(this.input, begin, leftPos, this.file);
}
}
if (this.rmatch(tagDelimiterLeft))
break;
this.p++;
}
}
throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin);
}
mkError(msg, begin) {
return new TokenizationError(msg, new IdentifierToken(this.input, begin, this.N, this.file));
}
snapshot(begin = this.p) {
return JSON.stringify(ellipsis(this.input.slice(begin), 16));
}
/**
* @deprecated
*/
readWord() {
console.warn('Tokenizer#readWord() will be removed, use #readIdentifier instead');
return this.readIdentifier();
}
readIdentifier() {
this.skipBlank();
const begin = this.p;
while (this.peekType() & IDENTIFIER)
++this.p;
return new IdentifierToken(this.input, begin, this.p, this.file);
}
readHashes() {
const hashes = [];
while (true) {
const hash = this.readHash();
if (!hash)
return hashes;
hashes.push(hash);
}
}
readHash() {
this.skipBlank();
if (this.peek() === ',')
++this.p;
const begin = this.p;
const name = this.readIdentifier();
if (!name.size())
return;
let value;
this.skipBlank();
if (this.peek() === ':') {
++this.p;
value = this.readValue();
}
return new HashToken(this.input, begin, this.p, name, value, this.file);
}
remaining() {
return this.input.slice(this.p);
}
advance(i = 1) {
this.p += i;
}
end() {
return this.p >= this.N;
}
readTo(end) {
while (this.p < this.N) {
++this.p;
if (this.rmatch(end))
return this.p;
}
return -1;
}
readValue() {
const value = this.readQuoted() || this.readRange();
if (value)
return value;
if (this.peek() === '[') {
this.p++;
const prop = this.readQuoted();
if (!prop)
return;
if (this.peek() !== ']')
return;
this.p++;
return new PropertyAccessToken(prop, [], this.p);
}
const variable = this.readIdentifier();
if (!variable.size())
return;
let isNumber = variable.isNumber(true);
const props = [];
while (true) {
if (this.peek() === '[') {
isNumber = false;
this.p++;
const prop = this.readValue() || new IdentifierToken(this.input, this.p, this.p, this.file);
this.readTo(']');
props.push(prop);
}
else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
this.p++;
const prop = this.readIdentifier();
if (!prop.size())
break;
if (!prop.isNumber())
isNumber = false;
props.push(prop);
}
else
break;
}
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
return new LiteralToken(this.input, variable.begin, variable.end, this.file);
}
if (isNumber)
return new NumberToken(variable, props[0]);
return new PropertyAccessToken(variable, props, this.p);
}
readRange() {
this.skipBlank();
const begin = this.p;
if (this.peek() !== '(')
return;
++this.p;
const lhs = this.readValueOrThrow();
this.p += 2;
const rhs = this.readValueOrThrow();
++this.p;
return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file);
}
readValueOrThrow() {
const value = this.readValue();
assert(value, () => `unexpected token ${this.snapshot()}, value expected`);
return value;
}
readQuoted() {
this.skipBlank();
const begin = this.p;
if (!(this.peekType() & QUOTE))
return;
++this.p;
let escaped = false;
while (this.p < this.N) {
++this.p;
if (this.input[this.p - 1] === this.input[begin] && !escaped)
break;
if (escaped)
escaped = false;
else if (this.input[this.p - 1] === '\\')
escaped = true;
}
return new QuotedToken(this.input, begin, this.p, this.file);
}
readFileName() {
const begin = this.p;
while (!(this.peekType() & BLANK) && this.peek() !== ',' && this.p < this.N)
this.p++;
return new IdentifierToken(this.input, begin, this.p, this.file);
}
match(word) {
for (let i = 0; i < word.length; i++) {
if (word[i] !== this.input[this.p + i])
return false;
}
return true;
}
rmatch(pattern) {
for (let i = 0; i < pattern.length; i++) {
if (pattern[pattern.length - 1 - i] !== this.input[this.p - 1 - i])
return false;
}
return true;
}
peekType(n = 0) {
return TYPES[this.input.charCodeAt(this.p + n)];
}
peek(n = 0) {
return this.input[this.p + n];
}
skipBlank() {
while (this.peekType() & BLANK)
++this.p;
}
}
class Emitter {
constructor(keepOutputType) {
this.html = '';
this.break = false;
this.continue = false;
this.keepOutputType = false;
this.keepOutputType = keepOutputType;
}
write(html) {
if (this.keepOutputType === true) {
html = toValue(html);
}
else {
html = stringify(toValue(html));
}
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (this.keepOutputType === true && typeof html !== 'string' && this.html === '') {
this.html = html;
}
else {
this.html = stringify(this.html) + stringify(html);
}
}
}
class Render {
*renderTemplates(templates, ctx, emitter) {
if (!emitter) {
emitter = new Emitter(ctx.opts.keepOutputType);
}
for (const tpl of templates) {
try {
const html = yield tpl.render(ctx, emitter);
html && emitter.write(html);
if (emitter.break || emitter.continue)
break;
}
catch (e) {
const err = RenderError.is(e) ? e : new RenderError(e, tpl);
throw err;
}
}
return emitter.html;
}
}
class ParseStream {
constructor(tokens, parseToken) {
this.handlers = {};
this.stopRequested = false;
this.tokens = tokens;
this.parseToken = parseToken;
}
on(name, cb) {
this.handlers[name] = cb;
return this;
}
trigger(event, arg) {
const h = this.handlers[event];
return h ? (h(arg), true) : false;
}
start() {
this.trigger('start');
let token;
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token))
continue;
if (isTagToken(token) && this.trigger(`tag:${token.name}`, token)) {
continue;
}
const template = this.parseToken(token, this.tokens);
this.trigger('template', template);
}
if (!this.stopRequested)
this.trigger('end');
return this;
}
stop() {
this.stopRequested = true;
return this;
}
}
class TemplateImpl {
constructor(token) {
this.token = token;
}
}
/**
* Key-Value Pairs Representing Tag Arguments
* Example:
* For the markup `, foo:'bar', coo:2 reversed %}`,
* hash['foo'] === 'bar'
* hash['coo'] === 2
* hash['reversed'] === undefined
*/
class Hash {
constructor(markup) {
this.hash = {};
const tokenizer = new Tokenizer(markup, {});
for (const hash of tokenizer.readHashes()) {
this.hash[hash.name.content] = hash.value;
}
}
*render(ctx) {
const hash = {};
for (const key of Object.keys(this.hash)) {
hash[key] = yield evalToken(this.hash[key], ctx);
}
return hash;
}
}
function isKeyValuePair(arr) {
return isArray(arr);
}
class Filter {
constructor(name, impl, args, liquid) {
this.name = name;
this.impl = impl || identify;
this.args = args;
this.liquid = liquid;
}
render(value, context) {
const argv = [];
for (const arg of this.args) {
if (isKeyValuePair(arg))
argv.push([arg[0], evalToken(arg[1], context)]);
else
argv.push(evalToken(arg, context));
}
return this.impl.apply({ context, liquid: this.liquid }, [value, ...argv]);
}
}
class Value {
/**
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
*/
constructor(str, liquid) {
this.filters = [];
const tokenizer = new Tokenizer(str, liquid.options.operatorsTrie);
this.initial = tokenizer.readExpression();
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, liquid.filters.get(name), args, liquid));
}
*value(ctx, lenient) {
lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default');
let val = yield this.initial.evaluate(ctx, lenient);
for (const filter of this.filters) {
val = yield filter.render(val, ctx);
}
return val;
}
}
function createResolvedThenable(value) {
const ret = {
then: (resolve) => resolve(value),
catch: () => ret
};
return ret;
}
function createRejectedThenable(err) {
const ret = {
then: (resolve, reject) => {
if (reject)
return reject(err);
return re