diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index dc121bbc054a3..dc7e452a11ef4 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -16,6 +16,19 @@ export interface Node { visit(visitor: Visitor): 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(_visitor: Visitor): Result { + throw new Error('visit() not implemented for Comment'); + } +} + export class Text implements Node { constructor(public value: string, public sourceSpan: ParseSourceSpan) {} visit(visitor: Visitor): Result { diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index 716e5254e7b10..a09a4e0aae9e2 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -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; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index c4671a9bd85bf..7aea606211e5a 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -2086,6 +2086,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; } /** @@ -2107,7 +2117,7 @@ export function parseTemplate( if (!options.alwaysAttemptHtmlToR3AstConversion && parseResult.errors && parseResult.errors.length > 0) { - return { + const parsedTemplate: ParsedTemplate = { interpolationConfig, preserveWhitespaces, template, @@ -2119,6 +2129,10 @@ export function parseTemplate( styles: [], ngContentSelectors: [] }; + if (options.collectCommentNodes) { + parsedTemplate.commentNodes = []; + } + return parsedTemplate; } let rootNodes: html.Node[] = parseResult.rootNodes; @@ -2134,7 +2148,7 @@ export function parseTemplate( if (!options.alwaysAttemptHtmlToR3AstConversion && i18nMetaResult.errors && i18nMetaResult.errors.length > 0) { - return { + const parsedTemplate: ParsedTemplate = { interpolationConfig, preserveWhitespaces, template, @@ -2146,6 +2160,10 @@ export function parseTemplate( styles: [], ngContentSelectors: [] }; + if (options.collectCommentNodes) { + parsedTemplate.commentNodes = []; + } + return parsedTemplate; } rootNodes = i18nMetaResult.rootNodes; @@ -2163,11 +2181,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, @@ -2179,6 +2197,10 @@ export function parseTemplate( styles, ngContentSelectors }; + if (options.collectCommentNodes) { + parsedTemplate.commentNodes = commentNodes; + } + return parsedTemplate; } const elementRegistry = new DomElementSchemaRegistry(); @@ -2384,4 +2406,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[]; } diff --git a/packages/compiler/test/render3/view/parse_template_options_spec.ts b/packages/compiler/test/render3/view/parse_template_options_spec.ts new file mode 100644 index 0000000000000..d795b055faf42 --- /dev/null +++ b/packages/compiler/test/render3/view/parse_template_options_spec.ts @@ -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 = ` + +
+ {{item.name}} +
+ +
+

+ + Text +

+
+ `; + + 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); + }); +}); diff --git a/packages/compiler/test/render3/view/util.ts b/packages/compiler/test/render3/view/util.ts index 7ace02c092869..cf99f7f34c4b7 100644 --- a/packages/compiler/test/render3/view/util.ts +++ b/packages/compiler/test/render3/view/util.ts @@ -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');