UNPKG

liquidjs

Version:

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

1,689 lines (1,635 loc) 166 kB
/* * liquidjs@10.21.1, https://github.com/harttle/liquidjs * (c) 2016-2025 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 toNumber(value) { value = Number(value); return isNaN(value) ? 0 : 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 isArrayLike(value) { return value && isNumber(value.length); } 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 function (...args) { return fn.call(this, ...args.map(toValue)); }; } function escapeRegExp(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); } /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */ function* strictUniq(array) { const seen = new Set(); for (const element of array) { const key = JSON.stringify(element); if (!seen.has(key)) { seen.add(key); yield element; } } } /** * 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); } } function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate)}`) { assert(!predicate, message); } 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 ''; } static is(value) { return value instanceof EmptyDrop; } } 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); } static is(value) { return value instanceof BlankDrop; } } 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 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 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() { const emitter = new SimpleEmitter(); yield this.superBlockRender(emitter); return emitter.buffer; } } 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])?(.)/; // 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 }; const padSpaceChars = new Set('aAbBceklpP'); 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) => d.getShortWeekdayName(), A: (d) => d.getLongWeekdayName(), b: (d) => d.getShortMonthName(), B: (d) => d.getLongMonthName(), 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) => d.getTimeZoneName() || getTimezoneOffset(d, opts), '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 = padSpaceChars.has(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); } function getDateTimeFormat() { return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat : undefined); } // one minute in milliseconds const OneMinute = 60000; /** * Need support both ISO8601 and RFC2822 as in major browsers & NodeJS * RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3 */ const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/; const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const monthNamesShort = monthNames.map(name => name.slice(0, 3)); const dayNames = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; const dayNamesShort = dayNames.map(name => name.slice(0, 3)); /** * 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 LiquidDate { constructor(init, locale, timezone) { this.locale = locale; this.DateTimeFormat = getDateTimeFormat(); this.date = new Date(init); this.timezoneFixed = timezone !== undefined; if (timezone === undefined) { timezone = this.date.getTimezoneOffset(); } this.timezoneOffset = isString(timezone) ? LiquidDate.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() { if (this.timezoneFixed) return this.timezoneName; if (!this.DateTimeFormat) return; return this.DateTimeFormat().resolvedOptions().timeZone; } getLongMonthName() { return this.format({ month: 'long' }) ?? monthNames[this.getMonth()]; } getShortMonthName() { return this.format({ month: 'short' }) ?? monthNamesShort[this.getMonth()]; } getLongWeekdayName() { return this.format({ weekday: 'long' }) ?? dayNames[this.displayDate.getDay()]; } getShortWeekdayName() { return this.format({ weekday: 'short' }) ?? dayNamesShort[this.displayDate.getDay()]; } valid() { return !isNaN(this.getTime()); } format(options) { return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate); } /** * 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, locale) { const m = dateString.match(TIMEZONE_PATTERN); // representing a UTC timestamp if (m && m[1] === 'Z') { return new LiquidDate(+new Date(dateString), locale, 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 LiquidDate(+new Date(dateString), locale, offset); } return new LiquidDate(dateString, locale); } static getTimezoneOffset(timezoneName, 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 Limiter { constructor(resource, limit) { this.base = 0; this.message = `${resource} limit exceeded`; this.limit = limit; } use(count) { count = toNumber(count); assert(this.base + count <= this.limit, this.message); this.base += count; } check(count) { count = toNumber(count); assert(count <= this.limit, this.message); } } 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(); this.args = 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(); } } 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(); } get args() { return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]); } } /** * 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; } } const polyfill = { now: () => Date.now() }; function getPerformance() { return (typeof global === 'object' && global.performance) || (typeof window === 'object' && window.performance) || polyfill; } 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) { ctx.renderLimit.check(getPerformance().now()); 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 (ctx.breakCalled || ctx.continueCalled) 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); ctx.memoryLimit.use(high - low + 1); 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])); } function arrayIncludes(arr, item) { return arr.some(value => equals(value, item)); } 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 = (partial) => require.resolve(partial, { paths: ['.'] }); 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) { str = stringify(str); this.context.memoryLimit.use(str.length); return str.replace(/&|<|>|"|'/g, m => escapeMap[m]); } function xml_escape(str) { return escape.call(this, str); } function unescape(str) { str = stringify(str); this.context.memoryLimit.use(str.length); return str.replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]); } function escape_once(str) { return escape.call(this, unescape.call(this, str)); } function newline_to_br(v) { const str = stringify(v); this.context.memoryLimit.use(str.length); return str.replace(/\r?\n/gm, '<br />\n'); } function strip_html(v) { const str = stringify(v); this.context.memoryLimit.use(str.length); return str.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 }); class MapFS { constructor(mapping) { this.mapping = mapping; this.sep = '/'; } async exists(filepath) { return this.existsSync(filepath); } existsSync(filepath) { return !isNil(this.mapping[filepath]); } async readFile(filepath) { return this.readFileSync(filepath); } readFileSync(filepath) { const content = this.mapping[filepath]; if (isNil(content)) throw new Error(`ENOENT: ${filepath}`); return content; } dirname(filepath) { const segments = filepath.split(this.sep); segments.pop(); return segments.join(this.sep); } resolve(dir, file, ext) { file += ext; if (dir === '.') return file; const segments = dir.split(/\/+/); for (const segment of file.split(this.sep)) { if (segment === '.' || segment === '') continue; else if (segment === '..') { if (segments.length > 1 || segments[0] !== '') segments.pop(); } else segments.push(segment); } return segments.join(this.sep); } } const defaultOptions = { root: ['.'], layouts: ['.'], partials: ['.'], relativeReference: true, jekyllInclude: false, keyValueSeparator: ':', cache: undefined, extname: '', fs: fs, dynamicPartials: true, jsTruthy: false, dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z', locale: '', trimTagRight: false, trimTagLeft: false, trimOutputRight: false, trimOutputLeft: false, greedy: true, tagDelimiterLeft: '{%', tagDelimiterRight: '%}', outputDelimiterLeft: '{{', outputDeli