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:
parent
81a88c009c
commit
5e46901ffc
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue