UNPKG

liquidjs

Version:

A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.

1,656 lines (1,601 loc) 97 kB
/* * liquidjs@9.24.2, https://github.com/harttle/liquidjs * (c) 2016-2021 harttle * Released under the MIT License. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var path = require('path'); var fs$1 = require('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(fs$1.stat); const readFileAsync = promisify(fs$1.readFile); function exists(filepath) { return statAsync(filepath).then(() => true).catch(() => false); } function readFile(filepath) { return readFileAsync(filepath, 'utf8'); } function existsSync(filepath) { try { fs$1.statSync(filepath); return true; } catch (err) { return false; } } function readFileSync(filepath) { return fs$1.readFileSync(filepath, 'utf8'); } function resolve(root, file, ext) { if (!path.extname(file)) file += ext; return path.resolve(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']; } (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"; })(exports.TokenKind || (exports.TokenKind = {})); function isDelimitedToken(val) { return !!(getKind(val) & exports.TokenKind.Delimited); } function isOperatorToken(val) { return getKind(val) === exports.TokenKind.Operator; } function isHTMLToken(val) { return getKind(val) === exports.TokenKind.HTML; } function isOutputToken(val) { return getKind(val) === exports.TokenKind.Output; } function isTagToken(val) { return getKind(val) === exports.TokenKind.Tag; } function isQuotedToken(val) { return getKind(val) === exports.TokenKind.Quoted; } function isLiteralToken(val) { return getKind(val) === exports.TokenKind.Literal; } function isNumberToken(val) { return getKind(val) === exports.TokenKind.Number; } function isPropertyAccessToken(val) { return getKind(val) === exports.TokenKind.PropertyAccess; } function isWordToken(val) { return getKind(val) === exports.TokenKind.Word; } function isRangeToken(val) { return getKind(val) === exports.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(exports.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(exports.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(exports.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(exports.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(exports.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(exports.TokenKind.Filter, input, begin, end, file); this.name = name; this.args = args; } } class HashToken extends Token { constructor(input, begin, end, name, value, file) { super(exports.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(exports.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(exports.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(exports.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(exports.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(exports.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 }; re