UNPKG

liquidjs

Version:

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

1,683 lines (1,631 loc) 133 kB
/* * liquidjs@10.14.0, https://github.com/harttle/liquidjs * (c) 2016-2024 harttle * Released under the MIT License. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var stream = require('stream'); var path = require('path'); var fs$1 = require('fs'); 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 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) { val = toValue(val); if (isNil(val)) return []; if (isArray(val)) return val; return [val]; } 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 isUndefined(value) { return value === undefined; } 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(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, '\\$&'); } /** * targeting ES5, extends Error won't create a proper prototype chain, need a trait to keep track of classes */ const TRAIT = '__liquidClass__'; class LiquidError extends Error { constructor(err, token) { /** * note: for ES5 targeting, `this` will be replaced by return value of Error(), * thus everything on `this` will be lost, avoid calling `LiquidError` methods here */ super(typeof err === 'string' ? err : err.message); this.context = ''; if (typeof err !== 'string') Object.defineProperty(this, 'originalError', { value: err, enumerable: false }); Object.defineProperty(this, 'token', { value: token, enumerable: false }); Object.defineProperty(this, TRAIT, { value: 'LiquidError', enumerable: false }); } update() { Object.defineProperty(this, 'context', { value: mkContext(this.token), enumerable: false }); this.message = mkMessage(this.message, this.token); this.stack = this.message + '\n' + this.context + '\n' + this.stack; if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack; } static is(obj) { return obj?.[TRAIT] === 'LiquidError'; } } class TokenizationError extends LiquidError { constructor(message, token) { super(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 LiquidErrors extends LiquidError { constructor(errors) { super(errors[0], errors[0].token); this.errors = errors; this.name = 'LiquidErrors'; const s = errors.length > 1 ? 's' : ''; this.message = `${errors.length} error${s} found`; super.update(); } static is(obj) { return obj.name === 'LiquidErrors'; } } 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, col] = 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 rowIndicator = (lineNumber === line) ? '>> ' : ' '; const num = padStart(String(lineNumber), String(end).length); let text = `${rowIndicator}${num}| `; const colIndicator = lineNumber === line ? '\n' + padStart('^', col + text.length) : ''; text += lines[lineNumber - 1]; text += colIndicator; return 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 WORD = 1; const BLANK = 4; const QUOTE = 8; const INLINE_BLANK = 16; const NUMBER = 32; const SIGN = 64; const PUNCTUATION = 128; function isWord(char) { const code = char.charCodeAt(0); return code >= 128 ? !TYPES[code] : !!(TYPES[code] & WORD); } 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; TYPES[8220] = TYPES[8221] = PUNCTUATION; 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) && isFunction(arg.gt) && isFunction(arg.geq) && isFunction(arg.lt) && isFunction(arg.leq)); } const nil = new NullDrop(); const literalValues = { 'true': true, 'false': false, 'nil': nil, 'null': nil, 'empty': new EmptyDrop(), 'blank': new BlankDrop() }; function createTrie(input) { const trie = {}; for (const [name, data] of Object.entries(input)) { let node = trie; for (let i = 0; i < name.length; i++) { const c = name[i]; node[c] = node[c] || {}; if (i === name.length - 1 && isWord(name[i])) { node[c].needBoundary = true; } node = node[c]; } node.data = data; node.end = true; } return trie; } // convert an async iterator to a Promise async function toPromise(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'; try { if (isIterator(value)) value = toPromise(value); if (isPromise(value)) value = await 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; } 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); 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 ordinal(d) { const date = d.getDate(); if ([11, 12, 13].includes(date)) return 'th'; switch (date % 10) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } 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: ' ' }; function getTimezoneOffset(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'); } 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) => ordinal(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: getTimezoneOffset, Z: (d, opts) => { if (d.getTimezoneName) { return d.getTimezoneName() || getTimezoneOffset(d, opts); } return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : ''); }, '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 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, timezone) { this.date = init instanceof TimezoneDate ? init.date : new Date(init); this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone; this.timezoneName = isString(timezone) ? timezone : ''; const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute; const time = this.date.getTime() + diff; this.displayDate = new Date(time); } getTime() { return this.displayDate.getTime(); } getMilliseconds() { return this.displayDate.getMilliseconds(); } getSeconds() { return this.displayDate.getSeconds(); } getMinutes() { return this.displayDate.getMinutes(); } getHours() { return this.displayDate.getHours(); } getDay() { return this.displayDate.getDay(); } getDate() { return this.displayDate.getDate(); } getMonth() { return this.displayDate.getMonth(); } getFullYear() { return this.displayDate.getFullYear(); } toLocaleString(locale, init) { if (init?.timeZone) { return this.date.toLocaleString(locale, init); } return this.displayDate.toLocaleString(locale, init); } toLocaleTimeString(locale) { return this.displayDate.toLocaleTimeString(locale); } toLocaleDateString(locale) { return this.displayDate.toLocaleDateString(locale); } getTimezoneOffset() { return this.timezoneOffset; } getTimezoneName() { return this.timezoneName; } /** * 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 offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10)); return new TimezoneDate(+new Date(dateString), offset); } return new Date(dateString); } static getTimezoneOffset(timezoneName, date = new Date()) { const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName }); const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' }); const localDate = new Date(localDateString); const utcDate = new Date(utcDateString); return (+utcDate - +localDate) / (60 * 1000); } } class DelimitedToken extends Token { constructor(kind, [contentBegin, contentEnd], input, begin, end, trimLeft, trimRight, file) { super(kind, input, begin, end, file); this.trimLeft = false; this.trimRight = false; const tl = input[contentBegin] === '-'; const tr = input[contentEnd - 1] === '-'; let l = tl ? contentBegin + 1 : contentBegin; let r = tr ? contentEnd - 1 : contentEnd; while (l < r && (TYPES[input.charCodeAt(l)] & BLANK)) l++; while (r > l && (TYPES[input.charCodeAt(r - 1)] & BLANK)) r--; this.contentRange = [l, r]; this.trimLeft = tl || trimLeft; this.trimRight = tr || trimRight; } get content() { return this.input.slice(this.contentRange[0], this.contentRange[1]); } } class TagToken extends DelimitedToken { constructor(input, begin, end, options, file) { const { trimTagLeft, trimTagRight, tagDelimiterLeft, tagDelimiterRight } = options; const [valueBegin, valueEnd] = [begin + tagDelimiterLeft.length, end - tagDelimiterRight.length]; super(exports.TokenKind.Tag, [valueBegin, valueEnd], input, begin, end, trimTagLeft, trimTagRight, file); this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange); this.name = this.tokenizer.readTagName(); this.tokenizer.assert(this.name, `illegal tag syntax, tag name expected`); this.tokenizer.skipBlank(); } get args() { return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]); } } class OutputToken extends DelimitedToken { constructor(input, begin, end, options, file) { const { trimOutputLeft, trimOutputRight, outputDelimiterLeft, outputDelimiterRight } = options; const valueRange = [begin + outputDelimiterLeft.length, end - outputDelimiterRight.length]; super(exports.TokenKind.Output, valueRange, input, begin, end, trimOutputLeft, trimOutputRight, 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 NumberToken extends Token { constructor(input, begin, end, file) { super(exports.TokenKind.Number, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.file = file; this.content = Number(this.getText()); } } 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 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(); this.content = literalValues[this.literal]; } } const operatorPrecedences = { '==': 2, '!=': 2, '>': 2, '<': 2, '>=': 2, '<=': 2, 'contains': 2, 'not': 1, 'and': 0, 'or': 0 }; const operatorTypes = { '==': 0 /* OperatorType.Binary */, '!=': 0 /* OperatorType.Binary */, '>': 0 /* OperatorType.Binary */, '<': 0 /* OperatorType.Binary */, '>=': 0 /* OperatorType.Binary */, '<=': 0 /* OperatorType.Binary */, 'contains': 0 /* OperatorType.Binary */, 'not': 1 /* OperatorType.Unary */, 'and': 0 /* OperatorType.Binary */, 'or': 0 /* OperatorType.Binary */ }; 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 operatorPrecedences ? operatorPrecedences[key] : 1; } } class PropertyAccessToken extends Token { constructor(variable, props, input, begin, end, file) { super(exports.TokenKind.PropertyAccess, input, begin, end, file); this.variable = variable; this.props = props; } } 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; } } 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 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; this.content = parseStringLiteral(this.getText()); } } 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; } } /** * LiquidTagToken is different from TagToken by not having delimiters `{%` or `%}` */ class LiquidTagToken extends DelimitedToken { constructor(input, begin, end, options, file) { super(exports.TokenKind.Tag, [begin, end], input, begin, end, false, false, file); this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange); this.name = this.tokenizer.readTagName(); this.tokenizer.assert(this.name, 'illegal liquid tag syntax'); this.tokenizer.skipBlank(); this.args = this.tokenizer.remaining(); } } /** * value expression with optional filters * e.g. * {% assign foo="bar" | append: "coo" %} */ class FilteredValueToken extends Token { constructor(initial, filters, input, begin, end, file) { super(exports.TokenKind.FilteredValue, input, begin, end, file); this.initial = initial; this.filters = filters; this.input = input; this.begin = begin; this.end = end; this.file = file; } } class SimpleEmitter { constructor() { this.buffer = ''; } write(html) { this.buffer += stringify(html); } } class StreamedEmitter { constructor() { this.buffer = ''; this.stream = new stream.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(); } const errors = []; 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 = LiquidError.is(e) ? e : new RenderError(e, tpl); if (ctx.opts.catchAllErrors) errors.push(err); else throw err; } } if (errors.length) { throw new LiquidErrors(errors); } 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 /* OperatorType.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)); } } return operands[0]; } valid() { return !!this.postfix.length; } } function* evalToken(token, ctx, lenient = false) { if (!token) return; if ('content' in token) return token.content; if (isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient); if (isRangeToken(token)) return yield evalRangeToken(token, ctx); } function* evalPropertyAccessToken(token, ctx, lenient) { const props = []; for (const prop of token.props) { props.push((yield evalToken(prop, ctx, false))); } try { if (token.variable) { const variable = yield evalToken(token.variable, ctx, lenient); return yield ctx._getFromScope(variable, props); } else { return yield ctx._get(props); } } catch (e) { if (lenient && e.name === 'InternalUndefinedVariableError') return null; throw (new UndefinedVariableError(e, token)); } } function evalQuotedToken(token) { return token.content; } 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) { val = toValue(val); if (ctx.opts.jsTruthy) { return !val; } else { return val === false || undefined === val || val === null; } } const defaultOperators = { '==': equals, '!=': (l, r) => !equals(l, 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); if (isArray(l)) return l.some((i) => equals(i, r)); if (isFunction(l?.indexOf)) return l.indexOf(toValue(r)) > -1; return 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) }; function equals(lhs, rhs) { if (isComparable(lhs)) return lhs.equals(rhs); if (isComparable(rhs)) return rhs.equals(lhs); lhs = toValue(lhs); rhs = toValue(rhs); if (isArray(lhs)) { return isArray(rhs) && arrayEquals(lhs, rhs); } return lhs === rhs; } function arrayEquals(lhs, rhs) { if (lhs.length !== rhs.length) return false; return !lhs.some((value, i) => !equals(value, rhs[i])); } 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 requireResolve = require.resolve; const statAsync = promisify(fs$1.stat); const readFileAsync = promisify(fs$1.readFile); async function exists(filepath) { try { await statAsync(filepath); return true; } catch (err) { return 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 requireResolve(file); } catch (e) { } } function dirname(filepath) { return path.dirname(filepath); } function contains(root, file) { root = path.resolve(root); root = root.endsWith(path.sep) ? root : root + path.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: path.sep }); function defaultFilter(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, space = 0) { return JSON.stringify(value, null, space); } function inspect(value, space = 0) { const ancestors = []; return JSON.stringify(value, function (_key, value) { if (typeof value !== 'object' || value === null) return value; // `this` is the object that value is contained in, i.e., its direct parent. while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) ancestors.pop(); if (ancestors.includes(value)) return '[Circular]'; ancestors.push(value); return value; }, space); } function to_integer(value) { return Number(value); } const raw = { raw: true, handler: identify }; var misc = { default: defaultFilter, raw, jsonify: json, to_integer, json, inspect }; 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 xml_escape(str) { return escape(str); } 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(/\r?\n/gm, '<br />\n'); } function strip_html(v) { return stringify(v).replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, ''); } var htmlFilters = /*#__PURE__*/Object.freeze({ __proto__: null, escape: escape, xml_escape: xml_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, dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z', 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 = { ...defaultOptions, ...(options.jekyllInclude ? { dynamicPartials: false } : {}), ...options }; if ((!options.fs.dirname || !options.fs.sep) && options.relativeReference) { console.warn('[LiquidJS] `fs.dirname` and `fs.sep` are required for relativeReference, set relativeReference to `false` to suppress this warning'); 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 misc.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 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, range) { this.input = input; this.file = file; this.rawBeginAt = -1; this.p = range ? range[0] : 0; this.N = range ? range[1] : input.length; this.opTrie = createTrie(operators); this.literalTrie = createTrie(literalValues); } readExpression() { return new Expression(this.readExpressionTokens()); } *readExpressionTokens() { while (this.p < this.N) { const operator = this.readOperator(); if (operator) { yield operator; continue; } const operand = thi