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;
|
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 {
|
export class Text implements Node {
|
||||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||||
visit<Result>(visitor: Visitor<Result>): Result {
|
visit<Result>(visitor: Visitor<Result>): Result {
|
||||||
|
|
|
@ -51,23 +51,34 @@ export interface Render3ParseResult {
|
||||||
styles: string[];
|
styles: string[];
|
||||||
styleUrls: string[];
|
styleUrls: string[];
|
||||||
ngContentSelectors: string[];
|
ngContentSelectors: string[];
|
||||||
|
// Will be defined if `Render3ParseOptions['collectCommentNodes']` is true
|
||||||
|
commentNodes?: t.Comment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Render3ParseOptions {
|
||||||
|
collectCommentNodes: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function htmlAstToRender3Ast(
|
export function htmlAstToRender3Ast(
|
||||||
htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult {
|
htmlNodes: html.Node[], bindingParser: BindingParser,
|
||||||
const transformer = new HtmlAstToIvyAst(bindingParser);
|
options: Render3ParseOptions): Render3ParseResult {
|
||||||
|
const transformer = new HtmlAstToIvyAst(bindingParser, options);
|
||||||
const ivyNodes = html.visitAll(transformer, htmlNodes);
|
const ivyNodes = html.visitAll(transformer, htmlNodes);
|
||||||
|
|
||||||
// Errors might originate in either the binding parser or the html to ivy transformer
|
// Errors might originate in either the binding parser or the html to ivy transformer
|
||||||
const allErrors = bindingParser.errors.concat(transformer.errors);
|
const allErrors = bindingParser.errors.concat(transformer.errors);
|
||||||
|
|
||||||
return {
|
const result: Render3ParseResult = {
|
||||||
nodes: ivyNodes,
|
nodes: ivyNodes,
|
||||||
errors: allErrors,
|
errors: allErrors,
|
||||||
styleUrls: transformer.styleUrls,
|
styleUrls: transformer.styleUrls,
|
||||||
styles: transformer.styles,
|
styles: transformer.styles,
|
||||||
ngContentSelectors: transformer.ngContentSelectors,
|
ngContentSelectors: transformer.ngContentSelectors
|
||||||
};
|
};
|
||||||
|
if (options.collectCommentNodes) {
|
||||||
|
result.commentNodes = transformer.commentNodes;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HtmlAstToIvyAst implements html.Visitor {
|
class HtmlAstToIvyAst implements html.Visitor {
|
||||||
|
@ -75,9 +86,11 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
styles: string[] = [];
|
styles: string[] = [];
|
||||||
styleUrls: string[] = [];
|
styleUrls: string[] = [];
|
||||||
ngContentSelectors: string[] = [];
|
ngContentSelectors: string[] = [];
|
||||||
|
// This array will be populated if `Render3ParseOptions['collectCommentNodes']` is true
|
||||||
|
commentNodes: t.Comment[] = [];
|
||||||
private inI18nBlock: boolean = false;
|
private inI18nBlock: boolean = false;
|
||||||
|
|
||||||
constructor(private bindingParser: BindingParser) {}
|
constructor(private bindingParser: BindingParser, private options: Render3ParseOptions) {}
|
||||||
|
|
||||||
// HTML visitor
|
// HTML visitor
|
||||||
visitElement(element: html.Element): t.Node|null {
|
visitElement(element: html.Element): t.Node|null {
|
||||||
|
@ -287,6 +300,9 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitComment(comment: html.Comment): null {
|
visitComment(comment: html.Comment): null {
|
||||||
|
if (this.options.collectCommentNodes) {
|
||||||
|
this.commentNodes.push(new t.Comment(comment.value || '', comment.sourceSpan));
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2112,6 +2112,16 @@ export interface ParseTemplateOptions {
|
||||||
* output, but this is done after converting the HTML AST to R3 AST.
|
* output, but this is done after converting the HTML AST to R3 AST.
|
||||||
*/
|
*/
|
||||||
alwaysAttemptHtmlToR3AstConversion?: boolean;
|
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 &&
|
if (!options.alwaysAttemptHtmlToR3AstConversion && parseResult.errors &&
|
||||||
parseResult.errors.length > 0) {
|
parseResult.errors.length > 0) {
|
||||||
return {
|
const parsedTemplate: ParsedTemplate = {
|
||||||
interpolationConfig,
|
interpolationConfig,
|
||||||
preserveWhitespaces,
|
preserveWhitespaces,
|
||||||
template,
|
template,
|
||||||
|
@ -2145,6 +2155,10 @@ export function parseTemplate(
|
||||||
styles: [],
|
styles: [],
|
||||||
ngContentSelectors: []
|
ngContentSelectors: []
|
||||||
};
|
};
|
||||||
|
if (options.collectCommentNodes) {
|
||||||
|
parsedTemplate.commentNodes = [];
|
||||||
|
}
|
||||||
|
return parsedTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||||
|
@ -2160,7 +2174,7 @@ export function parseTemplate(
|
||||||
|
|
||||||
if (!options.alwaysAttemptHtmlToR3AstConversion && i18nMetaResult.errors &&
|
if (!options.alwaysAttemptHtmlToR3AstConversion && i18nMetaResult.errors &&
|
||||||
i18nMetaResult.errors.length > 0) {
|
i18nMetaResult.errors.length > 0) {
|
||||||
return {
|
const parsedTemplate: ParsedTemplate = {
|
||||||
interpolationConfig,
|
interpolationConfig,
|
||||||
preserveWhitespaces,
|
preserveWhitespaces,
|
||||||
template,
|
template,
|
||||||
|
@ -2172,6 +2186,10 @@ export function parseTemplate(
|
||||||
styles: [],
|
styles: [],
|
||||||
ngContentSelectors: []
|
ngContentSelectors: []
|
||||||
};
|
};
|
||||||
|
if (options.collectCommentNodes) {
|
||||||
|
parsedTemplate.commentNodes = [];
|
||||||
|
}
|
||||||
|
return parsedTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
rootNodes = i18nMetaResult.rootNodes;
|
rootNodes = i18nMetaResult.rootNodes;
|
||||||
|
@ -2189,11 +2207,11 @@ export function parseTemplate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {nodes, errors, styleUrls, styles, ngContentSelectors} =
|
const {nodes, errors, styleUrls, styles, ngContentSelectors, commentNodes} = htmlAstToRender3Ast(
|
||||||
htmlAstToRender3Ast(rootNodes, bindingParser);
|
rootNodes, bindingParser, {collectCommentNodes: !!options.collectCommentNodes});
|
||||||
errors.push(...parseResult.errors, ...i18nMetaResult.errors);
|
errors.push(...parseResult.errors, ...i18nMetaResult.errors);
|
||||||
|
|
||||||
return {
|
const parsedTemplate: ParsedTemplate = {
|
||||||
interpolationConfig,
|
interpolationConfig,
|
||||||
preserveWhitespaces,
|
preserveWhitespaces,
|
||||||
errors: errors.length > 0 ? errors : null,
|
errors: errors.length > 0 ? errors : null,
|
||||||
|
@ -2205,6 +2223,10 @@ export function parseTemplate(
|
||||||
styles,
|
styles,
|
||||||
ngContentSelectors
|
ngContentSelectors
|
||||||
};
|
};
|
||||||
|
if (options.collectCommentNodes) {
|
||||||
|
parsedTemplate.commentNodes = commentNodes;
|
||||||
|
}
|
||||||
|
return parsedTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementRegistry = new DomElementSchemaRegistry();
|
const elementRegistry = new DomElementSchemaRegistry();
|
||||||
|
@ -2410,4 +2432,10 @@ export interface ParsedTemplate {
|
||||||
* Any ng-content selectors extracted from the template.
|
* Any ng-content selectors extracted from the template.
|
||||||
*/
|
*/
|
||||||
ngContentSelectors: string[];
|
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']);
|
['onEvent'], ['onEvent']);
|
||||||
const bindingParser =
|
const bindingParser =
|
||||||
new BindingParser(expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, []);
|
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) {
|
if (r3Result.errors.length > 0 && !options.ignoreError) {
|
||||||
const msg = r3Result.errors.map(e => e.toString()).join('\n');
|
const msg = r3Result.errors.map(e => e.toString()).join('\n');
|
||||||
|
|
Loading…
Reference in New Issue