Skip to content

Commit

Permalink
refactor(compiler): option to include html comments in `ParsedTemplat…
Browse files Browse the repository at this point in the history
…e` (#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
  • Loading branch information
JamesHenry authored and alxhub committed Mar 29, 2021
1 parent 5e8afc9 commit 6dcea34
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 11 deletions.
13 changes: 13 additions & 0 deletions packages/compiler/src/render3/r3_ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 21 additions & 5 deletions packages/compiler/src/render3/r3_template_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,46 @@ 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 {
errors: ParseError[] = [];
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 {
Expand Down Expand Up @@ -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;
}

Expand Down
38 changes: 33 additions & 5 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -2107,7 +2117,7 @@ export function parseTemplate(

if (!options.alwaysAttemptHtmlToR3AstConversion && parseResult.errors &&
parseResult.errors.length > 0) {
return {
const parsedTemplate: ParsedTemplate = {
interpolationConfig,
preserveWhitespaces,
template,
Expand All @@ -2119,6 +2129,10 @@ export function parseTemplate(
styles: [],
ngContentSelectors: []
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = [];
}
return parsedTemplate;
}

let rootNodes: html.Node[] = parseResult.rootNodes;
Expand All @@ -2134,7 +2148,7 @@ export function parseTemplate(

if (!options.alwaysAttemptHtmlToR3AstConversion && i18nMetaResult.errors &&
i18nMetaResult.errors.length > 0) {
return {
const parsedTemplate: ParsedTemplate = {
interpolationConfig,
preserveWhitespaces,
template,
Expand All @@ -2146,6 +2160,10 @@ export function parseTemplate(
styles: [],
ngContentSelectors: []
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = [];
}
return parsedTemplate;
}

rootNodes = i18nMetaResult.rootNodes;
Expand All @@ -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,
Expand All @@ -2179,6 +2197,10 @@ export function parseTemplate(
styles,
ngContentSelectors
};
if (options.collectCommentNodes) {
parsedTemplate.commentNodes = commentNodes;
}
return parsedTemplate;
}

const elementRegistry = new DomElementSchemaRegistry();
Expand Down Expand Up @@ -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[];
}
47 changes: 47 additions & 0 deletions packages/compiler/test/render3/view/parse_template_options_spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 1 addition & 1 deletion packages/compiler/test/render3/view/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 6dcea34

Please sign in to comment.