UNPKG

angular-estree-parser

Version:

A parser that converts Angular source code into an ESTree-compatible form

223 lines (222 loc) 8.53 kB
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 };