freemarker-parser
Version:
Freemarker Parser is a javascript implementation of the Freemarker
598 lines • 21.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParamsParser = void 0;
const AbstractTokenizer_1 = __importDefault(require("./AbstractTokenizer"));
const CharCodes_1 = __importDefault(require("./enum/CharCodes"));
const Operators_1 = require("./enum/Operators");
const ParamNames_1 = __importDefault(require("./enum/ParamNames"));
const ParseError_1 = __importDefault(require("./errors/ParseError"));
const Chars_1 = require("./utils/Chars");
function isIBiopInfo(object) {
return !!object && 'prec' in object;
}
function isAllParamTypes(object) {
return !!object && 'type' in object;
}
/**
* Returns the precedence of a binary operator or `0` if it isn't a binary operator
* @param opVal
*/
function binaryPrecedence(opVal) {
return Operators_1.BinaryOps[opVal] || 0;
}
function createAssignmentExpression(operator, left, right) {
return { type: ParamNames_1.default.AssignmentExpression, operator, left, right };
}
function createBuiltInExpression(operator, left, right) {
return { type: ParamNames_1.default.BuiltInExpression, operator, left, right };
}
function createUpdateExpression(operator, argument, prefix = true) {
return { type: ParamNames_1.default.UpdateExpression, operator, argument, prefix };
}
/**
* Utility function (gets called from multiple places)
* Also note that `a && b` and `a || b` are *logical* expressions, not binary expressions
*/
function createBinaryExpression(operator, left, right) {
switch (operator) {
case Operators_1.Operators.EQUALS:
case Operators_1.Operators.PLUS_EQUALS:
case Operators_1.Operators.MINUS_EQUALS:
case Operators_1.Operators.TIMES_EQUALS:
case Operators_1.Operators.DIV_EQUALS:
case Operators_1.Operators.MOD_EQUALS:
return createAssignmentExpression(operator, left, right);
case Operators_1.Operators.BUILT_IN:
return createBuiltInExpression(operator, left, right);
case Operators_1.Operators.OR:
case Operators_1.Operators.AND:
return { type: ParamNames_1.default.LogicalExpression, operator, left, right };
default:
return { type: ParamNames_1.default.BinaryExpression, operator, left, right };
}
}
function createUnaryExpression(operator, argument, prefix = true) {
if (!argument) {
throw new ParseError_1.default(`Missing argument in ${prefix ? 'before' : 'after'} '${operator}'`, { start: 0, end: 0 });
}
switch (operator) {
case Operators_1.Operators.PLUS_PLUS:
case Operators_1.Operators.MINUS_MINUS:
return createUpdateExpression(operator, argument, prefix);
default:
return { type: ParamNames_1.default.UnaryExpression, operator, argument, prefix };
}
}
class ParamsParser extends AbstractTokenizer_1.default {
constructor(template) {
super();
super.init(template);
}
parseExpressions() {
let node;
const nodes = [];
while (this.index < this.length) {
// Try to gobble each expression individually
node = this.parseExpression();
if (node) {
// If we weren't able to find a binary expression and are out of room, then
// the expression passed in probably has too much
nodes.push(node);
if (this.charCodeAt(this.index) === CharCodes_1.default.Comma) {
++this.index;
}
}
else if (this.index < this.length) {
throw new ParseError_1.default(`Unexpected "${this.charAt(this.index)}"`, {
start: this.index,
end: this.index,
});
}
}
// If there's only one expression just try returning the expression
if (nodes.length === 1) {
return nodes[0];
}
else {
return {
type: ParamNames_1.default.Compound,
body: nodes,
};
}
}
/**
* The main parsing function. Much of this code is dedicated to ternary expressions
*/
parseExpression() {
const test = this.parseBinaryExpression();
this.parseSpaces();
return test;
}
/**
* Push `index` up to the next non-space character
*/
parseSpaces() {
let ch = this.charCodeAt(this.index);
// space or tab
while (Chars_1.isWhitespace(ch)) {
ch = this.charCodeAt(++this.index);
}
}
/**
* Search for the operation portion of the string (e.g. `+`, `===`)
* Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
* and move down from 3 to 2 to 1 character until a matching binary operation is found
* then, return that binary operation
*/
parseBinaryOp() {
this.parseSpaces();
let toCheck = this.template.substr(this.index, Operators_1.maxBinaryOps);
let tcLen = toCheck.length;
while (tcLen > 0) {
if (toCheck in Operators_1.BinaryOps) {
this.index += tcLen;
return toCheck;
}
toCheck = toCheck.substr(0, --tcLen);
}
return null;
}
/**
* This function is responsible for gobbling an individual expression,
* e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
*/
parseBinaryExpression() {
let node;
let biop;
let prec;
let biopInfo;
let fbiop;
let left;
let right;
let i;
// First, try to get the leftmost thing
// Then, get the operator following the leftmost thing
left = this.parseToken();
biop = this.parseBinaryOp();
// If the operator is a unary operator, create a unary expression with the leftmost thing
if (biop === Operators_1.Operators.PLUS_PLUS ||
biop === Operators_1.Operators.MINUS_MINUS ||
biop === Operators_1.Operators.EXISTS) {
left = createUnaryExpression(biop, left, false);
biop = this.parseBinaryOp();
}
// If there wasn't a binary operator, just return the leftmost node
if (!biop) {
return left;
}
// Otherwise, we need to start a stack to properly place the binary operations in their
// precedence structure
biopInfo = {
value: biop,
prec: binaryPrecedence(biop),
};
right = this.parseToken();
if (!right || !left) {
throw new ParseError_1.default(`Expected expression after ${biop}`, {
start: this.index,
end: this.index,
});
}
const stack = [left, biopInfo, right];
/**
* Properly deal with precedence using
* @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
*/
// eslint-disable-next-line no-constant-condition
while (true) {
biop = this.parseBinaryOp();
if (!biop) {
break;
}
prec = binaryPrecedence(biop);
if (prec === 0) {
break;
}
biopInfo = { value: biop, prec };
// Reduce: make a binary expression from the three topmost entries.
while (stack.length > 2) {
fbiop = stack[stack.length - 2];
if (!isIBiopInfo(fbiop) || prec > fbiop.prec) {
break;
}
right = stack.pop();
stack.pop();
left = stack.pop();
if (!isAllParamTypes(right) || !isAllParamTypes(left)) {
break;
}
node = createBinaryExpression(fbiop.value, left, right);
stack.push(node);
}
node = this.parseToken();
if (!node) {
throw new ParseError_1.default(`Expected expression after ${biop}`, {
start: this.index,
end: this.index,
});
}
stack.push(biopInfo, node);
}
i = stack.length - 1;
node = stack[i];
while (i > 1) {
fbiop = stack[i - 1];
left = stack[i - 2];
if (!isIBiopInfo(fbiop) ||
!isAllParamTypes(left) ||
!isAllParamTypes(node)) {
throw new ParseError_1.default(`Expected expression`, {
start: this.index,
end: this.index,
});
}
node = createBinaryExpression(fbiop.value, left, node);
i -= 2;
}
if (!isAllParamTypes(node)) {
throw new ParseError_1.default(`Expected expression`, {
start: this.index,
end: this.index,
});
}
return node;
}
/**
* An individual part of a binary expression:
* e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
*/
parseToken() {
this.parseSpaces();
const ch = this.charCodeAt(this.index);
if (Chars_1.isDecimalDigit(ch) || ch === CharCodes_1.default.Period) {
// Char code 46 is a dot `.` which can start off a numeric literal
return this.parseNumericLiteral();
}
else if (ch === CharCodes_1.default.SingleQuote || ch === CharCodes_1.default.DoubleQuote) {
// Single or double quotes
return this.parseStringLiteral();
}
else if (Chars_1.isIdentifierStart(ch) || ch === CharCodes_1.default.OpenParenthesis) {
// open parenthesis
// `foo`, `bar.baz`
return this.parseVariable();
}
else if (ch === CharCodes_1.default.OpenBracket) {
return this.parseArray();
}
else if (ch === CharCodes_1.default.OpenBrace) {
return this.parseMap();
}
else {
let toCheck = this.template.substr(this.index, Operators_1.maxUnaryOps);
let tcLen = toCheck.length;
while (tcLen > 0) {
if (toCheck in Operators_1.UnaryOps) {
this.index += tcLen;
return createUnaryExpression(toCheck, this.parseToken(), true);
}
toCheck = toCheck.substr(0, --tcLen);
}
}
return null;
}
/**
* Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
* keep track of everything in the numeric literal and then calling `parseFloat` on that string
*/
parseNumericLiteral() {
let rawName = '';
while (Chars_1.isDecimalDigit(this.charCodeAt(this.index))) {
rawName += this.charAt(this.index++);
}
if (this.charCodeAt(this.index) === CharCodes_1.default.Period) {
// can start with a decimal marker
rawName += this.charAt(this.index++);
while (Chars_1.isDecimalDigit(this.charCodeAt(this.index))) {
rawName += this.charAt(this.index++);
}
}
const chCode = this.charCodeAt(this.index);
// Check to make sure this isn't a variable name that start with a number (123abc)
if (Chars_1.isIdentifierStart(chCode)) {
throw new ParseError_1.default(`Variable names cannot start with a number (${rawName}${this.charAt(this.index)})`, { start: this.index, end: this.index });
}
else if (chCode === CharCodes_1.default.Period) {
throw new ParseError_1.default('Unexpected period', {
start: this.index,
end: this.index,
});
}
return {
type: ParamNames_1.default.Literal,
value: parseFloat(rawName),
raw: rawName,
};
}
/**
* Parses a string literal, staring with single or double quotes with basic support for escape codes
* e.g. `"hello world"`, `'this is\nJSEP'`
*/
parseStringLiteral() {
let str = '';
const quote = this.charAt(this.index++);
let closed = false;
let ch;
while (this.index < this.length) {
ch = this.charAt(this.index++);
if (ch === quote) {
closed = true;
break;
}
else if (ch === '\\') {
// Check for all of the common escape codes
ch = this.charAt(this.index++);
str += `\\${ch}`;
}
else {
str += ch;
}
}
if (!closed) {
throw new ParseError_1.default(`Unclosed quote after "${str}"`, {
start: this.index,
end: this.index,
});
}
return {
type: ParamNames_1.default.Literal,
value: str,
raw: quote + str + quote,
};
}
/**
* Gobbles only identifiers
* e.g.: `foo`, `_value`, `$x1`
* Also, this function checks if that identifier is a literal:
* (e.g. `true`, `false`, `null`) or `this`
*/
parseIdentifier() {
let ch = this.charCodeAt(this.index);
const start = this.index;
if (Chars_1.isIdentifierStart(ch)) {
this.index++;
}
else {
throw new ParseError_1.default(`Unexpected ${this.charAt(this.index)}`, {
start: this.index,
end: this.index,
});
}
while (this.index < this.length) {
ch = this.charCodeAt(this.index);
if (Chars_1.isIdentifierPart(ch)) {
this.index++;
}
else {
break;
}
}
const identifier = this.template.slice(start, this.index);
if (identifier in Operators_1.Literals) {
return {
type: ParamNames_1.default.Literal,
value: Operators_1.Literals[identifier],
raw: identifier,
};
}
else {
return {
type: ParamNames_1.default.Identifier,
name: identifier,
};
}
}
/**
* Gobbles a list of arguments within the context of a function call
* or array literal. This function also assumes that the opening character
* `(` or `[` has already been gobbled, and gobbles expressions and commas
* until the terminator character `)` or `]` is encountered.
* e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
*/
parseArguments(termination) {
let chI;
const args = [];
let node;
let closed = false;
while (this.index < this.length) {
this.parseSpaces();
chI = this.charCodeAt(this.index);
if (chI === termination) {
// done parsing
closed = true;
this.index++;
break;
}
else if (chI === CharCodes_1.default.Comma) {
// between expressions
this.index++;
}
else {
node = this.parseExpression();
if (!node || node.type === ParamNames_1.default.Compound) {
throw new ParseError_1.default('Expected comma', {
start: this.index,
end: this.index,
});
}
args.push(node);
}
}
if (!closed) {
throw new ParseError_1.default(`Expected ${String.fromCharCode(termination)}`, {
start: this.index,
end: this.index,
});
}
return args;
}
/**
* Gobble a non-literal variable name. This variable name may include properties
* e.g. `foo`, `bar.baz`, `foo['bar'].baz`
* It also gobbles function calls:
* e.g. `Math.acos(obj.angle)`
*/
parseVariable() {
let chI;
chI = this.charCodeAt(this.index);
let node = chI === CharCodes_1.default.OpenParenthesis
? this.parseGroup()
: this.parseIdentifier();
this.parseSpaces();
chI = this.charCodeAt(this.index);
while (chI === CharCodes_1.default.Period ||
chI === CharCodes_1.default.OpenBracket ||
chI === CharCodes_1.default.OpenParenthesis) {
this.index++;
if (chI === CharCodes_1.default.Period) {
this.parseSpaces();
node = {
type: ParamNames_1.default.MemberExpression,
computed: false,
object: node,
property: this.parseIdentifier(),
};
}
else if (chI === CharCodes_1.default.OpenBracket) {
node = {
type: ParamNames_1.default.MemberExpression,
computed: true,
object: node,
property: this.parseExpression(),
};
this.parseSpaces();
chI = this.charCodeAt(this.index);
if (chI !== CharCodes_1.default.CloseBracket) {
throw new ParseError_1.default('Unclosed [', {
start: this.index,
end: this.index,
});
}
this.index++;
}
else if (chI === CharCodes_1.default.OpenParenthesis) {
// A function call is being made; gobble all the arguments
node = {
type: ParamNames_1.default.CallExpression,
arguments: this.parseArguments(CharCodes_1.default.CloseParenthesis),
callee: node,
};
}
this.parseSpaces();
chI = this.charCodeAt(this.index);
}
return node;
}
/**
* Responsible for parsing a group of things within parentheses `()`
* This function assumes that it needs to gobble the opening parenthesis
* and then tries to gobble everything within that parenthesis, assuming
* that the next thing it should see is the close parenthesis. If not,
* then the expression probably doesn't have a `)`
*/
parseGroup() {
this.index++;
const node = this.parseExpression();
this.parseSpaces();
if (this.charCodeAt(this.index) === CharCodes_1.default.CloseParenthesis) {
this.index++;
return node;
}
else {
throw new ParseError_1.default('Unclosed (', {
start: this.index,
end: this.index,
});
}
}
/**
* Responsible for parsing Array literals `[1, 2, 3]`
* This function assumes that it needs to gobble the opening bracket
* and then tries to gobble the expressions as arguments.
*/
parseArray() {
this.index++;
return {
type: ParamNames_1.default.ArrayExpression,
elements: this.parseArguments(CharCodes_1.default.CloseBracket),
};
}
/**
* Responsible for parsing Map literals `[a: 1, b: 2, c: 3]`
* This function assumes that it needs to gobble the opening brace
* and then tries to gobble the expressions as arguments.
*/
parseMap() {
let ch;
let closed = false;
const elements = [];
++this.index;
while (this.index < this.length) {
this.parseSpaces();
ch = this.charCodeAt(this.index);
if (ch === CharCodes_1.default.CloseBrace) {
++this.index;
closed = true;
break;
}
if (ch !== CharCodes_1.default.SingleQuote && ch !== CharCodes_1.default.DoubleQuote) {
throw new ParseError_1.default(`Invalid character ${String.fromCharCode(ch)}`, {
start: this.index,
end: this.index,
});
}
const key = this.parseStringLiteral();
this.parseSpaces();
ch = this.charCodeAt(this.index);
if (ch !== CharCodes_1.default.Colon) {
throw new ParseError_1.default(`Invalid character ${String.fromCharCode(ch)}`, {
start: this.index,
end: this.index,
});
}
++this.index;
this.parseSpaces();
const value = this.parseExpression();
if (!value) {
throw new ParseError_1.default(`Invalid character ${String.fromCharCode(ch)}`, {
start: this.index,
end: this.index,
});
}
ch = this.charCodeAt(this.index);
if (ch === CharCodes_1.default.Comma) {
++this.index;
}
elements.push({
key,
value,
});
}
if (!closed) {
ch = this.charCodeAt(this.index);
throw new ParseError_1.default('Unclosed {', {
start: this.index,
end: this.index,
});
}
return {
type: ParamNames_1.default.MapExpression,
elements,
};
}
}
exports.ParamsParser = ParamsParser;
//# sourceMappingURL=ParamsParser.js.map