@surface/custom-element
Version:
Provides support of directives and data binding on custom elements.
122 lines (121 loc) • 4.35 kB
JavaScript
import { assert } from "@surface/core";
import Expression, { SyntaxError } from "@surface/expression";
import { getOffsetSyntaxError, parseExpression } from "./expression-parsers.js";
const stringTokens = ["\"", "'", "`"];
export default class InterpolatedExpression {
constructor(source) {
this.quasis = [];
this.expressions = [];
this.expressionEnd = 0;
this.expressionStart = 0;
this.index = 0;
this.source = source;
}
get current() {
return this.source[this.index];
}
get eof() {
return this.index == this.source.length;
}
static parse(source) {
return new InterpolatedExpression(source).scan();
}
advance() {
this.index++;
}
collectTextFragment(start, end) {
const textFragment = this.source.substring(start, end)
.replace(/(?<!\\)\\{/g, "{")
.replace(/\\\\{/g, "\\")
.replace(/{$/, "");
this.quasis.push(Expression.templateElement(textFragment, textFragment, end >= this.source.length));
}
parse(start) {
try {
let scaped = false;
while (!this.eof && this.current != "{" || scaped) {
scaped = this.current == "\\" && !scaped;
if (scaped && this.source.substring(this.index, this.index + 3) == "\\\\{") {
scaped = false;
this.advance();
}
this.advance();
}
if (start == 0 || start < this.index) {
this.collectTextFragment(start, this.index + 1);
}
if (!this.eof) {
this.expressionStart = this.index + 1;
if (this.scanBalance()) {
this.expressionEnd = this.index - 1;
const expression = parseExpression(this.source.substring(this.expressionStart, this.expressionEnd));
this.expressions.push(expression);
if (!this.eof) {
this.parse(this.index);
}
else if (this.expressions.length > 0) {
this.collectTextFragment(this.index, this.index);
}
}
else {
throw new SyntaxError("Unexpected end of expression", (this.source.match(/\n/g)?.length ?? 0) + 1, this.source.length - 1, this.source.length);
}
}
}
catch (error) {
assert(error instanceof SyntaxError);
throw getOffsetSyntaxError(this.source, this.source.substring(this.expressionStart, this.expressionEnd), error);
}
}
scan() {
this.parse(0);
return Expression.template(this.quasis, this.expressions);
}
scanBalance() {
let stack = 0;
do {
if (stringTokens.includes(this.current)) {
if (!this.scanString()) {
return false;
}
}
if (this.current == "{") {
stack++;
}
if (this.current == "}") {
stack--;
}
this.advance();
} while (!this.eof && stack > 0);
return stack == 0;
}
scanString() {
const token = this.current;
this.advance();
if (token == this.current) {
return true;
}
let scaped = false;
if (token == "`") {
do {
scaped = this.current == "\\" && !scaped;
if (!scaped && this.source.substring(this.index, this.index + 2) == "${") {
this.advance();
if (!this.scanBalance()) {
return false;
}
}
else {
this.advance();
}
} while (!this.eof && this.current != token || scaped);
}
else {
do {
scaped = this.current == "\\" && !scaped;
this.advance();
} while (!this.eof && this.current != token || scaped);
}
return this.current == token;
}
}