Skip to content

Commit 33c3277

Browse files
wagnermacielmmalerba
authored andcommitted
feat(material/schematics): tree operation helper functions (#24539)
* feat(material/schematics): tree operation helper functions * created tree-traversal.ts for storing tree operation helper fns * moved visitElements to the new file * created helper fns for parseTemplate, and tag name changes * added unit tests which don't need to call the whole schematic
1 parent 98d09ff commit 33c3277

File tree

4 files changed

+179
-32
lines changed

4 files changed

+179
-32
lines changed

src/material/schematics/ng-generate/mdc-migration/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ts_library(
5757
"//src/cdk/schematics/testing",
5858
"@npm//@angular-devkit/core",
5959
"@npm//@angular-devkit/schematics",
60+
"@npm//@angular/compiler",
6061
"@npm//@bazel/runfiles",
6162
"@npm//@types/jasmine",
6263
"@npm//@types/node",

src/material/schematics/ng-generate/mdc-migration/rules/template-migration.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,13 @@
99
import {Migration, ResolvedResource} from '@angular/cdk/schematics';
1010
import {SchematicContext} from '@angular-devkit/schematics';
1111
import {StyleMigrator} from './style-migrator';
12-
import * as compiler from '@angular/compiler';
13-
14-
/**
15-
* Traverses the given tree of nodes and runs the given callbacks for each Element node encountered.
16-
*
17-
* Note that updates to the start tags of html element should be done in the postorder callback,
18-
* and updates to the end tags of html elements should be done in the preorder callback to avoid
19-
* issues with line collisions.
20-
*
21-
* @param nodes The nodes of the ast from a parsed template.
22-
* @param preorderCallback A function that gets run for each Element node in a preorder traversal.
23-
* @param postorderCallback A function that gets run for each Element node in a postorder traversal.
24-
*/
25-
function visitElements(
26-
nodes: compiler.TmplAstNode[],
27-
preorderCallback: (node: compiler.TmplAstElement) => void = () => {},
28-
postorderCallback: (node: compiler.TmplAstElement) => void = () => {},
29-
): void {
30-
for (let i = 0; i < nodes.length; i++) {
31-
const node = nodes[i];
32-
if (node instanceof compiler.TmplAstElement) {
33-
preorderCallback(node);
34-
visitElements(node.children, preorderCallback, postorderCallback);
35-
postorderCallback(node);
36-
}
37-
}
38-
}
12+
import {visitElements, parseTemplate} from './tree-traversal';
3913

4014
export class TemplateMigration extends Migration<StyleMigrator[], SchematicContext> {
4115
enabled = true;
4216

4317
override visitTemplate(template: ResolvedResource) {
44-
const ast = compiler.parseTemplate(template.content, template.filePath, {
45-
preserveWhitespaces: true,
46-
preserveLineEndings: true,
47-
leadingTriviaChars: [],
48-
});
18+
const ast = parseTemplate(template.content, template.filePath);
4919

5020
visitElements(ast.nodes, node => {
5121
// TODO(wagnermaciel): implement the migration updates.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {visitElements, parseTemplate, replaceStartTag, replaceEndTag} from './tree-traversal';
2+
3+
function runTagNameDuplicationTest(html: string, result: string): void {
4+
visitElements(
5+
parseTemplate(html).nodes,
6+
node => {
7+
html = replaceEndTag(html, node, node.name.repeat(2));
8+
},
9+
node => {
10+
html = replaceStartTag(html, node, node.name.repeat(2));
11+
},
12+
);
13+
expect(html).toBe(result);
14+
}
15+
16+
describe('#visitElements', () => {
17+
describe('tag name replacements', () => {
18+
it('should handle basic cases', async () => {
19+
runTagNameDuplicationTest('<a></a>', '<aa></aa>');
20+
});
21+
22+
it('should handle multiple same line', async () => {
23+
runTagNameDuplicationTest('<a></a><b></b>', '<aa></aa><bb></bb>');
24+
});
25+
26+
it('should handle multiple same line nested', async () => {
27+
runTagNameDuplicationTest('<a><b></b></a>', '<aa><bb></bb></aa>');
28+
});
29+
30+
it('should handle multiple same line nested and unnested', async () => {
31+
runTagNameDuplicationTest('<a><b></b><c></c></a>', '<aa><bb></bb><cc></cc></aa>');
32+
});
33+
34+
it('should handle multiple multi-line', async () => {
35+
runTagNameDuplicationTest(
36+
`
37+
<a></a>
38+
<b></b>
39+
`,
40+
`
41+
<aa></aa>
42+
<bb></bb>
43+
`,
44+
);
45+
});
46+
47+
it('should handle multiple multi-line nested', async () => {
48+
runTagNameDuplicationTest(
49+
`
50+
<a>
51+
<b></b>
52+
</a>
53+
`,
54+
`
55+
<aa>
56+
<bb></bb>
57+
</aa>
58+
`,
59+
);
60+
});
61+
62+
it('should handle multiple multi-line nested and unnested', async () => {
63+
runTagNameDuplicationTest(
64+
`
65+
<a>
66+
<b></b>
67+
<c></c>
68+
</a>
69+
`,
70+
`
71+
<aa>
72+
<bb></bb>
73+
<cc></cc>
74+
</aa>
75+
`,
76+
);
77+
});
78+
});
79+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as compiler from '@angular/compiler';
10+
11+
/**
12+
* Traverses the given tree of nodes and runs the given callbacks for each Element node encountered.
13+
*
14+
* Note that updates to the start tags of html element should be done in the postorder callback,
15+
* and updates to the end tags of html elements should be done in the preorder callback to avoid
16+
* issues with line collisions.
17+
*
18+
* @param nodes The nodes of the ast from a parsed template.
19+
* @param preorderCallback A function that gets run for each Element node in a preorder traversal.
20+
* @param postorderCallback A function that gets run for each Element node in a postorder traversal.
21+
*/
22+
export function visitElements(
23+
nodes: compiler.TmplAstNode[],
24+
preorderCallback: (node: compiler.TmplAstElement) => void = () => {},
25+
postorderCallback: (node: compiler.TmplAstElement) => void = () => {},
26+
): void {
27+
nodes.reverse();
28+
for (let i = 0; i < nodes.length; i++) {
29+
const node = nodes[i];
30+
if (node instanceof compiler.TmplAstElement) {
31+
preorderCallback(node);
32+
visitElements(node.children, preorderCallback, postorderCallback);
33+
postorderCallback(node);
34+
}
35+
}
36+
}
37+
38+
/**
39+
* A wrapper for the Angular compilers parseTemplate, which passes the correct options to ensure
40+
* the parsed template is accurate.
41+
*
42+
* For more details, see https://github.com/angular/angular/blob/4332897baa2226ef246ee054fdd5254e3c129109/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts#L230.
43+
*
44+
* @param html text of the template to parse
45+
* @param filePath URL to use for source mapping of the parsed template
46+
* @returns the updated template html.
47+
*/
48+
export function parseTemplate(template: string, templateUrl: string = ''): compiler.ParsedTemplate {
49+
return compiler.parseTemplate(template, templateUrl, {
50+
preserveWhitespaces: true,
51+
preserveLineEndings: true,
52+
leadingTriviaChars: [],
53+
});
54+
}
55+
56+
/**
57+
* Replaces the start tag of the given Element node inside of the html document with a new tag name.
58+
*
59+
* @param html The full html document.
60+
* @param node The Element node to be updated.
61+
* @param tag A new tag name.
62+
* @returns an updated html document.
63+
*/
64+
export function replaceStartTag(html: string, node: compiler.TmplAstElement, tag: string): string {
65+
return replaceAt(html, node.startSourceSpan.start.offset + 1, node.name, tag);
66+
}
67+
68+
/**
69+
* Replaces the end tag of the given Element node inside of the html document with a new tag name.
70+
*
71+
* @param html The full html document.
72+
* @param node The Element node to be updated.
73+
* @param tag A new tag name.
74+
* @returns an updated html document.
75+
*/
76+
export function replaceEndTag(html: string, node: compiler.TmplAstElement, tag: string): string {
77+
if (!node.endSourceSpan) {
78+
return html;
79+
}
80+
return replaceAt(html, node.endSourceSpan.start.offset + 2, node.name, tag);
81+
}
82+
83+
/**
84+
* Replaces a substring of a given string starting at some offset index.
85+
*
86+
* @param str A string to be updated.
87+
* @param offset An offset index to start at.
88+
* @param oldSubstr The old substring to be replaced.
89+
* @param newSubstr A new substring.
90+
* @returns the updated string.
91+
*/
92+
function replaceAt(str: string, offset: number, oldSubstr: string, newSubstr: string): string {
93+
const index = offset;
94+
const prefix = str.slice(0, index);
95+
const suffix = str.slice(index + oldSubstr.length);
96+
return prefix + newSubstr + suffix;
97+
}

0 commit comments

Comments
 (0)