UNPKG

twing

Version:

First-class Twig engine for the JavaScript ecosystem

1,489 lines (1,387 loc) 425 kB
'use strict'; var Levenshtein = require('levenshtein'); var luxon = require('luxon'); var isPlainObject$1 = require('is-plain-object'); var parser = require('regex-parser'); var phpRange = require('locutus/php/array/range'); var array_chunk = require('locutus/php/array/array_chunk'); var array_merge = require('locutus/php/array/array_merge'); var snakeCase = require('snake-case'); var isObject = require('isobject'); var twigLexer = require('twig-lexer'); var strings = require('locutus/php/strings'); var phpSprintf = require('locutus/php/strings/sprintf'); var phpRawurlencode = require('locutus/php/url/rawurlencode'); var phpOrd = require('locutus/php/strings/ord'); var math = require('locutus/php/math'); var pad = require('pad'); var phpStrtr = require('locutus/php/strings/strtr'); var phpNumberFormat = require('locutus/php/strings/number_format'); var phpHttpBuildQuery = require('locutus/php/url/http_build_query'); var phpUcwords = require('locutus/php/strings/ucwords'); var words = require('capitalize'); var phpStripTags = require('locutus/php/strings/strip_tags'); var phpTrim = require('locutus/php/strings/trim'); var phpLeftTrim = require('locutus/php/strings/ltrim'); var phpRightTrim = require('locutus/php/strings/rtrim'); var phpNl2br = require('locutus/php/strings/nl2br'); var explode = require('locutus/php/strings/explode'); var esrever = require('esrever'); var phpRound = require('locutus/php/math/round'); var phpCeil = require('locutus/php/math/ceil'); var phpFloor = require('locutus/php/math/floor'); var runes = require('runes'); var mt_rand = require('locutus/php/math/mt_rand'); var array_rand = require('locutus/php/array/array_rand'); const createBaseError = (name, message, location, source, previous) => { const baseError = Error(message); baseError.name = name; const error = Object.create(baseError, { location: { get: () => location }, source: { get: () => source }, previous: { value: previous }, rootMessage: { value: message }, appendMessage: { value: (value) => { message += value; updateRepresentation(); } } }); const updateRepresentation = () => { let representation = message; let dot = false; if (representation.slice(-1) === '.') { representation = representation.slice(0, -1); dot = true; } let questionMark = false; if (representation.slice(-1) === '?') { representation = representation.slice(0, -1); questionMark = true; } representation += ` in "${source.name}"`; const { line, column } = location; representation += ` at line ${line}, column ${column}`; if (dot) { representation += '.'; } if (questionMark) { representation += '?'; } baseError.message = representation; }; updateRepresentation(); Error.captureStackTrace(error, createBaseError); return error; }; const parsingErrorName = 'TwingParsingError'; const createParsingError = (message, location, source, previous) => { const baseError = createBaseError(parsingErrorName, message, location, source, previous); Error.captureStackTrace(baseError, createParsingError); return Object.create(baseError, { addSuggestions: { value: (name, items) => { const alternatives = []; for (const item of items) { const levenshtein = new Levenshtein(name, item); if (levenshtein.distance <= (name.length / 3) || item.indexOf(name) > -1) { alternatives.push(item); } } if (alternatives.length < 1) { return; } alternatives.sort(); baseError.appendMessage(` Did you mean "${alternatives.join(', ')}"?`); } } }); }; const runtimeErrorName = 'TwingRuntimeError'; const createRuntimeError = (message, location, source, previous) => { const error = createBaseError(runtimeErrorName, message, location, source, previous); Error.captureStackTrace(error, createRuntimeError); return error; }; const isATwingError = (candidate) => { return [ parsingErrorName, runtimeErrorName ].includes(candidate.name); }; const createTemplateLoadingError = (names) => { let message; if (names.length === 1) { const name = names[0]; message = `Unable to find template "${name ? name : ''}".`; } else { message = `Unable to find one of the following templates: "${names.join('", "')}".`; } const error = Error(message); Error.captureStackTrace(error, createTemplateLoadingError); return error; }; const createSource = (name, code) => { return { get code() { return code; }, get name() { return name; } }; }; const createArrayLoader = (templates) => { const loader = { setTemplate: (name, template) => { templates[name] = template; }, getSource: (name, from) => { return loader.exists(name, from) .then((exists) => { if (!exists) { return null; } return createSource(name, templates[name]); }); }, exists(name) { return Promise.resolve(templates[name] !== undefined); }, resolve: (name, from) => { return loader.exists(name, from) .then((exists) => { if (!exists) { return null; } return name; }); }, isFresh: () => { return Promise.resolve(true); } }; return loader; }; const createSynchronousArrayLoader = (templates) => { const loader = { setTemplate: (name, template) => { templates[name] = template; }, getSource: (name, from) => { if (loader.exists(name, from)) { return createSource(name, templates[name]); } return null; }, exists(name) { return templates[name] !== undefined; }, resolve: (name, from) => { if (loader.exists(name, from)) { return name; } return null; }, isFresh: () => { return true; } }; return loader; }; const createChainLoader = (loaders) => { let existsCache = new Map(); const addLoader = (loader) => { loaders.push(loader); existsCache = new Map(); }; const loader = { get loaders() { return loaders; }, addLoader, exists: (name, from) => { const cachedResult = existsCache.get(name); if (cachedResult) { return Promise.resolve(cachedResult); } const existsAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; return loader.exists(name, from) .then((exists) => { existsCache.set(name, exists); if (!exists) { return existsAtIndex(index + 1); } else { return true; } }); } else { return Promise.resolve(false); } }; return existsAtIndex(0).then((exists) => { existsCache.set(name, exists); return exists; }); }, resolve: (name, from) => { const resolveAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; return loader.exists(name, from) .then((exists) => { if (!exists) { return resolveAtIndex(index + 1); } else { return loader.resolve(name, from); } }) .then((key) => { if (key === null) { return resolveAtIndex(index + 1); } return key; }); } else { return Promise.resolve(null); } }; return resolveAtIndex(0) .then((key) => { if (key) { return key; } else { return null; } }); }, getSource: (name, from) => { const getSourceContextAtIndex = (index) => { if (index < loaders.length) { let loader = loaders[index]; return loader.getSource(name, from) .then((source) => { if (source === null) { return getSourceContextAtIndex(index + 1); } return source; }); } else { return Promise.resolve(null); } }; return getSourceContextAtIndex(0) .then((source) => { if (source) { return source; } else { return null; } }); }, isFresh: (name, time, from) => { const isFreshAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; return loader.isFresh(name, time, from) .then((isFresh) => { if (isFresh === null) { return isFreshAtIndex(index + 1); } return isFresh; }); } else { return Promise.resolve(null); } }; return isFreshAtIndex(0); } }; return loader; }; const createSynchronousChainLoader = (loaders) => { let existsCache = new Map(); const addLoader = (loader) => { loaders.push(loader); existsCache = new Map(); }; const loader = { get loaders() { return loaders; }, addLoader, exists: (name, from) => { const cachedResult = existsCache.get(name); if (cachedResult) { return cachedResult; } const existsAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; const exists = loader.exists(name, from); existsCache.set(name, exists); if (!exists) { return existsAtIndex(index + 1); } else { return true; } } else { return false; } }; const exists = existsAtIndex(0); existsCache.set(name, exists); return exists; }, resolve: (name, from) => { const resolveAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; const exists = loader.exists(name, from); const key = exists ? loader.resolve(name, from) : resolveAtIndex(index + 1); if (key === null) { return resolveAtIndex(index + 1); } return key; } else { return null; } }; const key = resolveAtIndex(0); if (key) { return key; } else { return null; } }, getSource: (name, from) => { const getSourceContextAtIndex = (index) => { if (index < loaders.length) { let loader = loaders[index]; const source = loader.getSource(name, from); if (source === null) { return getSourceContextAtIndex(index + 1); } return source; } else { return null; } }; const source = getSourceContextAtIndex(0); if (source) { return source; } else { return null; } }, isFresh: (name, time, from) => { const isFreshAtIndex = (index) => { if (index < loaders.length) { const loader = loaders[index]; const isFresh = loader.isFresh(name, time, from); if (isFresh === null) { return isFreshAtIndex(index + 1); } return isFresh; } else { return null; } }; return isFreshAtIndex(0); } }; return loader; }; const isAMarkup = (candidate) => { return candidate !== null && candidate !== undefined && candidate.charset !== undefined && candidate.content !== undefined && candidate.count !== undefined // todo: we should not test getter values but actual property existence && candidate.toJSON !== undefined && candidate.toString !== undefined; }; const createMarkup = (content, charset = 'UTF-8') => { return { get content() { return content; }, get charset() { return charset; }, get count() { return content.length; }, toString() { return content.toString(); }, toJSON() { return content.toString(); } }; }; const getChildren = (node) => { return Object.entries(node.children); }; const getChildrenCount = (node) => { return Object.keys(node.children).length; }; const createBaseNode = (type, attributes = {}, children = {}, line = 0, column = 0, tag = null) => { return { attributes, children, column, line, tag, type }; }; /** * Create a node acting as a container for the passed list of indexed nodes. * * @param children The children of the created node * @param line The line of the created node * @param column The column of the created node * @param tag The tag of the created node */ const createNode = (children = {}, line = 0, column = 0, tag = null) => { return createBaseNode(null, {}, children, line, column, tag); }; const createApplyNode = (filters, body, line, column) => { return createBaseNode("apply", {}, { body, filters }, line, column, 'apply'); }; const createAutoEscapeNode = (strategy, body, line, column, tag) => { return createBaseNode("auto_escape", { strategy }, { body }, line, column, tag); }; const createBlockNode = (name, body, line, column, tag = null) => { return createBaseNode("block", { name }, { body }, line, column, tag); }; const createBlockReferenceNode = (name, line, column, tag) => { return createBaseNode("block_reference", { name }, {}, line, column, tag); }; const createCheckSecurityNode = (usedFilters, usedTags, usedFunctions, line, column) => { return createBaseNode("check_security", { usedFilters, usedTags, usedFunctions }, {}, line, column); }; const createCheckToStringNode = (value, line, column) => { return createBaseNode("check_to_string", {}, { value }, line, column); }; const createCommentNode = (data, line, column) => { return createBaseNode("comment", { data }, {}, line, column); }; const createDeprecatedNode = (message, line, column, tag) => { return createBaseNode("deprecated", {}, { message }, line, column, tag); }; const createDoNode = (body, line, column, tag) => { return createBaseNode("do", {}, { body }, line, column, tag); }; const createFlushNode = (line, column, tag) => { return createBaseNode("flush", {}, {}, line, column, tag); }; const createForLoopNode = (line, column, tag) => { return createBaseNode("for_loop", { hasAnIf: false, hasAnElse: false }, {}, line, column, tag); }; const createIfNode = (testNode, elseNode, line, column, tag = null) => { const children = { tests: testNode }; if (elseNode) { children.else = elseNode; } return createBaseNode('if', {}, children, line, column, tag); }; const createForNode = (keyTarget, valueTarget, sequence, ifExpression, body, elseNode, line, column, tag) => { const loop = createForLoopNode(line, column, tag); const bodyChildren = {}; let i = 0; bodyChildren[i++] = body; bodyChildren[i++] = loop; let actualBody = createNode(bodyChildren, line, column); if (ifExpression) { const ifChildren = {}; let i = 0; ifChildren[i++] = ifExpression; ifChildren[i++] = actualBody; actualBody = createIfNode(createNode(ifChildren, line, column), null, line, column); loop.attributes.hasAnIf = true; } const children = { keyTarget: keyTarget, valueTarget: valueTarget, sequence: sequence, body: actualBody }; if (elseNode) { children.else = elseNode; loop.attributes.hasAnElse = true; } return createBaseNode("for", { hasAnIf: ifExpression !== null }, children, line, column, tag); }; const createImportNode = (templateName, alias, global, line, column, tag) => { return createBaseNode("import", { global }, { templateName, alias }, line, column, tag); }; const createBaseIncludeNode = (type, attributes, children, line, column, tag) => { return createBaseNode(type, attributes, children, line, column, tag); }; const createLineNode = (data, line, column, tag) => { return createBaseNode("line", { data }, {}, line, column, tag); }; const VARARGS_NAME = 'varargs'; const createMacroNode = (name, body, macroArguments, line, column, tag) => { return createBaseNode("macro", { name }, { body, arguments: macroArguments }, line, column, tag); }; const createPrintNode = (expression, line, column) => { return createBaseNode("print", {}, { expression: expression }, line, column, null); }; const createSandboxNode = (body, line, column, tag) => { return createBaseNode("sandbox", {}, { body }, line, column, tag); }; const createBaseExpressionNode = createBaseNode; const createConstantNode = (value, line, column) => { return createBaseExpressionNode("constant", { value }, {}, line, column); }; const createSetNode = (captures, names, values, line, column, tag) => { const setNode = createBaseNode("set", { captures }, { names, values }, line, column, tag); /* * Optimizes the node when capture is used for a large block of text. * * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig_Markup("foo"); */ if (setNode.attributes.captures) { const values = setNode.children.values; if (values.type === "text") { setNode.children.values = createNode({ 0: createConstantNode(values.attributes.data, values.line, values.column) }, values.line, values.column); setNode.attributes.captures = false; } } return setNode; }; const createSpacelessNode = (body, line, column, tag) => { return createBaseNode("spaceless", {}, { body }, line, column, tag); }; const createTemplateNode = (body, parent, blocks, macros, traits, embeddedTemplates, source, line, column) => { const children = { body, blocks, macros, traits, securityCheck: createNode() }; if (parent !== null) { children.parent = parent; } const baseNode = createBaseNode("template", { index: 0, source }, children, line, column); return Object.assign(Object.assign({}, baseNode), { get embeddedTemplates() { return embeddedTemplates; } }); }; const createBaseTextNode = (type, data, line, column, tag = null) => { return createBaseNode(type, { data }, {}, line, column, tag); }; const createTextNode = (data, line, column) => createBaseTextNode("text", data, line, column); const createTraitNode = (template, targets, line, column) => { return Object.assign({}, createBaseNode("trait", {}, { template, targets }, line, column)); }; const createVerbatimNode = (data, line, column, tag) => createBaseTextNode("verbatim", data, line, column, tag); const createWithNode = (body, variables, only, line, column, tag) => { const children = { body }; if (variables) { children.variables = variables; } return createBaseNode("with", { only }, children, line, column, tag); }; const getRecordSize = (record) => { return Object.keys(record).length; }; const pushToRecord = (record, value) => { const size = getRecordSize(record); record[size] = value; }; const createBaseArrayNode = (type, elements, line, column) => { const children = {}; for (const { key, value } of elements) { pushToRecord(children, key); pushToRecord(children, value); } return createBaseExpressionNode(type, {}, children, line, column); }; const createArrayNode = (elements, line, column) => { let index = 0; const baseNode = createBaseArrayNode("array", elements.map(({ key, value }) => { return { key: key || createConstantNode(index++, line, column), value }; }), line, column); return Object.assign({}, baseNode); }; const createArrowFunctionNode = (body, names, line, column) => { return createBaseExpressionNode("arrow_function", {}, { body, names }, line, column); }; // todo: probably a useless node const createAssignmentNode = (name, line, column) => { return createBaseNode("assignment", { name }, {}, line, column); }; const createAttributeAccessorNode = (target, attribute, methodArguments, type, line, column) => { return createBaseExpressionNode("attribute_accessor", { isOptimizable: true, type, shouldTestExistence: false }, { target, attribute, arguments: methodArguments }, line, column); }; const cloneGetAttributeNode = (attributeAccessorNode) => { const { children, attributes, line, column } = attributeAccessorNode; const { arguments: methodArguments, attribute, target } = children; const { type } = attributes; return createAttributeAccessorNode(target, attribute, methodArguments, type, line, column); }; const createBaseBinaryNode = (type, operands, line, column) => { const baseNode = createBaseExpressionNode(type, {}, { left: operands[0], right: operands[1] }, line, column); return Object.assign({}, baseNode); }; const createBinaryNodeFactory = (type) => { const factory = (operands, line, column) => { const baseNode = createBaseBinaryNode(type, operands, line, column); return Object.assign({}, baseNode); }; return factory; }; const createBlockFunctionNode = (name, template, line, column, tag) => { const children = { name }; if (template) { children.template = template; } return createBaseExpressionNode("block_function", { shouldTestExistence: false }, children, line, column, tag); }; const cloneBlockReferenceExpressionNode = (blockFunctionNode) => { return createBlockFunctionNode(blockFunctionNode.children.name, blockFunctionNode.children.template || null, blockFunctionNode.line, blockFunctionNode.column); }; const createBaseCallNode = (type, operatorName, operand, callArguments, line, column) => { let children = { arguments: callArguments }; if (operand !== null) { children.operand = operand; } return createBaseExpressionNode(type, { operatorName }, children, line, column); }; const createBaseConditionalNode = (type, expr1, expr2, expr3, line, column) => { return createBaseExpressionNode(type, {}, { expr1, expr2, expr3 }, line, column); }; const createConditionalNode = (expr1, expr2, expr3, line, column) => createBaseConditionalNode("conditional", expr1, expr2, expr3, line, column); const createEscapeNode = (body, strategy) => { return createBaseExpressionNode("escape", { strategy }, { body }, body.line, body.column); }; const createHashNode = (elements, line, column) => { return createBaseArrayNode("hash", elements, line, column); }; const createMethodCallNode = (operand, methodName, methodArguments, line, column) => { return createBaseExpressionNode("method_call", { methodName, shouldTestExistence: false }, { operand, arguments: methodArguments }, line, column); }; const cloneMethodCallNode = (methodCallNode) => { return createMethodCallNode(methodCallNode.children.operand, methodCallNode.attributes.methodName, methodCallNode.children.arguments, methodCallNode.line, methodCallNode.column); }; const createNameNode = (name, line, column) => { const attributes = { name, isAlwaysDefined: false, shouldIgnoreStrictCheck: false, shouldTestExistence: false }; return createBaseNode("name", attributes, {}, line, column); }; const cloneNameNode = (nameNode) => { return createNameNode(nameNode.attributes.name, nameNode.line, nameNode.column); }; const createUnaryNodeFactory = (type) => { const factory = (operand, line, column) => { const baseNode = createBaseUnaryNode(type, operand, line, column); return Object.assign({}, baseNode); }; return factory; }; const createBaseUnaryNode = (type, operand, line, column) => { const baseNode = createBaseExpressionNode(type, {}, { operand }, line, column); return Object.assign({}, baseNode); }; const createNotNode = createUnaryNodeFactory("not"); const createAndNode = createBinaryNodeFactory("and"); const createTestNode = (operand, testName, testArguments, line, column) => { return createBaseCallNode("test", testName, operand, testArguments, line, column); }; const createNullishCoalescingNode = (operands, line, column) => { const [left, right] = operands; if (left.type === "name") { left.attributes.isAlwaysDefined = true; } const testNode = createAndNode([ createTestNode(left, "defined", createArrayNode([], line, column), line, column), createNotNode(createTestNode(left, 'null', createArrayNode([], line, column), line, column), line, column) ], line, column); return createBaseConditionalNode("nullish_coalescing", testNode, left, right, line, column); }; const createParentFunctionNode = (name, line, column) => { return createBaseExpressionNode("parent_function", { name, //output: false }, {}, line, column); }; const createSpreadNode = (iterable, line, column) => { return createBaseExpressionNode("spread", {}, { iterable }, line, column); }; const createAddNode = createBinaryNodeFactory("add"); const createBitwiseAndNode = createBinaryNodeFactory("bitwise_and"); const createBitwiseOrNode = createBinaryNodeFactory("bitwise_or"); const createBitwiseXorNode = createBinaryNodeFactory("bitwise_xor"); const createConcatenateNode = createBinaryNodeFactory("concatenate"); const createDivideAndFloorNode = createBinaryNodeFactory("divide_and_floor"); const createDivideNode = createBinaryNodeFactory("divide"); const createEndsWithNode = createBinaryNodeFactory("ends_with"); const createHasEveryNode = createBinaryNodeFactory("has_every"); const createHasSomeNode = createBinaryNodeFactory("has_some"); const createIsEqualNode = createBinaryNodeFactory("is_equal_to"); const createIsGreaterThanNode = createBinaryNodeFactory("is_greater_than"); const createIsGreaterThanOrEqualToNode = createBinaryNodeFactory("is_greater_than_or_equal_to"); const createIsInNode = createBinaryNodeFactory("is_in"); const createIsLessThanNode = createBinaryNodeFactory("is_less_than"); const createIsLessThanOrEqualToNode = createBinaryNodeFactory("is_less_than_or_equal_to"); const createIsNotEqualToNode = createBinaryNodeFactory("is_not_equal_to"); const createIsNotInNode = createBinaryNodeFactory("is_not_in"); const createMatchesNode = createBinaryNodeFactory("matches"); const createModuloNode = createBinaryNodeFactory("modulo"); const createMultiplyNode = createBinaryNodeFactory("multiply"); const createOrNode = createBinaryNodeFactory("or"); const createPowerNode = createBinaryNodeFactory("power"); const createRangeNode = createBinaryNodeFactory("range"); const createStartsWithNode = createBinaryNodeFactory("starts_with"); const createSubtractNode = createBinaryNodeFactory("subtract"); const createFilterNode = (operand, filterName, filterArguments, line, column) => { return createBaseCallNode("filter", filterName, operand, filterArguments, line, column); }; const createFunctionNode = (functionName, functionArguments, line, column) => { return createBaseCallNode("function", functionName, null, functionArguments, line, column); }; const createNegativeNode = createUnaryNodeFactory("negative"); const createPositiveNode = createUnaryNodeFactory("positive"); const createEmbedNode = (attributes, children, line, column, tag) => { return createBaseIncludeNode("embed", attributes, children, line, column, tag); }; const createIncludeNode = (attributes, children, line, column, tag) => { return createBaseIncludeNode("include", attributes, children, line, column, tag); }; /** * Converts input to Map. * * @param {*} thing * @returns {Map<any, any>} */ const iteratorToMap = (thing) => { if (thing.entries) { return new Map(thing.entries()); } else { const result = new Map(); if (typeof thing[Symbol.iterator] === 'function') { let i = 0; for (const value of thing) { result.set(i++, value); } } else { for (const key in thing) { result.set(key, thing[key]); } } return result; } }; const iterableToMap = iteratorToMap; function isAMapLike(candidate) { return candidate !== null && candidate !== undefined && candidate.delete !== undefined && candidate.get !== undefined && candidate.has !== undefined && candidate.set !== undefined && candidate.entries !== undefined; } const every = async (iterable, comparator) => { if (Array.isArray(iterable)) { iterable = iteratorToMap(iterable); } for (const [key, value] of iterable) { if (await comparator(value, key) === false) { return false; } } return true; }; const everySynchronously = (iterable, comparator) => { if (Array.isArray(iterable)) { iterable = iteratorToMap(iterable); } for (const [key, value] of iterable) { if (comparator(value, key) === false) { return false; } } return true; }; const some = async (iterable, comparator) => { if (Array.isArray(iterable)) { iterable = iteratorToMap(iterable); } for (const [key, value] of iterable) { if (await comparator(value, key) === true) { return true; } } return false; }; const someSynchronously = (iterable, comparator) => { if (Array.isArray(iterable)) { iterable = iteratorToMap(iterable); } for (const [key, value] of iterable) { if (comparator(value, key) === true) { return true; } } return false; }; /** * Compare by conforming to PHP loose comparisons rules * * @see http://php.net/manual/en/types.comparisons.php * @see https://stackoverflow.com/questions/47969711/php-algorithm-loose-equality-comparison */ function compare(firstOperand, secondOperand) { // Array<any> if (Array.isArray(firstOperand)) { firstOperand = iteratorToMap(firstOperand); } if (Array.isArray(secondOperand)) { secondOperand = iteratorToMap(secondOperand); } // null if (firstOperand === null) { return compareToNull(secondOperand); } if (secondOperand === null) { return compareToNull(firstOperand); } // boolean if (typeof firstOperand === 'boolean') { return compareToBoolean(firstOperand, secondOperand); } if (typeof secondOperand === 'boolean') { return compareToBoolean(secondOperand, firstOperand); } // number if (typeof firstOperand === 'number') { return compareToNumber(firstOperand, secondOperand); } if (typeof secondOperand === 'number') { return compareToNumber(secondOperand, firstOperand); } // TwingMarkup if (isAMarkup(firstOperand)) { firstOperand = firstOperand.toString(); } if (isAMarkup(secondOperand)) { secondOperand = secondOperand.toString(); } // Map if (isAMapLike(firstOperand)) { return compareToMap(firstOperand, secondOperand); } // string if (typeof firstOperand === 'string') { return compareToString(firstOperand, secondOperand); } // date if (firstOperand instanceof luxon.DateTime) { return compareToDateTime(firstOperand, secondOperand); } // fallback to strict comparison return firstOperand === secondOperand; } /** * Compare a Map to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ [] │ ["php"] | "php" │ "" │ * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────────┼───────┼───────┤ * │ [] │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ TRUE │ FALSE │ FALSE │ FALSE | * │ ["php"] │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE | * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┘ */ function compareToMap(firstOperand, secondOperand) { if (firstOperand.size === 0) { return isAMapLike(secondOperand) && (secondOperand.size === 0); } else { if (!isAMapLike(secondOperand)) { return false; } else if (firstOperand.size !== secondOperand.size) { return false; } let result = false; for (let [i, valueItem] of firstOperand) { let compareItem = secondOperand.get(i); result = compare(valueItem, compareItem); if (!result) { break; } } return result; } } /** * Compare a boolean to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ array() │ "php" │ "" │ * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────── ─┼───────┼───────┤ * │ TRUE │ TRUE │ FALSE │ TRUE │ FALSE │ TRUE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ * │ FALSE │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ TRUE │ TRUE │ FALSE │ TRUE │ * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┘ */ function compareToBoolean(firstOperand, secondOperand) { if (secondOperand instanceof luxon.DateTime) { return firstOperand === true; } if (typeof secondOperand === 'boolean') { return firstOperand === secondOperand; } if (typeof secondOperand === 'number') { return firstOperand === (secondOperand !== 0); } if (typeof secondOperand === 'string') { if (secondOperand.length > 1) { return firstOperand; } else { let float = parseFloat(secondOperand); if (!isNaN(float)) { return firstOperand === (float !== 0); } else { return firstOperand === (secondOperand.length > 0); } } } if (isAMapLike(secondOperand)) { return firstOperand === secondOperand.size > 0; } return firstOperand === true; } /** * Compare a DateTime to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ [] │ ["php"] | "php" │ "" │ NOW | LATER | * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────────┼───────┼───────┼───────┼───────┤ * │ NOW │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ * │ LATER │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┴───────┴───────┘ */ function compareToDateTime(firstOperand, secondOperand) { if (secondOperand instanceof luxon.DateTime) { return firstOperand.valueOf() === secondOperand.valueOf(); } return false; } /** * Compare null to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ [] │ ["php"] | "php" │ "" │ * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────────┼───────┼───────┤ * │ NULL │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ TRUE │ FALSE │ FALSE │ TRUE | * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┘ */ function compareToNull(value) { if (typeof value === 'boolean') { return (value === false); } if (typeof value === 'number') { return value === 0; } if (typeof value === 'string') { return value.length < 1; } if (value === null) { return true; } if (isAMapLike(value)) { return value.size < 1; } return false; } /** * Compare a number to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ array() │ "php" │ "" │ * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────── ─┼───────┼───────┤ * │ 1 │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ * │ 0 │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ TRUE │ TRUE │ * │ -1 │ TRUE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┘ */ function compareToNumber(firstOperand, secondOperand) { if (typeof secondOperand === 'number') { return firstOperand === secondOperand; } if (typeof secondOperand === 'string') { let float = parseFloat(secondOperand); if (float) { return firstOperand === float; } else { return firstOperand === 0; } } // date if (secondOperand instanceof luxon.DateTime) { return firstOperand === 1; } return false; } /** * Compare a string to something else by conforming to PHP loose comparisons rules * ┌─────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬─────────┬───────┬───────┐ * │ │ TRUE │ FALSE │ 1 │ 0 │ -1 │ "1" │ "0" │ "-1" │ NULL │ array() │ "php" │ "" │ * ├─────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼─────── ─┼───────┼───────┤ * │ "1" │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ * │ "0" │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ * │ "-1" │ TRUE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ * │ "" │ FALSE │ TRUE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ TRUE │ * │ "php" │ TRUE │ FALSE │ FALSE │ TRUE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ FALSE │ TRUE │ FALSE │ * └─────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴─────────┴───────┴───────┘ */ function compareToString(firstOperand, secondOperand) { if (typeof secondOperand === 'string') { return firstOperand === secondOperand; } return false; } const concatenate = (object1, object2) => { if ((object1 === null) || (object1 === undefined)) { object1 = ''; } if ((object2 === null) || (object2 === undefined)) { object2 = ''; } return String(object1) + String(object2); }; function isPlainObject(thing) { return isPlainObject$1(thing); } /** * Check that an object is traversable in the sense of PHP, * i.e. implements PHP Traversable interface * * @param value * @returns {boolean} */ function isTraversable(value) { if (isPlainObject(value)) { return true; } if ((value !== null) && (value !== undefined)) { if (typeof value === 'string') { return false; } if (typeof value['entries'] === 'function') { return true; } if ((typeof value[Symbol.iterator] === 'function') || (typeof value['next'] === 'function')) { return true; } } return false; } function iteratorToArray(value) { if (Array.isArray(value)) { return value; } else { let result = []; if (value.entries) { for (let entry of value.entries()) { result.push(entry[1]); } } else if (typeof value[Symbol.iterator] === 'function') { for (let entry of value) { result.push(entry); } } else if (typeof value['next'] === 'function') { let next; while ((next = value.next()) && !next.done) { result.push(next.value); } } else { for (let k in value) { result.push(value[k]); } } return result; } } function isIn(value, compare$1) { let result = false; if (isAMarkup(value)) { value = value.toString(); } if (isAMarkup(compare$1)) { compare$1 = compare$1.toString(); } if (isAMapLike(compare$1)) { for (let [, item] of compare$1) { if (compare(item, value)) { result = true; break; } } } else if (typeof compare$1 === 'string' && (typeof value === 'string' || typeof value === 'number')) { result = (value === '' || compare$1.includes('' + value)); } else if (isTraversable(compare$1)) { for (let item of iteratorToArray(compare$1)) { if (compare(item, value)) { result = true; break; } } } return result; } /** * @param {string} input * @returns {RegExp} */ function parseRegularExpression(input) { return parser(input); } function createRange(low, high, step) { let range = phpRange(low, high, step); return iteratorToMap(range); } const executeBinaryNode = async (node, executionContext) => { const { left, right } = node.children; const { nodeExecutor: execute, template } = executionContext; switch (node.type) { case "add": { const leftValue = await execute(left, executionContext); const leftValueType = typeof leftValue; if (leftValueType === "string") { return Promise.reject(createRuntimeError(`Unsupported operand type "${leftValueType}"`, left, template.source)); } const rightValue = await execute(right, executionContext); const rightValueType = typeof rightValue; if (rightValueType === "string") { return Promise.reject(createRuntimeError(`Unsupported operand type "${rightValueType}"`, right, template.source)); } return leftValue + rightValue; } case "and": { return !!(await execute(left, executionContext) && await execute(right, executionContext)); } case "bitwise_and": { return await execute(left, executionContext) & await execute(right, executionContext); } case "bitwise_or": { return await execute(left, executionContext) | await execute(right, executionContext); } case "bitwise_xor": { return await execute(left, executionContext) ^ await execute(right, executionContext); } case "concatenate": { const leftValue = await execute(left, executionContext); const rightValue = await execute(right, executionContext); return concatenate(leftValue, rightValue); } case "divide": { return await execute(left, executionContext) / await execute(right, executionContext); } case "divide_and_floor": { return Math.floor(await execute(left, executionContext) / await execute(right, executionContext)); } case "ends_with": { const leftValue = await execute(left, executionContext); if (typeof leftValue !== "string") { return false; } const rightValue = await execute(right, executionContext); if (typeof rightValue !== "string") { return false; } return rightValue.length < 1 || leftValue.endsWith(rightValue); } case "has_every": { const leftValue = await execute(left, executionContext); const rightValue = await execute(right, executionContext); if (typeof rightValue !== "function") { return Promise.resolve(true); } if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) { return Promise.resolve(true); } return every(leftValue, rightValue); } case "has_some": { const leftValue = await execute(left, executionContext); const rightValue = await execute(right, executionContext); if (typeof rightValue !== "function") { return Promise.resolve(false); } if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) { return Promise.resolve(false); } return some(leftValue, rightValue); } case "is_equal_to": { const leftValue = await execute(left, executionContext); const rightValue = await execute(right, executionContext); return compare(leftValue, rightValue); } case "is_greater_than": { return await execute(left, executionContext) > await execute(right, executionContext); } case "is_greater_than_or_equal_to": { return await execute(left, executionContext) >= await execute(right, executionContext); } case "is_in": { return isIn(await execute(left, executionContext), await execute(right, executionContext)); } case "is_less_than": { return await execute(left, executionContext) < await execute(right, executionContext); } case "is_le