liquidjs
Version:
A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.
1,676 lines (1,624 loc) • 119 kB
JavaScript
/*
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
const unescapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'"
};
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,