Andrew Scott 21651d362d refactor(compiler-cli): add keySpan to text attributes (#39613)
Similar to #39609 and #38898, though we currently have the knowledge of where the key for an
attribute appears during parsing, we do not propagate this
information to the output AST. This means that once we produce the
template AST, we have no way of mapping a template position to the key
span alone. The best we can currently do is map back to the
sourceSpan. This presents problems downstream, specifically for the
language service, where we cannot provide correct information about a
position in a template because the AST is not granular enough.

PR Close #39613
2020-11-12 14:19:00 -08:00

180 lines
5.5 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AstPath} from '../ast_path';
import {I18nMeta} from '../i18n/i18n_ast';
import {ParseSourceSpan} from '../parse_util';
export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context: any): any;
}
export abstract class NodeWithI18n implements Node {
constructor(public sourceSpan: ParseSourceSpan, public i18n?: I18nMeta) {}
abstract visit(visitor: Visitor, context: any): any;
}
export class Text extends NodeWithI18n {
constructor(public value: string, sourceSpan: ParseSourceSpan, i18n?: I18nMeta) {
super(sourceSpan, i18n);
}
visit(visitor: Visitor, context: any): any {
return visitor.visitText(this, context);
}
}
export class Expansion extends NodeWithI18n {
constructor(
public switchValue: string, public type: string, public cases: ExpansionCase[],
sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan, i18n?: I18nMeta) {
super(sourceSpan, i18n);
}
visit(visitor: Visitor, context: any): any {
return visitor.visitExpansion(this, context);
}
}
export class ExpansionCase implements Node {
constructor(
public value: string, public expression: Node[], public sourceSpan: ParseSourceSpan,
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any {
return visitor.visitExpansionCase(this, context);
}
}
export class Attribute extends NodeWithI18n {
constructor(
public name: string, public value: string, sourceSpan: ParseSourceSpan,
readonly keySpan: ParseSourceSpan|undefined, public valueSpan?: ParseSourceSpan,
i18n?: I18nMeta) {
super(sourceSpan, i18n);
}
visit(visitor: Visitor, context: any): any {
return visitor.visitAttribute(this, context);
}
}
export class Element extends NodeWithI18n {
constructor(
public name: string, public attrs: Attribute[], public children: Node[],
sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null = null, i18n?: I18nMeta) {
super(sourceSpan, i18n);
}
visit(visitor: Visitor, context: any): any {
return visitor.visitElement(this, context);
}
}
export class Comment implements Node {
constructor(public value: string|null, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any {
return visitor.visitComment(this, context);
}
}
export interface Visitor {
// Returning a truthy value from `visit()` will prevent `visitAll()` from the call to the typed
// method and result returned will become the result included in `visitAll()`s result array.
visit?(node: Node, context: any): any;
visitElement(element: Element, context: any): any;
visitAttribute(attribute: Attribute, context: any): any;
visitText(text: Text, context: any): any;
visitComment(comment: Comment, context: any): any;
visitExpansion(expansion: Expansion, context: any): any;
visitExpansionCase(expansionCase: ExpansionCase, context: any): any;
}
export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): any[] {
const result: any[] = [];
const visit = visitor.visit ?
(ast: Node) => visitor.visit!(ast, context) || ast.visit(visitor, context) :
(ast: Node) => ast.visit(visitor, context);
nodes.forEach(ast => {
const astResult = visit(ast);
if (astResult) {
result.push(astResult);
}
});
return result;
}
export class RecursiveVisitor implements Visitor {
constructor() {}
visitElement(ast: Element, context: any): any {
this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.children);
});
}
visitAttribute(ast: Attribute, context: any): any {}
visitText(ast: Text, context: any): any {}
visitComment(ast: Comment, context: any): any {}
visitExpansion(ast: Expansion, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.cases);
});
}
visitExpansionCase(ast: ExpansionCase, context: any): any {}
private visitChildren<T extends Node>(
context: any, cb: (visit: (<V extends Node>(children: V[]|undefined) => void)) => void) {
let results: any[][] = [];
let t = this;
function visit<T extends Node>(children: T[]|undefined) {
if (children) results.push(visitAll(t, children, context));
}
cb(visit);
return Array.prototype.concat.apply([], results);
}
}
export type HtmlAstPath = AstPath<Node>;
function spanOf(ast: Node) {
const start = ast.sourceSpan.start.offset;
let end = ast.sourceSpan.end.offset;
if (ast instanceof Element) {
if (ast.endSourceSpan) {
end = ast.endSourceSpan.end.offset;
} else if (ast.children && ast.children.length) {
end = spanOf(ast.children[ast.children.length - 1]).end;
}
}
return {start, end};
}
export function findNode(nodes: Node[], position: number): HtmlAstPath {
const path: Node[] = [];
const visitor = new class extends RecursiveVisitor {
visit(ast: Node, context: any): any {
const span = spanOf(ast);
if (span.start <= position && position < span.end) {
path.push(ast);
} else {
// Returning a value here will result in the children being skipped.
return true;
}
}
};
visitAll(visitor, nodes);
return new AstPath<Node>(path, position);
}