UNPKG

liquidjs

Version:

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

1,676 lines (1,624 loc) 119 kB
/* * liquidjs@10.5.0, https://github.com/harttle/liquidjs * (c) 2016-2023 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 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; } // 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; } 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(exports.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(exports.TokenKind.Output, value, 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(whole, decimal) { super(exports.TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file); this.whole = whole; this.decimal = decimal; } } class IdentifierToken extends Token { constructor(input, begin, end, file) { super(exports.TokenKind.Word, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.file = file; this.content = this.getText(); } isNumber(allowSign = false) { const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN ? this.begin + 1 : this.begin; for (let i = begin; i < this.end; i++) { if (!(TYPES[this.input.charCodeAt(i)] & NUMBER)) return false; } return true; } } class LiteralToken extends Token { constructor(input, begin, end, file) { super(exports.TokenKind.Literal, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.file = file; this.literal = this.getText(); } } const 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(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, end) { super(exports.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(exports.TokenKind.Filter, input, begin, end, file); this.name = name; this.args = args; } } class HashToken extends Token { constructor(input, begin, end, name, value, file) { super(exports.TokenKind.Hash, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.name = name; this.value = value; this.file = file; } } class QuotedToken extends Token { constructor(input, begin, end, file) { super(exports.TokenKind.Quoted, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.file = file; } } class RangeToken extends Token { constructor(input, begin, end, lhs, rhs, file) { super(exports.TokenKind.Range, input, begin, end, file); this.input = input; this.begin = begin; this.end = end; this.lhs = lhs; this.rhs = rhs; this.file = file; } } class LiquidTagToken extends DelimitedToken { constructor(input, begin, end, options, file) { const value = input.slice(begin, end); super(exports.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 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(); } 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); } } 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 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 = { ...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 this.p; } return -1; } readOutputToken(options = defaultOptions) { const { file, input } = this; const { outputDelimiterRight } = options; const begin = this.p; if (this.readToDelimiter(outputDelimiterRight) === -1) { throw this.mkError(`output ${this.snapshot(begin)} not closed`, begin); } return new OutputToken(input, begin, this.p, options, file); } readEndrawOrRawContent(options) { const { tagDelimiterLeft, tagDelimiterRight } = options; const begin = this.p; let leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length; while (this.p < this.N) { if (this.readIdentifier().getText() !== 'endraw') { leftPos = this.readTo(tagDelimiterLeft) - tagDelimiterLeft.length; continue; } while (this.p <= this.N) { if (this.rmatch(tagDelimiterRight)) { const end = this.p; if (begin === leftPos) { this.rawBeginAt = -1; return new TagToken(this.input, begin, end, options, this.file); } else { this.p = leftPos; return new HTMLToken(this.input, begin, leftPos, this.file); } } if (this.rmatch(tagDelimiterLeft)) break; this.p++; } } throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin); } readLiquidTagTokens(options = defaultOptions) { const tokens = []; while (this.p < this.N) { const token = this.readLiquidTagToken(options); if (token.name) tokens.push(token); } return tokens; } readLiquidTagToken(options) { const { file, input } = this; const begin = this.p; let end = this.N; if (this.readToDelimiter('\n') !== -1) end = this.p; return new LiquidTagToken(input, begin, end, options,