twig
Version:
JS port of the Twig templating language.
842 lines (686 loc) • 27.7 kB
JavaScript
// ## twig.filters.js
//
// This file handles parsing filters.
module.exports = function (Twig) {
// Determine object type
function is(type, obj) {
const clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
Twig.filters = {
// String Filters
upper(value) {
if (typeof value !== 'string') {
return value;
}
return value.toUpperCase();
},
lower(value) {
if (typeof value !== 'string') {
return value;
}
return value.toLowerCase();
},
capitalize(value) {
if (typeof value !== 'string') {
return value;
}
return value.slice(0, 1).toUpperCase() + value.toLowerCase().slice(1);
},
title(value) {
if (typeof value !== 'string') {
return value;
}
return value.toLowerCase().replace(/(^|\s)([a-z])/g, (m, p1, p2) => {
return p1 + p2.toUpperCase();
});
},
length(value) {
if (Twig.lib.is('Array', value) || typeof value === 'string') {
return value.length;
}
if (Twig.lib.is('Object', value)) {
if (value._keys === undefined) {
return Object.keys(value).length;
}
return value._keys.length;
}
return 0;
},
// Array/Object Filters
reverse(value) {
if (is('Array', value)) {
return value.reverse();
}
if (is('String', value)) {
return value.split('').reverse().join('');
}
if (is('Object', value)) {
const keys = value._keys || Object.keys(value).reverse();
value._keys = keys;
return value;
}
},
sort(value) {
if (is('Array', value)) {
return value.sort();
}
if (is('Object', value)) {
// Sorting objects isn't obvious since the order of
// returned keys isn't guaranteed in JavaScript.
// Because of this we use a "hidden" key called _keys to
// store the keys in the order we want to return them.
delete value._keys;
const keys = Object.keys(value);
const sortedKeys = keys.sort((a, b) => {
let a1;
let b1;
// If a and b are comparable, we're fine :-)
if ((value[a] > value[b]) === !(value[a] <= value[b])) {
return value[a] > value[b] ? 1 :
(value[a] < value[b] ? -1 : 0);
}
// If a and b can be parsed as numbers, we can compare
// their numeric value
if (!isNaN(a1 = parseFloat(value[a])) &&
!isNaN(b1 = parseFloat(value[b]))) {
return a1 > b1 ? 1 : (a1 < b1 ? -1 : 0);
}
// If one of the values is a string, we convert the
// other value to string as well
if (typeof value[a] === 'string') {
return value[a] > value[b].toString() ? 1 :
(value[a] < value[b].toString() ? -1 : 0);
}
if (typeof value[b] === 'string') {
return value[a].toString() > value[b] ? 1 :
(value[a].toString() < value[b] ? -1 : 0);
}
// Everything failed - return 'null' as sign, that
// the values are not comparable
return null;
});
value._keys = sortedKeys;
return value;
}
},
keys(value) {
if (value === undefined || value === null) {
return;
}
const keyset = value._keys || Object.keys(value);
const output = [];
keyset.forEach(key => {
if (key === '_keys') {
return;
} // Ignore the _keys property
if (Object.hasOwnProperty.call(value, key)) {
output.push(key);
}
});
return output;
},
url_encode(value) {
if (value === undefined || value === null) {
return;
}
if (Twig.lib.is('Object', value)) {
const serialize = function (obj, prefix) {
const result = [];
const keyset = obj._keys || Object.keys(obj);
keyset.forEach(key => {
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
return;
}
const resultKey = prefix ? prefix + '[' + key + ']' : key;
const resultValue = obj[key];
result.push(
(Twig.lib.is('Object', resultValue) || Array.isArray(resultValue)) ?
serialize(resultValue, resultKey) :
encodeURIComponent(resultKey) + '=' + encodeURIComponent(resultValue)
);
});
return result.join('&');
};
return serialize(value);
}
let result = encodeURIComponent(value);
result = result.replace('\'', '%27');
return result;
},
join(value, params) {
if (value === undefined || value === null) {
return;
}
let joinStr = '';
let output = [];
let keyset = null;
if (params && params[0]) {
joinStr = params[0];
}
if (is('Array', value)) {
output = value;
} else {
keyset = value._keys || Object.keys(value);
keyset.forEach(key => {
if (key === '_keys') {
return;
} // Ignore the _keys property
if (Object.hasOwnProperty.call(value, key)) {
output.push(value[key]);
}
});
}
return output.join(joinStr);
},
default(value, params) {
if (params !== undefined && params.length > 1) {
throw new Twig.Error('default filter expects one argument');
}
if (value === undefined || value === null || value === '') {
if (params === undefined) {
return '';
}
return params[0];
}
return value;
},
json_encode(value) {
if (value === undefined || value === null) {
return 'null';
}
if ((typeof value === 'object') && (is('Array', value))) {
const output = [];
value.forEach(v => {
output.push(Twig.filters.json_encode(v));
});
return '[' + output.join(',') + ']';
}
if ((typeof value === 'object') && (is('Date', value))) {
return '"' + value.toISOString() + '"';
}
if (typeof value === 'object') {
const keyset = value._keys || Object.keys(value);
const output = [];
keyset.forEach(key => {
output.push(JSON.stringify(key) + ':' + Twig.filters.json_encode(value[key]));
});
return '{' + output.join(',') + '}';
}
return JSON.stringify(value);
},
merge(value, params) {
let obj = [];
let arrIndex = 0;
let keyset = [];
// Check to see if all the objects being merged are arrays
if (is('Array', value)) {
params.forEach(param => {
if (!is('Array', param)) {
obj = { };
}
});
} else {
// Create obj as an Object
obj = { };
}
if (!is('Array', obj)) {
obj._keys = [];
}
if (is('Array', value)) {
value.forEach(val => {
if (obj._keys) {
obj._keys.push(arrIndex);
}
obj[arrIndex] = val;
arrIndex++;
});
} else {
keyset = value._keys || Object.keys(value);
keyset.forEach(key => {
obj[key] = value[key];
obj._keys.push(key);
// Handle edge case where a number index in an object is greater than
// the array counter. In such a case, the array counter is increased
// one past the index.
//
// Example {{ ["a", "b"]|merge({"4":"value"}, ["c", "d"])
// Without this, d would have an index of "4" and overwrite the value
// of "value"
const intKey = parseInt(key, 10);
if (!isNaN(intKey) && intKey >= arrIndex) {
arrIndex = intKey + 1;
}
});
}
// Mixin the merge arrays
params.forEach(param => {
if (is('Array', param)) {
param.forEach(val => {
if (obj._keys) {
obj._keys.push(arrIndex);
}
obj[arrIndex] = val;
arrIndex++;
});
} else {
keyset = param._keys || Object.keys(param);
keyset.forEach(key => {
if (!obj[key]) {
obj._keys.push(key);
}
obj[key] = param[key];
const intKey = parseInt(key, 10);
if (!isNaN(intKey) && intKey >= arrIndex) {
arrIndex = intKey + 1;
}
});
}
});
if (params.length === 0) {
throw new Twig.Error('Filter merge expects at least one parameter');
}
return obj;
},
date(value, params) {
const date = Twig.functions.date(value);
const format = params && Boolean(params.length) ? params[0] : 'F j, Y H:i';
return Twig.lib.date(format.replace(/\\\\/g, '\\'), date);
},
date_modify(value, params) {
if (value === undefined || value === null) {
return;
}
if (params === undefined || params.length !== 1) {
throw new Twig.Error('date_modify filter expects 1 argument');
}
const modifyText = params[0];
let time;
if (Twig.lib.is('Date', value)) {
time = Twig.lib.strtotime(modifyText, value.getTime() / 1000);
}
if (Twig.lib.is('String', value)) {
time = Twig.lib.strtotime(modifyText, Twig.lib.strtotime(value));
}
if (Twig.lib.is('Number', value)) {
time = Twig.lib.strtotime(modifyText, value);
}
return new Date(time * 1000);
},
replace(value, params) {
if (value === undefined || value === null) {
return;
}
const pairs = params[0];
let tag;
for (tag in pairs) {
if (Object.hasOwnProperty.call(pairs, tag) && tag !== '_keys') {
value = Twig.lib.replaceAll(value, tag, pairs[tag]);
}
}
return value;
},
format(value, params) {
if (value === undefined || value === null || value === '') {
return;
}
return Twig.lib.vsprintf(value, params);
},
striptags(value, allowed) {
if (value === undefined || value === null) {
return;
}
return Twig.lib.stripTags(value, allowed);
},
escape(value, params) {
if (value === undefined || value === null || value === '') {
return;
}
let strategy = 'html';
if (params && Boolean(params.length) && params[0] !== true) {
strategy = params[0];
}
if (strategy === 'html') {
const rawValue = value.toString().replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
return new Twig.Markup(rawValue, 'html');
}
if (strategy === 'js') {
const rawValue = value.toString();
let result = '';
for (let i = 0; i < rawValue.length; i++) {
if (rawValue[i].match(/^[a-zA-Z0-9,._]$/)) {
result += rawValue[i];
} else {
const char = rawValue.charAt(i);
const charCode = rawValue.charCodeAt(i);
// A few characters have short escape sequences in JSON and JavaScript.
// Escape sequences supported only by JavaScript, not JSON, are ommitted.
// \" is also supported but omitted, because the resulting string is not HTML safe.
const shortMap = {
'\\': '\\\\',
'/': '\\/',
'\u0008': '\\b',
'\u000C': '\\f',
'\u000A': '\\n',
'\u000D': '\\r',
'\u0009': '\\t'
};
if (shortMap[char]) {
result += shortMap[char];
} else {
result += Twig.lib.sprintf('\\u%04s', charCode.toString(16).toUpperCase());
}
}
}
return new Twig.Markup(result, 'js');
}
if (strategy === 'css') {
const rawValue = value.toString();
let result = '';
for (let i = 0; i < rawValue.length; i++) {
if (rawValue[i].match(/^[a-zA-Z0-9]$/)) {
result += rawValue[i];
} else {
const charCode = rawValue.charCodeAt(i);
result += '\\' + charCode.toString(16).toUpperCase() + ' ';
}
}
return new Twig.Markup(result, 'css');
}
if (strategy === 'url') {
const result = Twig.filters.url_encode(value);
return new Twig.Markup(result, 'url');
}
if (strategy === 'html_attr') {
const rawValue = value.toString();
let result = '';
for (let i = 0; i < rawValue.length; i++) {
if (rawValue[i].match(/^[a-zA-Z0-9,.\-_]$/)) {
result += rawValue[i];
} else if (rawValue[i].match(/^[&<>"]$/)) {
result += rawValue[i].replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
} else {
const charCode = rawValue.charCodeAt(i);
// The following replaces characters undefined in HTML with
// the hex entity for the Unicode replacement character.
if (charCode <= 0x1F && charCode !== 0x09 && charCode !== 0x0A && charCode !== 0x0D) {
result += '�';
} else if (charCode < 0x80) {
result += Twig.lib.sprintf('&#x%02s;', charCode.toString(16).toUpperCase());
} else {
result += Twig.lib.sprintf('&#x%04s;', charCode.toString(16).toUpperCase());
}
}
}
return new Twig.Markup(result, 'html_attr');
}
throw new Twig.Error('escape strategy unsupported');
},
/* Alias of escape */
e(value, params) {
return Twig.filters.escape(value, params);
},
nl2br(value) {
if (value === undefined || value === null || value === '') {
return;
}
const linebreakTag = 'BACKSLASH_n_replace';
const br = '<br />' + linebreakTag;
value = Twig.filters.escape(value)
.replace(/\r\n/g, br)
.replace(/\r/g, br)
.replace(/\n/g, br);
value = Twig.lib.replaceAll(value, linebreakTag, '\n');
return new Twig.Markup(value);
},
/**
* Adapted from: http://phpjs.org/functions/number_format:481
*/
number_format(value, params) {
let number = value;
const decimals = (params && params[0]) ? params[0] : undefined;
const dec = (params && params[1] !== undefined) ? params[1] : '.';
const sep = (params && params[2] !== undefined) ? params[2] : ',';
number = (String(number)).replace(/[^0-9+\-Ee.]/g, '');
const n = isFinite(Number(number)) ? Number(number) : 0;
const prec = isFinite(Number(decimals)) ? Math.abs(decimals) : 0;
let s = '';
const toFixedFix = function (n, prec) {
const k = 10 ** prec;
return String(Math.round(n * k) / k);
};
// Fix for IE parseFloat(0.55).toFixed(0) = 0;
s = (prec ? toFixedFix(n, prec) : String(Math.round(n))).split('.');
if (s[0].length > 3) {
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
},
trim(value, params) {
if (value === undefined || value === null) {
return;
}
let str = String(value);
let whitespace;
if (params && params[0]) {
whitespace = String(params[0]);
} else {
whitespace = ' \n\r\t\f\u000B\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000';
}
for (let i = 0; i < str.length; i++) {
if (!whitespace.includes(str.charAt(i))) {
str = str.slice(Math.max(0, i));
break;
}
}
for (let i = str.length - 1; i >= 0; i--) {
if (!whitespace.includes(str.charAt(i))) {
str = str.slice(0, Math.max(0, i + 1));
break;
}
}
return whitespace.includes(str.charAt(0)) ? '' : str;
},
truncate(value, params) {
let length = 30;
let preserve = false;
let separator = '...';
value = String(value);
if (params) {
if (params[0]) {
length = params[0];
}
if (params[1]) {
preserve = params[1];
}
if (params[2]) {
separator = params[2];
}
}
if (value.length > length) {
if (preserve) {
length = value.indexOf(' ', length);
if (length === -1) {
return value;
}
}
value = value.slice(0, length) + separator;
}
return value;
},
slice(value, params) {
if (value === undefined || value === null) {
return;
}
if (params === undefined || params.length === 0) {
throw new Twig.Error('slice filter expects at least 1 argument');
}
// Default to start of string
const start = params[0] || 0;
// Default to length of string
let length = params.length > 1 ? params[1] : value.length;
// Handle negative start values
const startIndex = start >= 0 ? start : Math.max(value.length + start, 0);
// Handle negative length values
if (length < 0) {
length = value.length - startIndex + length;
}
if (Twig.lib.is('Array', value)) {
const output = [];
for (let i = startIndex; i < startIndex + length && i < value.length; i++) {
output.push(value[i]);
}
return output;
}
if (Twig.lib.is('String', value)) {
return value.slice(startIndex, startIndex + length);
}
throw new Twig.Error('slice filter expects value to be an array or string');
},
abs(value) {
if (value === undefined || value === null) {
return;
}
return Math.abs(value);
},
first(value) {
if (is('Array', value)) {
return value[0];
}
if (is('Object', value)) {
if ('_keys' in value) {
return value[value._keys[0]];
}
} else if (typeof value === 'string') {
return value.slice(0, 1);
}
},
split(value, params) {
if (value === undefined || value === null) {
return;
}
if (params === undefined || params.length === 0 || params.length > 2) {
throw new Twig.Error('split filter expects 1 or 2 argument');
}
if (Twig.lib.is('String', value)) {
const delimiter = params[0];
const limit = params[1];
const split = value.split(delimiter);
if (limit === undefined) {
return split;
}
if (limit < 0) {
return value.split(delimiter, split.length + limit);
}
const limitedSplit = [];
if (delimiter === '') {
// Empty delimiter
// "aabbcc"|split('', 2)
// -> ['aa', 'bb', 'cc']
while (split.length > 0) {
let temp = '';
for (let i = 0; i < limit && split.length > 0; i++) {
temp += split.shift();
}
limitedSplit.push(temp);
}
} else {
// Non-empty delimiter
// "one,two,three,four,five"|split(',', 3)
// -> ['one', 'two', 'three,four,five']
for (let i = 0; i < limit - 1 && split.length > 0; i++) {
limitedSplit.push(split.shift());
}
if (split.length > 0) {
limitedSplit.push(split.join(delimiter));
}
}
return limitedSplit;
}
throw new Twig.Error('split filter expects value to be a string');
},
last(value) {
if (Twig.lib.is('Object', value)) {
let keys;
if (value._keys === undefined) {
keys = Object.keys(value);
} else {
keys = value._keys;
}
return value[keys[keys.length - 1]];
}
if (Twig.lib.is('Number', value)) {
return value.toString().slice(-1);
}
// String|array
return value[value.length - 1];
},
raw(value) {
return new Twig.Markup(value || '');
},
batch(items, params) {
let size = params.shift();
const fill = params.shift();
let last;
let missing;
if (!Twig.lib.is('Array', items)) {
throw new Twig.Error('batch filter expects items to be an array');
}
if (!Twig.lib.is('Number', size)) {
throw new Twig.Error('batch filter expects size to be a number');
}
size = Math.ceil(size);
const result = Twig.lib.chunkArray(items, size);
if (fill && items.length % size !== 0) {
last = result.pop();
missing = size - last.length;
while (missing--) {
last.push(fill);
}
result.push(last);
}
return result;
},
round(value, params) {
params = params || [];
const precision = params.length > 0 ? params[0] : 0;
const method = params.length > 1 ? params[1] : 'common';
value = parseFloat(value);
if (precision && !Twig.lib.is('Number', precision)) {
throw new Twig.Error('round filter expects precision to be a number');
}
if (method === 'common') {
return Twig.lib.round(value, precision);
}
if (!Twig.lib.is('Function', Math[method])) {
throw new Twig.Error('round filter expects method to be \'floor\', \'ceil\', or \'common\'');
}
return Math[method](value * (10 ** precision)) / (10 ** precision);
},
spaceless(value) {
return value.replace(/>\s+</g, '><').trim();
}
};
Twig.filter = function (filter, value, params) {
const state = this;
if (!Twig.filters[filter]) {
throw new Twig.Error('Unable to find filter ' + filter);
}
return Twig.filters[filter].call(state, value, params);
};
Twig.filter.extend = function (filter, definition) {
Twig.filters[filter] = definition;
};
return Twig;
};