angular-estree-parser
Version:
A parser that converts Angular source code into an ESTree-compatible form
223 lines (222 loc) • 8.53 kB
JavaScript
import { ExpressionBinding as NGExpressionBinding, VariableBinding as NGVariableBinding, } from '@angular/compiler';
import { Transformer as NodeTransformer } from './transform-node.js';
import { lowercaseFirst } from './utils.js';
function isExpressionBinding(templateBinding) {
return templateBinding instanceof NGExpressionBinding;
}
function isVariableBinding(templateBinding) {
return templateBinding instanceof NGVariableBinding;
}
class Transformer extends NodeTransformer {
#rawTemplateBindings;
#text;
constructor(rawTemplateBindings, text) {
super(undefined, text);
this.#rawTemplateBindings = rawTemplateBindings;
this.#text = text;
for (const expression of rawTemplateBindings) {
this.#fixTemplateBindingSpan(expression);
}
}
get expressions() {
return this.#transformTemplateBindings();
}
get #prefix() {
return this.#rawTemplateBindings[0].key;
}
#create(properties, { stripSpaces = true } = {}) {
return this.createNode(properties, { stripSpaces });
}
#transform(node) {
return this.transformNode(node);
}
#removePrefix(string) {
return lowercaseFirst(string.slice(this.#prefix.source.length));
}
/**
* - "a" (start=0 end=1) -> (start=0 end=3)
* - '\'' (start=0 end=1) -> (start=0 end=4)
*/
#fixSpan(span) {
const text = this.#text;
if (text[span.start] !== '"' && text[span.start] !== "'") {
return;
}
const quote = text[span.start];
let hasBackSlash = false;
for (let i = span.start + 1; i < text.length; i++) {
switch (text[i]) {
case quote:
if (!hasBackSlash) {
span.end = i + 1;
return;
}
// fall through
default:
hasBackSlash = false;
break;
case '\\':
hasBackSlash = !hasBackSlash;
break;
}
}
}
#fixTemplateBindingSpan(templateBinding) {
this.#fixSpan(templateBinding.key.span);
if (isVariableBinding(templateBinding) && templateBinding.value) {
this.#fixSpan(templateBinding.value.span);
}
}
/**
* - "as b" (value="NgEstreeParser" key="b") -> (value="$implicit" key="b")
*/
#getAsVariableBindingValue(variableBinding) {
if (!variableBinding.value || variableBinding.value.source) {
return variableBinding.value;
}
const index = this.getCharacterIndex(/\S/, variableBinding.sourceSpan.start);
return {
source: '$implicit',
span: { start: index, end: index },
};
}
#transformTemplateBindings() {
const rawTemplateBindings = this.#rawTemplateBindings;
const [firstTemplateBinding] = rawTemplateBindings;
const templateBindings = this.#text
.slice(firstTemplateBinding.sourceSpan.start, firstTemplateBinding.sourceSpan.end)
.trim().length === 0
? rawTemplateBindings.slice(1)
: rawTemplateBindings;
const body = [];
let lastTemplateBinding = null;
for (const [index, templateBinding] of templateBindings.entries()) {
if (lastTemplateBinding &&
isExpressionBinding(lastTemplateBinding) &&
isVariableBinding(templateBinding) &&
templateBinding.value &&
templateBinding.value.source === lastTemplateBinding.key.source) {
const alias = this.#create({
type: 'NGMicrosyntaxKey',
name: templateBinding.key.source,
...templateBinding.key.span,
});
const updateSpanEnd = (node, end) => ({
...node,
...this.transformSpan({ start: node.start, end }),
});
const updateExpressionAlias = (expression) => ({
...updateSpanEnd(expression, alias.end),
alias,
});
const lastNode = body.pop();
// istanbul ignore else
if (lastNode.type === 'NGMicrosyntaxExpression') {
body.push(updateExpressionAlias(lastNode));
}
else if (lastNode.type === 'NGMicrosyntaxKeyedExpression') {
const expression = updateExpressionAlias(lastNode.expression);
body.push(updateSpanEnd({ ...lastNode, expression }, expression.end));
}
else {
throw new Error(`Unexpected type ${lastNode.type}`);
}
}
else {
body.push(this.#transformTemplateBinding(templateBinding, index));
}
lastTemplateBinding = templateBinding;
}
return this.#create({
type: 'NGMicrosyntax',
body,
...(body.length === 0
? rawTemplateBindings[0].sourceSpan
: { start: body[0].start, end: body.at(-1).end }),
});
}
#transformTemplateBinding(templateBinding, index) {
if (isExpressionBinding(templateBinding)) {
const { key, value } = templateBinding;
if (!value) {
return this.#create({
type: 'NGMicrosyntaxKey',
name: this.#removePrefix(key.source),
...key.span,
});
}
else if (index === 0) {
return this.#create({
type: 'NGMicrosyntaxExpression',
expression: this.#transform(value.ast),
alias: null,
...value.sourceSpan,
});
}
else {
return this.#create({
type: 'NGMicrosyntaxKeyedExpression',
key: this.#create({
type: 'NGMicrosyntaxKey',
name: this.#removePrefix(key.source),
...key.span,
}),
expression: this.#create({
type: 'NGMicrosyntaxExpression',
expression: this.#transform(value.ast),
alias: null,
...value.sourceSpan,
}),
start: key.span.start,
end: value.sourceSpan.end,
});
}
}
else {
const { key, sourceSpan } = templateBinding;
const startsWithLet = /^let\s$/.test(this.#text.slice(sourceSpan.start, sourceSpan.start + 4));
if (startsWithLet) {
const { value } = templateBinding;
return this.#create({
type: 'NGMicrosyntaxLet',
key: this.#create({
type: 'NGMicrosyntaxKey',
name: key.source,
...key.span,
}),
value: !value
? null
: this.#create({
type: 'NGMicrosyntaxKey',
name: value.source,
...value.span,
}),
start: sourceSpan.start,
end: value ? value.span.end : key.span.end,
});
}
else {
const value = this.#getAsVariableBindingValue(templateBinding);
return this.#create({
type: 'NGMicrosyntaxAs',
key: this.#create({
type: 'NGMicrosyntaxKey',
name: value.source,
...value.span,
}),
alias: this.#create({
type: 'NGMicrosyntaxKey',
name: key.source,
...key.span,
}),
start: value.span.start,
end: key.span.end,
});
}
}
}
}
function transform(rawTemplateBindings, text) {
return new Transformer(rawTemplateBindings, text).expressions;
}
export { transform };