UNPKG

liquidjs

Version:

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

1,664 lines (1,609 loc) 119 kB
/* * liquidjs@10.5.0, https://github.com/harttle/liquidjs * (c) 2016-2023 harttle * Released under the MIT License. */ import { PassThrough } from 'stream'; import { extname, resolve as resolve$1, dirname as dirname$1, sep } from 'path'; import { statSync, readFileSync as readFileSync$1, stat, readFile as readFile$1 } from 'fs'; import { createRequire } from 'module'; 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 Drop { liquidMethodMissing(key) { return undefined; } } const toString$1 = Object.prototype.toString; const toLowerCase = String.prototype.toLowerCase; const hasOwnProperty = Object.hasOwnProperty; function isString(value) { return typeof value === 'string'; } // eslint-disable-next-line @typescript-eslint/ban-types function isFunction(value) { return typeof value === 'function'; } function isPromise(val) { return val && isFunction(val.then); } function isIterator(val) { return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return); } function escapeRegex(str) { return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); } 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); if (isString(value)) return value; if (isNil(value)) return ''; if (isArray(value)) return value.map(x => stringify(x)).join(''); return String(value); } function toValue(value) { return (value instanceof Drop && isFunction(value.valueOf)) ? 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; } function isArray(value) { // be compatible with IE 8 return toString$1.call(value) === '[object Array]'; } function isIterable(value) { return isObject(value) && Symbol.iterator in value; } /* * 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(obj, iteratee) { obj = obj || {}; for (const k in obj) { if (hasOwnProperty.call(obj, k)) { if (iteratee(obj[k], k, obj) === false) break; } } return obj; } function last$1(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 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.slice(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; } function argumentsToValue(fn) { return (...args) => fn(...args.map(toValue)); } function escapeRegExp(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); } 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; } // **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 assert(predicate, message) { if (!predicate) { const msg = typeof message === 'function' ? message() : (message || `expect ${predicate} to be true`); throw new AssertionError(msg); } } 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); } } class ForloopDrop extends Drop { constructor(length, collection, variable) { super(); this.i = 0; this.length = length; this.name = `${variable}-${collection}`; } next() { this.i++; } index0() { return this.i; } index() { return this.i + 1; } first() { return this.i === 0; } last() { return this.i === this.length - 1; } rindex() { return this.length - this.i; } rindex0() { return this.length - this.i - 1; } valueOf() { return JSON.stringify(this); } } class BlockDrop extends Drop { constructor( // the block render from layout template superBlockRender = () => '') { super(); this.superBlockRender = superBlockRender; } /** * Provide parent access in child block by * {{ block.super }} */ super() { return this.superBlockRender(); } } function isComparable(arg) { return arg && isFunction(arg.equals); } const nil = new NullDrop(); const literalValues = { 'true': true, 'false': false, 'nil': nil, 'null': nil, 'empty': new EmptyDrop(), 'blank': new BlankDrop() }; 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; } /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ 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); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } // convert an async iterator to a Promise function toPromise(val) { return __awaiter(this, void 0, void 0, function* () { if (!isIterator(val)) return val; let value; let done = false; let next = 'next'; do { const state = val[next](value); done = state.done; value = state.value; next = 'next'; try { if (isIterator(value)) value = toPromise(value); if (isPromise(value)) value = yield value; } catch (err) { next = 'throw'; value = err; } } while (!done); return value; }); } // convert an async iterator to a value in a synchronous manner function toValueSync(val) { if (!isIterator(val)) return val; let value; let done = false; let next = 'next'; do { const state = val[next](value); done = state.done; value = state.value; next = 'next'; if (isIterator(value)) { try { value = toValueSync(value); } catch (err) { next = 'throw'; value = err; } } } while (!done); return value; } function toEnumerable(val) { val = toValue(val); if (isArray(val)) return val; if (isString(val) && val.length > 0) return [val]; if (isIterable(val)) return Array.from(val); if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]); return []; } function toArray(val) { if (isNil(val)) return []; if (isArray(val)) return val; return [val]; } const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/; const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const dayNames = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; const monthNamesShort = monthNames.map(abbr); const dayNamesShort = dayNames.map(abbr); const suffixes = { 1: 'st', 2: 'nd', 3: 'rd', 'default': 'th' }; function abbr(str) { return str.slice(0, 3); } // prototype extensions function daysInMonth(d) { const feb = isLeapYear(d) ? 29 : 28; return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; } function getDayOfYear(d) { let num = 0; for (let i = 0; i < d.getMonth(); ++i) { num += daysInMonth(d)[i]; } return num + d.getDate(); } function getWeekOfYear(d, startDay) { // Skip to startDay of this week const now = getDayOfYear(d) + (startDay - d.getDay()); // Find the first startDay of the year const jan1 = new Date(d.getFullYear(), 0, 1); const then = (7 - jan1.getDay() + startDay); return String(Math.floor((now - then) / 7) + 1); } function isLeapYear(d) { const year = d.getFullYear(); return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))); } function getSuffix(d) { const str = d.getDate().toString(); const index = parseInt(str.slice(-1)); return suffixes[index] || suffixes['default']; } function century(d) { return parseInt(d.getFullYear().toString().substring(0, 2), 10); } // default to 0 const padWidths = { d: 2, e: 2, H: 2, I: 2, j: 3, k: 2, l: 2, L: 3, m: 2, M: 2, S: 2, U: 2, W: 2 }; // default to '0' const padChars = { a: ' ', A: ' ', b: ' ', B: ' ', c: ' ', e: ' ', k: ' ', l: ' ', p: ' ', P: ' ' }; const formatCodes = { a: (d) => dayNamesShort[d.getDay()], A: (d) => dayNames[d.getDay()], b: (d) => monthNamesShort[d.getMonth()], B: (d) => monthNames[d.getMonth()], c: (d) => d.toLocaleString(), C: (d) => century(d), d: (d) => d.getDate(), e: (d) => d.getDate(), H: (d) => d.getHours(), I: (d) => String(d.getHours() % 12 || 12), j: (d) => getDayOfYear(d), k: (d) => d.getHours(), l: (d) => String(d.getHours() % 12 || 12), L: (d) => d.getMilliseconds(), m: (d) => d.getMonth() + 1, M: (d) => d.getMinutes(), N: (d, opts) => { const width = Number(opts.width) || 9; const str = String(d.getMilliseconds()).slice(0, width); return padEnd(str, width, '0'); }, p: (d) => (d.getHours() < 12 ? 'AM' : 'PM'), P: (d) => (d.getHours() < 12 ? 'am' : 'pm'), q: (d) => getSuffix(d), s: (d) => Math.round(d.getTime() / 1000), S: (d) => d.getSeconds(), u: (d) => d.getDay() || 7, U: (d) => getWeekOfYear(d, 0), w: (d) => d.getDay(), W: (d) => getWeekOfYear(d, 1), x: (d) => d.toLocaleDateString(), X: (d) => d.toLocaleTimeString(), y: (d) => d.getFullYear().toString().slice(2, 4), Y: (d) => d.getFullYear(), z: (d, opts) => { const nOffset = Math.abs(d.getTimezoneOffset()); const h = Math.floor(nOffset / 60); const m = nOffset % 60; return (d.getTimezoneOffset() > 0 ? '-' : '+') + padStart(h, 2, '0') + (opts.flags[':'] ? ':' : '') + padStart(m, 2, '0'); }, 't': () => '\t', 'n': () => '\n', '%': () => '%' }; formatCodes.h = formatCodes.b; function strftime(d, formatStr) { let output = ''; let remaining = formatStr; let match; while ((match = rFormat.exec(remaining))) { output += remaining.slice(0, match.index); remaining = remaining.slice(match.index + match[0].length); output += format(d, match); } return output + remaining; } function format(d, match) { const [input, flagStr = '', width, modifier, conversion] = match; const convert = formatCodes[conversion]; if (!convert) return input; const flags = {}; for (const flag of flagStr) flags[flag] = true; let ret = String(convert(d, { flags, width, modifier })); let padChar = padChars[conversion] || '0'; let padWidth = width || padWidths[conversion] || 0; if (flags['^']) ret = ret.toUpperCase(); else if (flags['#']) ret = changeCase(ret); if (flags['_']) padChar = ' '; else if (flags['0']) padChar = '0'; if (flags['-']) padWidth = 0; return padStart(ret, padWidth, padChar); } // one minute in milliseconds const OneMinute = 60000; const hostTimezoneOffset = new Date().getTimezoneOffset(); const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/; /** * A date implementation with timezone info, just like Ruby date * * Implementation: * - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods * - rewrite getTimezoneOffset() to trick strftime */ class TimezoneDate { constructor(init, timezoneOffset) { if (init instanceof TimezoneDate) { this.date = init.date; timezoneOffset = init.timezoneOffset; } else { const diff = (hostTimezoneOffset - timezoneOffset) * OneMinute; const time = new Date(init).getTime() + diff; this.date = new Date(time); } this.timezoneOffset = timezoneOffset; } getTime() { return this.date.getTime(); } getMilliseconds() { return this.date.getMilliseconds(); } getSeconds() { return this.date.getSeconds(); } getMinutes() { return this.date.getMinutes(); } getHours() { return this.date.getHours(); } getDay() { return this.date.getDay(); } getDate() { return this.date.getDate(); } getMonth() { return this.date.getMonth(); } getFullYear() { return this.date.getFullYear(); } toLocaleTimeString(locale) { return this.date.toLocaleTimeString(locale); } toLocaleDateString(locale) { return this.date.toLocaleDateString(locale); } getTimezoneOffset() { return this.timezoneOffset; } /** * Create a Date object fixed to it's declared Timezone. Both * - 2021-08-06T02:29:00.000Z and * - 2021-08-06T02:29:00.000+08:00 * will always be displayed as * - 2021-08-06 02:29:00 * regardless timezoneOffset in JavaScript realm * * The implementation hack: * Instead of calling `.getMonth()`/`.getUTCMonth()` respect to `preserveTimezones`, * we create a different Date to trick strftime, it's both simpler and more performant. * Given that a template is expected to be parsed fewer times than rendered. */ static createDateFixedToTimezone(dateString) { const m = dateString.match(ISO8601_TIMEZONE_PATTERN); // representing a UTC timestamp if (m && m[1] === 'Z') { return new TimezoneDate(+new Date(dateString), 0); } // has a timezone specified if (m && m[2] && m[3] && m[4]) { const [, , sign, hours, minutes] = m; const delta = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10)); return new TimezoneDate(+new Date(dateString), delta); } return new Date(dateString); } } 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$1(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.operators); this.name = tokenizer.readTagName(); if (!this.name) throw new TokenizationError(`illegal tag syntax`, this); tokenizer.skipBlank(); this.args = tokenizer.remaining(); } } 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); } } 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 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 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 operatorPrecedences = { '==': 2, '!=': 2, '>': 2, '<': 2, '>=': 2, '<=': 2, 'contains': 2, 'not': 1, 'and': 0, 'or': 0 }; const operatorTypes = { '==': 0 /* Binary */, '!=': 0 /* Binary */, '>': 0 /* Binary */, '<': 0 /* Binary */, '>=': 0 /* Binary */, '<=': 0 /* Binary */, 'contains': 0 /* Binary */, 'not': 1 /* Unary */, 'and': 0 /* Binary */, 'or': 0 /* Binary */ }; 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 operatorPrecedences ? operatorPrecedences[key] : 1; } } class PropertyAccessToken extends Token { constructor(variable, props, end) { super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file); this.variable = variable; this.props = props; this.propertyName = this.variable instanceof IdentifierToken ? this.variable.getText() : parseStringLiteral(this.variable.getText()); } } 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 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 LiquidTagToken extends DelimitedToken { constructor(input, begin, end, options, file) { const value = input.slice(begin, end); super(TokenKind.Tag, value, input, begin, end, false, false, file); if (!/\S/.test(value)) { // A line that contains only whitespace. this.name = ''; this.args = ''; } else { const tokenizer = new Tokenizer(this.content, options.operators); this.name = tokenizer.readTagName(); if (!this.name) throw new TokenizationError(`illegal liquid tag syntax`, this); tokenizer.skipBlank(); this.args = tokenizer.remaining(); } } } class SimpleEmitter { constructor() { this.buffer = ''; } write(html) { this.buffer += stringify(html); } } class StreamedEmitter { constructor() { this.buffer = ''; this.stream = new PassThrough(); } write(html) { this.stream.write(stringify(html)); } error(err) { this.stream.emit('error', err); } end() { this.stream.end(); } } class KeepingTypeEmitter { constructor() { this.buffer = ''; } write(html) { html = 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 (typeof html !== 'string' && this.buffer === '') { this.buffer = html; } else { this.buffer = stringify(this.buffer) + stringify(html); } } } class Render { renderTemplatesToNodeStream(templates, ctx) { const emitter = new StreamedEmitter(); Promise.resolve().then(() => toPromise(this.renderTemplates(templates, ctx, emitter))) .then(() => emitter.end(), err => emitter.error(err)); return emitter.stream; } *renderTemplates(templates, ctx, emitter) { if (!emitter) { emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter(); } for (const tpl of templates) { try { // if tpl.render supports emitter, it'll return empty `html` const html = yield tpl.render(ctx, emitter); // if not, it'll return an `html`, write to the emitter for it 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.buffer; } } 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 = operands.pop(); let result; if (operatorTypes[token.operator] === 1 /* Unary */) { result = yield ctx.opts.operators[token.operator](r, ctx); } else { const l = operands.pop(); result = yield ctx.opts.operators[token.operator](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 yield evalPropertyAccessToken(token, ctx, lenient); if (isRangeToken(token)) return yield 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 props = []; for (const prop of token.props) { props.push((yield evalToken(prop, ctx, false))); } try { return yield ctx._get([token.propertyName, ...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 evalLiteralToken(token) { return literalValues[token.literal]; } function* evalRangeToken(token, ctx) { const low = yield evalToken(token.lhs, ctx); const high = yield 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(); } } 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 toValue(l) === toValue(r); }, '!=': (l, r) => { if (isComparable(l)) return !l.equals(r); if (isComparable(r)) return !r.equals(l); return toValue(l) !== toValue(r); }, '>': (l, r) => { if (isComparable(l)) return l.gt(r); if (isComparable(r)) return r.lt(l); return toValue(l) > toValue(r); }, '<': (l, r) => { if (isComparable(l)) return l.lt(r); if (isComparable(r)) return r.gt(l); return toValue(l) < toValue(r); }, '>=': (l, r) => { if (isComparable(l)) return l.geq(r); if (isComparable(r)) return r.leq(l); return toValue(l) >= toValue(r); }, '<=': (l, r) => { if (isComparable(l)) return l.leq(r); if (isComparable(r)) return r.geq(l); return toValue(l) <= toValue(r); }, 'contains': (l, r) => { l = toValue(l); r = toValue(r); return l && isFunction(l.indexOf) ? l.indexOf(r) > -1 : false; }, 'not': (v, ctx) => isFalsy(toValue(v), ctx), 'and': (l, r, ctx) => isTruthy(toValue(l), ctx) && isTruthy(toValue(r), ctx), 'or': (l, r, ctx) => isTruthy(toValue(l), ctx) || isTruthy(toValue(r), ctx) }; 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); } } function requireResolve (file) { /** * createRequire() can throw, * when import.meta.url not begin with "file://". */ const require = createRequire(import.meta.url); return require.resolve(file) } const statAsync = promisify(stat); const readFileAsync = promisify(readFile$1); function exists(filepath) { return __awaiter(this, void 0, void 0, function* () { try { yield statAsync(filepath); return true; } catch (err) { return 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 requireResolve(file); } catch (e) { } } function dirname(filepath) { return dirname$1(filepath); } function contains(root, file) { root = resolve$1(root); root = root.endsWith(sep) ? root : root + sep; return file.startsWith(root); } var fs = /*#__PURE__*/Object.freeze({ __proto__: null, exists: exists, readFile: readFile, existsSync: existsSync, readFileSync: readFileSync, resolve: resolve, fallback: fallback, dirname: dirname, contains: contains, sep: sep }); function Default(value, defaultValue, ...args) { value = toValue(value); if (isArray(value) || isString(value)) return value.length ? value : defaultValue; if (value === false && (new Map(args)).get('allow_false')) return false; return isFalsy(value, this.context) ? defaultValue : value; } function json(value) { return JSON.stringify(value); } const raw = { raw: true, handler: identify }; const escapeMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&#34;', "'": '&#39;' }; const unescapeMap = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&#34;': '"', '&#39;': "'" }; function escape(str) { return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]); } function unescape(str) { return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]); } function escape_once(str) { return escape(unescape(stringify(str))); } function newline_to_br(v) { return stringify(v).replace(/\n/g, '<br />\n'); } function strip_html(v) { return stringify(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, ''); } var htmlFilters = /*#__PURE__*/Object.freeze({ __proto__: null, escape: escape, escape_once: escape_once, newline_to_br: newline_to_br, strip_html: strip_html }); const defaultOptions = { root: ['.'], layouts: ['.'], partials: ['.'], relativeReference: true, jekyllInclude: false, 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, ownPropertyOnly: true, lenientIf: false, globals: {}, keepOutputType: false, operators: defaultOperators }; function normalize(options) { if (options.hasOwnProperty('root')) { if (!options.hasOwnProperty('partials')) options.partials = options.root; if (!options.hasOwnProperty('layouts')) options.layouts = 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; } options = Object.assign(Object.assign(Object.assign({}, defaultOptions), (options.jekyllInclude ? { dynamicPartials: false } : {})), options); if (!options.fs.dirname && options.relativeReference) { console.warn('[LiquidJS] `fs.dirname` is required for relativeReference, set relativeReference to `false` to suppress this warning, or provide implementation for `fs.dirname`'); options.relativeReference = false; } options.root = normalizeDirectoryList(options.root); options.partials = normalizeDirectoryList(options.partials); options.layouts = normalizeDirectoryList(options.layouts); options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape); return options; } function getOutputEscapeFunction(nameOrFunction) { if (nameOrFunction === 'escape') return escape; if (nameOrFunction === 'json') return json; assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function'); return nameOrFunction; } function normalizeDirectoryList(value) { let list = []; if (isArray(value)) list = value; if (isString(value)) list = [value]; return list; } 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; } 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 Tokenizer { constructor(input, operators = defaultOptions.operators, file) { this.input = input; this.file = file; this.p = 0; this.rawBeginAt = -1; this.N = input.length; this.opTrie = createTrie(operators); } readExpression() { return new Expression(this.readExpressionTokens()); } *readExpressionTokens() { while (this.p < this.N) { const operator = this.readOperator(); if (operator) { yield operator; continue; } const operand = this.readValue(); if (operand) { yield operand; continue; } return; } } readOperator() { this.skipBlank(); const end = matchOperator(this.input, this.p, this.opTrie); 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); this.skipBlank(); assert(this.end() || this.peek() === ',' || this.peek() === '|', () => `unexpected character ${this.snapshot()}`); } 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([tagDelimiterLeft, outputDelimiterLeft]); } readHTMLToken(stopStrings) { const begin = this.p; while (this.p < this.N) { if (stopStrings.some(str => this.match(str))) 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 thi