mancha
Version:
Javscript HTML rendering engine
340 lines • 12.5 kB
JavaScript
/*
* @license
* Portions Copyright (c) 2013, the Dart project authors.
*/
import { BINARY_OPERATORS, KEYWORDS, POSTFIX_PRECEDENCE, UNARY_OPERATORS } from "./constants.js";
import { Kind, Tokenizer } from "./tokenizer.js";
export const parse = (expr, astFactory) => new Parser(expr, astFactory).parse();
export class Parser {
_kind;
_tokenizer;
_ast;
_token;
_value;
constructor(input, astFactory) {
this._tokenizer = new Tokenizer(input);
this._ast = astFactory;
}
parse() {
this._advance();
const result = this._parseExpression();
// Ensure all input was consumed.
if (this._token) {
throw new Error(`Unexpected token: ${this._token.value}`);
}
return result;
}
_advance(kind, value) {
if (!this._matches(kind, value)) {
throw new Error(`Expected kind ${kind} (${value}), was ${this._token?.kind} (${this._token?.value})`);
}
const t = this._tokenizer.nextToken();
this._token = t;
this._kind = t?.kind;
this._value = t?.value;
}
_matches(kind, value) {
return !((kind && this._kind !== kind) || (value && this._value !== value));
}
_parseExpression() {
if (!this._token)
return this._ast.empty();
const expr = this._parseUnary();
return expr === undefined ? undefined : this._parsePrecedence(expr, 0);
}
// _parsePrecedence and _parseBinary implement the precedence climbing
// algorithm as described in:
// http://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method
_parsePrecedence(left, precedence) {
if (left === undefined) {
throw new Error("Expected left to be defined.");
}
while (this._token) {
if (this._matches(Kind.GROUPER, "(")) {
const args = this._parseArguments();
left = this._ast.invoke(left, undefined, args);
}
else if (this._matches(Kind.GROUPER, "[")) {
const indexExpr = this._parseIndex();
left = this._ast.index(left, indexExpr);
}
else if (this._matches(Kind.DOT) || this._matches(Kind.OPTIONAL_DOT)) {
const optional = this._kind === Kind.OPTIONAL_DOT;
this._advance();
if (optional && this._matches(Kind.GROUPER, "[")) {
const indexExpr = this._parseIndex();
left = this._ast.index(left, indexExpr, true);
}
else if (optional && this._matches(Kind.GROUPER, "(")) {
const args = this._parseArguments();
left = this._ast.invoke(left, undefined, args, true);
}
else {
const right = this._parseUnary();
left = this._makeInvokeOrGetter(left, right, optional);
}
}
else if (this._matches(Kind.KEYWORD)) {
break;
}
else if (this._matches(Kind.OPERATOR) && this._token.precedence >= precedence) {
left =
this._value === "?" ? this._parseTernary(left) : this._parseBinary(left, this._token);
}
else {
break;
}
}
return left;
}
_makeInvokeOrGetter(left, right, optional) {
if (right === undefined) {
throw new Error("expected identifier");
}
if (right.type === "ID") {
return this._ast.getter(left, right.value, optional);
}
else if (right.type === "Invoke" && right.receiver.type === "ID") {
const method = right.receiver;
// biome-ignore lint/suspicious/noExplicitAny: arguments array type is complex
return this._ast.invoke(left, method.value, right.arguments, optional);
}
else {
throw new Error(`expected identifier: ${right}`);
}
}
_parseBinary(left, op) {
if (!BINARY_OPERATORS.has(op.value)) {
throw new Error(`unknown operator: ${op.value}`);
}
this._advance();
let right = this._parseUnary();
while ((this._kind === Kind.OPERATOR || this._kind === Kind.DOT || this._kind === Kind.GROUPER) &&
this._token &&
(this._token.precedence ?? 0) > op.precedence) {
right = this._parsePrecedence(right, this._token?.precedence ?? 0);
}
if (right === undefined) {
throw new Error(`Expected expression after ${op.value}`);
}
return this._ast.binary(left, op.value, right);
}
_parseUnary() {
// Handle keyword-based unary operators like 'typeof'.
if (this._matches(Kind.KEYWORD, "typeof")) {
this._advance();
const expr = this._parsePrecedence(this._parsePrimary(), POSTFIX_PRECEDENCE);
return this._ast.unary("typeof", expr);
}
if (this._matches(Kind.OPERATOR)) {
const value = this._value;
this._advance();
// Handle unary + and - on numbers as part of the literal, not as a
// unary operator.
if (value === "+" || value === "-") {
if (this._matches(Kind.INTEGER)) {
return this._parseInteger(value);
}
else if (this._matches(Kind.DECIMAL)) {
return this._parseDecimal(value);
}
}
if (!value || !UNARY_OPERATORS.has(value))
throw new Error(`unexpected token: ${value}`);
const expr = this._parsePrecedence(this._parsePrimary(), POSTFIX_PRECEDENCE);
return this._ast.unary(value, expr);
}
return this._parsePrimary();
}
_parseTernary(condition) {
this._advance(Kind.OPERATOR, "?");
const trueExpr = this._parseExpression();
this._advance(Kind.COLON);
const falseExpr = this._parseExpression();
return this._ast.ternary(condition, trueExpr, falseExpr);
}
_parsePrimary() {
switch (this._kind) {
case Kind.KEYWORD: {
const keyword = this._value ?? "";
if (keyword === "this") {
this._advance();
// TODO(justin): return keyword node
return this._ast.id(keyword);
}
else if (KEYWORDS.has(keyword)) {
throw new Error(`unexpected keyword: ${keyword}`);
}
throw new Error(`unrecognized keyword: ${keyword}`);
}
case Kind.IDENTIFIER:
return this._parseInvokeOrIdentifier();
case Kind.STRING:
return this._parseString();
case Kind.INTEGER:
return this._parseInteger();
case Kind.DECIMAL:
return this._parseDecimal();
case Kind.GROUPER:
if (this._value === "(") {
return this._parseParenOrFunction();
}
else if (this._value === "{") {
return this._parseMap();
}
else if (this._value === "[") {
return this._parseList();
}
return undefined;
case Kind.COLON:
throw new Error('unexpected token ":"');
default:
return undefined;
}
}
_parseList() {
const items = [];
do {
this._advance();
if (this._matches(Kind.GROUPER, "]"))
break;
if (this._matches(Kind.SPREAD)) {
this._advance();
const expr = this._parseExpression();
if (expr) {
items.push(this._ast.spreadElement(expr));
}
}
else {
items.push(this._parseExpression());
}
} while (this._matches(Kind.COMMA));
this._advance(Kind.GROUPER, "]");
return this._ast.list(items);
}
_parseMap() {
const properties = [];
do {
this._advance();
if (this._matches(Kind.GROUPER, "}"))
break;
if (this._matches(Kind.SPREAD)) {
this._advance();
const expr = this._parseExpression();
if (expr) {
properties.push(this._ast.spreadProperty(expr));
}
}
else {
const key = this._value ?? "";
if (this._matches(Kind.STRING) || this._matches(Kind.IDENTIFIER)) {
this._advance();
}
if (this._matches(Kind.COLON)) {
this._advance(Kind.COLON);
const value = this._parseExpression();
if (value) {
properties.push(this._ast.property(key, value));
}
}
else {
// Shorthand property
properties.push(this._ast.property(key, this._ast.id(key)));
}
}
} while (this._matches(Kind.COMMA));
this._advance(Kind.GROUPER, "}");
// biome-ignore lint/suspicious/noExplicitAny: properties array type is complex
return this._ast.map(properties);
}
_parseInvokeOrIdentifier() {
const value = this._value;
if (value === "true") {
this._advance();
return this._ast.literal(true);
}
if (value === "false") {
this._advance();
return this._ast.literal(false);
}
if (value === "null") {
this._advance();
return this._ast.literal(null);
}
if (value === "undefined") {
this._advance();
return this._ast.literal(undefined);
}
const identifier = this._parseIdentifier();
const args = this._parseArguments();
return !args ? identifier : this._ast.invoke(identifier, undefined, args);
}
_parseIdentifier() {
if (!this._matches(Kind.IDENTIFIER)) {
throw new Error(`expected identifier: ${this._value}`);
}
const value = this._value;
this._advance();
return this._ast.id(value ?? "");
}
_parseArguments() {
if (!this._matches(Kind.GROUPER, "(")) {
return undefined;
}
const args = [];
do {
this._advance();
if (this._matches(Kind.GROUPER, ")")) {
break;
}
if (this._matches(Kind.SPREAD)) {
this._advance();
const expr = this._parseExpression();
if (expr) {
args.push(this._ast.spreadElement(expr));
}
}
else {
const expr = this._parseExpression();
args.push(expr);
}
} while (this._matches(Kind.COMMA));
this._advance(Kind.GROUPER, ")");
return args;
}
_parseIndex() {
// console.assert(this._matches(Kind.GROUPER, '['));
this._advance();
const expr = this._parseExpression();
this._advance(Kind.GROUPER, "]");
return expr;
}
_parseParenOrFunction() {
const expressions = this._parseArguments();
if (this._matches(Kind.ARROW)) {
this._advance();
const body = this._parseExpression();
const params = expressions?.map((e) => e.value) ?? [];
return this._ast.arrowFunction(params, body);
}
else {
return this._ast.paren(expressions?.[0]);
}
}
_parseString() {
const value = this._ast.literal(this._value ?? "");
this._advance();
return value;
}
_parseInteger(prefix = "") {
const value = this._ast.literal(parseInt(`${prefix}${this._value}`, 10));
this._advance();
return value;
}
_parseDecimal(prefix = "") {
const value = this._ast.literal(parseFloat(`${prefix}${this._value}`));
this._advance();
return value;
}
}
//# sourceMappingURL=parser.js.map