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
180 lines
5.5 KiB
TypeScript
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);
|
|
}
|