refactor(compiler): option to include html comments in `ParsedTemplate` (#41251)

Adds a `collectCommentNodes` option on `ParseTemplateOptions` which will cause the returned `ParsedTemplate` to include an array of all html comments found in the template.

PR Close #41251
This commit is contained in:
James Henry 2021-03-17 23:18:30 +04:00 committed by Alex Rickabaugh
parent 81a88c009c
commit 5e46901ffc
5 changed files with 115 additions and 11 deletions

View File

@ -16,6 +16,19 @@ export interface Node {
visit<Result>(visitor: Visitor<Result>): Result;
}
/**
* This is an R3 `Node`-like wrapper for a raw `html.Comment` node. We do not currently
* require the implementation of a visitor for Comments as they are only collected at
* the top-level of the R3 AST, and only if `Render3ParseOptions['collectCommentNodes']`
* is true.
*/
export class Comment implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit<Result>(_visitor: Visitor<Result>): Result {
throw new Error('visit() not implemented for Comment');
}
}
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit<Result>(visitor: Visitor<Result>): Result {

View File

@ -51,23 +51,34 @@ export interface Render3ParseResult {
styles: string[];
styleUrls: string[];
ngContentSelectors: string[];
// Will be defined if `Render3ParseOptions['collectCommentNodes']` is true
commentNodes?: t.Comment[];
}
interface Render3ParseOptions {
collectCommentNodes: boolean;
}
export function htmlAstToRender3Ast(
htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult {
const transformer = new HtmlAstToIvyAst(bindingParser);
htmlNodes: html.Node[], bindingParser: BindingParser,
options: Render3ParseOptions): Render3ParseResult {
const transformer = new HtmlAstToIvyAst(bindingParser, options);
const ivyNodes = html.visitAll(transformer, htmlNodes);
// Errors might originate in either the binding parser or the html to ivy transformer
const allErrors = bindingParser.errors.concat(transformer.errors);
return {
const result: Render3ParseResult = {
nodes: ivyNodes,
errors: allErrors,
styleUrls: transformer.styleUrls,
styles: transformer.styles,
ngContentSelectors: transformer.ngContentSelectors,
ngContentSelectors: transformer.ngContentSelectors
};
if (options.collectCommentNodes) {
result.commentNodes = transformer.commentNodes;
}
return result;
}
class HtmlAstToIvyAst implements html.Visitor {
@ -75,9 +86,11 @@ class HtmlAstToIvyAst implements html.Visitor {
styles: string[] = [];
styleUrls: string[] = [];
ngContentSelectors: string[] = [];
// This array will be populated if `Render3ParseOptions['collectCommentNodes']` is true
commentNodes: t.Comment[] = [];
private inI18nBlock: boolean = false;
constructor(private bindingParser: BindingParser) {}
constructor(private bindingParser: BindingParser, private options: Render3ParseOptions) {}
// HTML visitor
visitElement(element: html.Element): t.Node|null {
@ -287,6 +300,9 @@ class HtmlAstToIvyAst implements html.Visitor {
}
visitComment(comment: html.Comment): null {
if (this.options.collectCommentNodes) {
this.commentNodes.push(new t.Comment(comment.value || '', comment.sourceSpan));
}
return null;
}

View File

@ -2112,6 +2112,16 @@ export interface ParseTemplateOptions {
* output, but this is done after converting the HTML AST to R3 AST.
*/
alwaysAttemptHtmlToR3AstConversion?: boolean;
/**
* Include HTML Comment nodes in a top-level comments array on the returned R3 AST.
*
* This option is required by tooling that needs to know the location of comment nodes within the
* AST. A concrete example is @angular-eslint which requires this in order to enable
* "eslint-disable" comments within HTML templates, which then allows users to turn off specific
* rules on a case by case basis, instead of for their whole project within a configuration file.
*/
collectCommentNodes?: boolean;
}
/**
@ -2133,7 +2143,7 @@ export function parseTemplate(
if (!options.alwaysAttemptHtmlToR3AstConversion && parseResult.errors &&
parseResult.errors.length > 0) {
return {
const parsedTemplate: ParsedTemplate = {
interpolationConfig,
preserveWhitespaces,
template,
@ -2145,6 +2155,10 @@ export function parseTemplate(
styles: [],
ngContentSelectors: []
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = [];
}
return parsedTemplate;
}
let rootNodes: html.Node[] = parseResult.rootNodes;
@ -2160,7 +2174,7 @@ export function parseTemplate(
if (!options.alwaysAttemptHtmlToR3AstConversion && i18nMetaResult.errors &&
i18nMetaResult.errors.length > 0) {
return {
const parsedTemplate: ParsedTemplate = {
interpolationConfig,
preserveWhitespaces,
template,
@ -2172,6 +2186,10 @@ export function parseTemplate(
styles: [],
ngContentSelectors: []
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = [];
}
return parsedTemplate;
}
rootNodes = i18nMetaResult.rootNodes;
@ -2189,11 +2207,11 @@ export function parseTemplate(
}
}
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
htmlAstToRender3Ast(rootNodes, bindingParser);
const {nodes, errors, styleUrls, styles, ngContentSelectors, commentNodes} = htmlAstToRender3Ast(
rootNodes, bindingParser, {collectCommentNodes: !!options.collectCommentNodes});
errors.push(...parseResult.errors, ...i18nMetaResult.errors);
return {
const parsedTemplate: ParsedTemplate = {
interpolationConfig,
preserveWhitespaces,
errors: errors.length > 0 ? errors : null,
@ -2205,6 +2223,10 @@ export function parseTemplate(
styles,
ngContentSelectors
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = commentNodes;
}
return parsedTemplate;
}
const elementRegistry = new DomElementSchemaRegistry();
@ -2410,4 +2432,10 @@ export interface ParsedTemplate {
* Any ng-content selectors extracted from the template.
*/
ngContentSelectors: string[];
/**
* Any R3 Comment Nodes extracted from the template when the `collectCommentNodes` parse template
* option is enabled.
*/
commentNodes?: t.Comment[];
}

View File

@ -0,0 +1,47 @@
/**
* @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 {ParseSourceSpan} from '../../../src/parse_util';
import {Comment} from '../../../src/render3/r3_ast';
import {parseTemplate} from '../../../src/render3/view/template';
describe('collectCommentNodes', () => {
it('should include an array of HTML comment nodes on the returned R3 AST', () => {
const html = `
<!-- eslint-disable-next-line -->
<div *ngFor="let item of items">
{{item.name}}
</div>
<div>
<p>
<!-- some nested comment -->
<span>Text</span>
</p>
</div>
`;
const templateNoCommentsOption = parseTemplate(html, '', {});
expect(templateNoCommentsOption.commentNodes).toBeUndefined();
const templateCommentsOptionDisabled = parseTemplate(html, '', {collectCommentNodes: false});
expect(templateCommentsOptionDisabled.commentNodes).toBeUndefined();
const templateCommentsOptionEnabled = parseTemplate(html, '', {collectCommentNodes: true});
expect(templateCommentsOptionEnabled.commentNodes!.length).toEqual(2);
expect(templateCommentsOptionEnabled.commentNodes![0]).toBeInstanceOf(Comment);
expect(templateCommentsOptionEnabled.commentNodes![0].value)
.toEqual('eslint-disable-next-line');
expect(templateCommentsOptionEnabled.commentNodes![0].sourceSpan)
.toBeInstanceOf(ParseSourceSpan);
expect(templateCommentsOptionEnabled.commentNodes![1]).toBeInstanceOf(Comment);
expect(templateCommentsOptionEnabled.commentNodes![1].value).toEqual('some nested comment');
expect(templateCommentsOptionEnabled.commentNodes![1].sourceSpan)
.toBeInstanceOf(ParseSourceSpan);
});
});

View File

@ -107,7 +107,7 @@ export function parseR3(
['onEvent'], ['onEvent']);
const bindingParser =
new BindingParser(expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, []);
const r3Result = htmlAstToRender3Ast(htmlNodes, bindingParser);
const r3Result = htmlAstToRender3Ast(htmlNodes, bindingParser, {collectCommentNodes: false});
if (r3Result.errors.length > 0 && !options.ignoreError) {
const msg = r3Result.errors.map(e => e.toString()).join('\n');