Skip to content

Commit 8e9c7e0

Browse files
committed
refactor(schematics): use parse5 for finding inputs and outputs
* No longer constructs a complex and unreadable RegExp for finding Angular inputs and outputs. Since we declared `parse5` as a dependency for the schematics, we could use `parse5`. * Adds types for parse5 as dev dependency since parse5 is often used in the schematics.
1 parent fc43513 commit 8e9c7e0

File tree

9 files changed

+136
-97
lines changed

9 files changed

+136
-97
lines changed

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@types/merge2": "^0.3.30",
6565
"@types/minimist": "^1.2.0",
6666
"@types/node": "^7.0.21",
67+
"@types/parse5": "^5.0.0",
6768
"@types/run-sequence": "^0.0.29",
6869
"autoprefixer": "^6.7.6",
6970
"axe-webdriverjs": "^1.1.1",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 {findAttributeOnElementWithAttrs, findAttributeOnElementWithTag} from './elements';
10+
11+
/** Finds the specified Angular @Input in the given elements with tag name. */
12+
export function findInputsOnElementWithTag(html: string, inputName: string, tagNames: string[]) {
13+
// Add one column to the mapped offset because the first bracket for the @Input
14+
// is part of the attribute and therefore also part of the offset. We only want to return
15+
// the offset for the inner name of the input.
16+
return findAttributeOnElementWithTag(html, `[${inputName}]`, tagNames).map(offset => offset + 1);
17+
}
18+
19+
/** Finds the specified Angular @Output in the given elements with tag name. */
20+
export function findOutputsOnElementWithTag(html: string, outputName: string, tagNames: string[]) {
21+
// Add one column to the mapped offset because the first parenthesis for the @Output
22+
// is part of the attribute and therefore also part of the offset. We only want to return
23+
// the offset for the inner name of the output.
24+
return findAttributeOnElementWithTag(html, `(${outputName})`, tagNames).map(offset => offset + 1);
25+
}
26+
27+
/** Finds the specified Angular @Input in elements that have one of the specified attributes. */
28+
export function findInputsOnElementWithAttr(html: string, inputName: string, attrs: string[]) {
29+
// Add one column to the mapped offset because the first bracket for the @Input
30+
// is part of the attribute and therefore also part of the offset. We only want to return
31+
// the offset for the inner name of the input.
32+
return findAttributeOnElementWithAttrs(html, `[${inputName}]`, attrs).map(offset => offset + 1);
33+
}
34+
35+
/** Finds the specified Angular @Output in elements that have one of the specified attributes. */
36+
export function findOutputsOnElementWithAttr(html: string, outputName: string, attrs: string[]) {
37+
// Add one column to the mapped offset because the first bracket for the @Output
38+
// is part of the attribute and therefore also part of the offset. We only want to return
39+
// the offset for the inner name of the output.
40+
return findAttributeOnElementWithAttrs(html, `(${outputName})`, attrs).map(offset => offset + 1);
41+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 {DefaultTreeDocument, DefaultTreeElement, parseFragment} from 'parse5';
10+
11+
/**
12+
* Parses a HTML fragment and traverses all AST nodes in order find elements that
13+
* include the specified attribute.
14+
*/
15+
export function findElementsWithAttribute(html: string, attributeName: string) {
16+
const document = parseFragment(html, {sourceCodeLocationInfo: true}) as DefaultTreeDocument;
17+
const elements: DefaultTreeElement[] = [];
18+
19+
const visitNodes = nodes => {
20+
nodes.forEach(node => {
21+
if (node.childNodes) {
22+
visitNodes(node.childNodes);
23+
}
24+
25+
if (node.attrs && node.attrs.some(attr => attr.name === attributeName.toLowerCase())) {
26+
elements.push(node);
27+
}
28+
});
29+
};
30+
31+
visitNodes(document.childNodes);
32+
33+
return elements;
34+
}
35+
36+
/**
37+
* Finds elements with explicit tag names that also contain the specified attribute. Returns the
38+
* attribute start offset based on the specified HTML.
39+
*/
40+
export function findAttributeOnElementWithTag(html: string, name: string, tagNames: string[]) {
41+
return findElementsWithAttribute(html, name)
42+
.filter(element => tagNames.includes(element.tagName))
43+
.map(element => getStartOffsetOfAttribute(element, name));
44+
}
45+
46+
/**
47+
* Finds elements that contain the given attribute and contain at least one of the other
48+
* specified attributes. Returns the primary attribute's start offset based on the specified HTML.
49+
*/
50+
export function findAttributeOnElementWithAttrs(html: string, name: string, attrs: string[]) {
51+
return findElementsWithAttribute(html, name)
52+
.filter(element => attrs.some(attr => hasElementAttribute(element, attr)))
53+
.map(element => getStartOffsetOfAttribute(element, name));
54+
}
55+
56+
/** Shorthand function that checks if the specified element contains the given attribute. */
57+
function hasElementAttribute(element: DefaultTreeElement, attributeName: string): boolean {
58+
return element.attrs && element.attrs.some(attr => attr.name === attributeName.toLowerCase());
59+
}
60+
61+
62+
/** Gets the start offset of the given attribute from a Parse5 element. */
63+
export function getStartOffsetOfAttribute(element: any, attributeName: string): number {
64+
return element.sourceCodeLocation.attrs[attributeName.toLowerCase()].startOffset;
65+
}

src/lib/schematics/update/rules/checkTemplateMiscRule.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
import {bold, green, red} from 'chalk';
1010
import {RuleFailure, Rules} from 'tslint';
1111
import * as ts from 'typescript';
12+
import {findInputsOnElementWithTag, findOutputsOnElementWithTag} from '../html/angular';
1213
import {ExternalResource} from '../tslint/component-file';
1314
import {ComponentWalker} from '../tslint/component-walker';
14-
import {findAll, findAllInputsInElWithTag, findAllOutputsInElWithTag} from '../typescript/literal';
15+
import {findAll} from '../typescript/literal';
1516

1617
/**
1718
* Rule that walks through every component decorator and updates their inline or external
@@ -55,7 +56,7 @@ export class CheckTemplateMiscWalker extends ComponentWalker {
5556
})));
5657

5758
failures = failures.concat(
58-
findAllOutputsInElWithTag(templateContent, 'selectionChange', ['mat-list-option'])
59+
findOutputsOnElementWithTag(templateContent, 'selectionChange', ['mat-list-option'])
5960
.map(offset => ({
6061
start: offset,
6162
end: offset + 'selectionChange'.length,
@@ -65,7 +66,7 @@ export class CheckTemplateMiscWalker extends ComponentWalker {
6566
})));
6667

6768
failures = failures.concat(
68-
findAllOutputsInElWithTag(templateContent, 'selectedChanged', ['mat-datepicker'])
69+
findOutputsOnElementWithTag(templateContent, 'selectedChanged', ['mat-datepicker'])
6970
.map(offset => ({
7071
start: offset,
7172
end: offset + 'selectionChange'.length,
@@ -75,7 +76,7 @@ export class CheckTemplateMiscWalker extends ComponentWalker {
7576
})));
7677

7778
failures = failures.concat(
78-
findAllInputsInElWithTag(templateContent, 'selected', ['mat-button-toggle-group'])
79+
findInputsOnElementWithTag(templateContent, 'selected', ['mat-button-toggle-group'])
7980
.map(offset => ({
8081
start: offset,
8182
end: offset + 'selected'.length,

src/lib/schematics/update/rules/switchTemplateInputNamesRule.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
import {green, red} from 'chalk';
1010
import {Replacement, RuleFailure, Rules} from 'tslint';
1111
import * as ts from 'typescript';
12+
import {findInputsOnElementWithAttr, findInputsOnElementWithTag} from '../html/angular';
1213
import {inputNames} from '../material/data/input-names';
1314
import {ExternalResource} from '../tslint/component-file';
1415
import {ComponentWalker} from '../tslint/component-walker';
15-
import {findAll, findAllInputsInElWithAttr, findAllInputsInElWithTag} from '../typescript/literal';
16+
import {findAll} from '../typescript/literal';
1617

1718
/**
1819
* Rule that walks through every component decorator and updates their inline or external
@@ -53,11 +54,11 @@ export class SwitchTemplateInputNamesWalker extends ComponentWalker {
5354
inputNames.forEach(name => {
5455
let offsets: number[] = [];
5556
if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) {
56-
offsets = offsets.concat(findAllInputsInElWithAttr(
57+
offsets = offsets.concat(findInputsOnElementWithAttr(
5758
templateContent, name.replace, name.whitelist.attributes));
5859
}
5960
if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) {
60-
offsets = offsets.concat(findAllInputsInElWithTag(
61+
offsets = offsets.concat(findInputsOnElementWithTag(
6162
templateContent, name.replace, name.whitelist.elements));
6263
}
6364
if (!name.whitelist) {

src/lib/schematics/update/rules/switchTemplateOutputNamesRule.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@
99
import {green, red} from 'chalk';
1010
import {Replacement, RuleFailure, Rules} from 'tslint';
1111
import * as ts from 'typescript';
12+
import {findOutputsOnElementWithAttr, findOutputsOnElementWithTag} from '../html/angular';
1213
import {outputNames} from '../material/data/output-names';
1314
import {ExternalResource} from '../tslint/component-file';
1415
import {ComponentWalker} from '../tslint/component-walker';
15-
import {
16-
findAll,
17-
findAllOutputsInElWithAttr,
18-
findAllOutputsInElWithTag
19-
} from '../typescript/literal';
16+
import {findAll} from '../typescript/literal';
2017

2118
/**
2219
* Rule that walks through every component decorator and updates their inline or external
@@ -57,11 +54,11 @@ export class SwitchTemplateOutputNamesWalker extends ComponentWalker {
5754
outputNames.forEach(name => {
5855
let offsets: number[] = [];
5956
if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) {
60-
offsets = offsets.concat(findAllOutputsInElWithAttr(
57+
offsets = offsets.concat(findOutputsOnElementWithAttr(
6158
templateContent, name.replace, name.whitelist.attributes));
6259
}
6360
if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) {
64-
offsets = offsets.concat(findAllOutputsInElWithTag(
61+
offsets = offsets.concat(findOutputsOnElementWithTag(
6562
templateContent, name.replace, name.whitelist.elements));
6663
}
6764
if (!name.whitelist) {

src/lib/schematics/update/typescript/literal.ts

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,74 +22,3 @@ export function findAll(str: string, search: string): number[] {
2222
}
2323
return result;
2424
}
25-
26-
export function findAllInputsInElWithTag(html: string, name: string, tagNames: string[]): number[] {
27-
return findAllIoInElWithTag(html, name, tagNames, String.raw`\[?`, String.raw`\]?`);
28-
}
29-
30-
export function findAllOutputsInElWithTag(html: string, name: string, tagNames: string[]):
31-
number[] {
32-
return findAllIoInElWithTag(html, name, tagNames, String.raw`\(`, String.raw`\)`);
33-
}
34-
35-
/**
36-
* Method that can be used to rename all occurrences of an `@Input()` in a HTML string that occur
37-
* inside an element with any of the given attributes. This is useful for replacing an `@Input()` on
38-
* a `@Directive()` with an attribute selector.
39-
*/
40-
export function findAllInputsInElWithAttr(html: string, name: string, attrs: string[]): number[] {
41-
return findAllIoInElWithAttr(html, name, attrs, String.raw`\[?`, String.raw`\]?`);
42-
}
43-
44-
/**
45-
* Method that can be used to rename all occurrences of an `@Output()` in a HTML string that occur
46-
* inside an element with any of the given attributes. This is useful for replacing an `@Output()`
47-
* on a `@Directive()` with an attribute selector.
48-
*/
49-
export function findAllOutputsInElWithAttr(html: string, name: string, attrs: string[]): number[] {
50-
return findAllIoInElWithAttr(html, name, attrs, String.raw`\(`, String.raw`\)`);
51-
}
52-
53-
function findAllIoInElWithTag(html: string, name: string, tagNames: string[],
54-
startIoPattern: string, endIoPattern: string): number[] {
55-
56-
const skipPattern = String.raw`[^>]*\s`;
57-
const openTagPattern = String.raw`<\s*`;
58-
const tagNamesPattern = String.raw`(?:${tagNames.join('|')})`;
59-
const replaceIoPattern = String.raw`
60-
(${openTagPattern}${tagNamesPattern}\s(?:${skipPattern})?${startIoPattern})
61-
${name}
62-
${endIoPattern}[=\s>]`;
63-
const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g');
64-
const result: number[] = [];
65-
66-
let match;
67-
while (match = replaceIoRegex.exec(html)) {
68-
result.push(match.index + match[1].length);
69-
}
70-
71-
return result;
72-
}
73-
74-
function findAllIoInElWithAttr(html: string, name: string, attrs: string[], startIoPattern: string,
75-
endIoPattern: string): number[] {
76-
const skipPattern = String.raw`[^>]*\s`;
77-
const openTagPattern = String.raw`<\s*\S`;
78-
const attrsPattern = String.raw`(?:${attrs.join('|')})`;
79-
const inputAfterAttrPattern = String.raw`
80-
(${openTagPattern}${skipPattern}${attrsPattern}[=\s](?:${skipPattern})?${startIoPattern})
81-
${name}
82-
${endIoPattern}[=\s>]`;
83-
const inputBeforeAttrPattern = String.raw`
84-
(${openTagPattern}${skipPattern}${startIoPattern})
85-
${name}
86-
${endIoPattern}[=\s](?:${skipPattern})?${attrsPattern}[=\s>]`;
87-
const replaceIoPattern = String.raw`${inputAfterAttrPattern}|${inputBeforeAttrPattern}`;
88-
const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g');
89-
const result = [];
90-
let match;
91-
while (match = replaceIoRegex.exec(html)) {
92-
result.push(match.index + (match[1] || match[2]).length);
93-
}
94-
return result;
95-
}

src/lib/schematics/utils/html.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Tree, SchematicsException} from '@angular-devkit/schematics';
10-
import * as parse5 from 'parse5';
10+
import {parse as parseHtml, DefaultTreeDocument, DefaultTreeElement} from 'parse5';
1111
import {getIndexHtmlPath} from './ast';
1212
import {InsertChange} from '@schematics/angular/utility/change';
1313
import {WorkspaceProject} from '@schematics/angular/utility/config';
@@ -17,27 +17,25 @@ import {WorkspaceProject} from '@schematics/angular/utility/config';
1717
* @param src the src path of the html file to parse
1818
*/
1919
export function getHeadTag(src: string) {
20-
const document = parse5.parse(src,
21-
{sourceCodeLocationInfo: true}) as parse5.AST.Default.Document;
20+
const document = parseHtml(src, {sourceCodeLocationInfo: true}) as DefaultTreeDocument;
2221

23-
let head;
24-
const visit = (nodes: parse5.AST.Default.Node[]) => {
22+
let head: DefaultTreeElement;
23+
const visitNodes = nodes => {
2524
nodes.forEach(node => {
26-
const element = <parse5.AST.Default.Element>node;
27-
if (element.tagName === 'head') {
28-
head = element;
25+
if (node.tagName === 'head') {
26+
head = node;
2927
} else {
30-
if (element.childNodes) {
31-
visit(element.childNodes);
28+
if (node.childNodes) {
29+
visitNodes(node.childNodes);
3230
}
3331
}
3432
});
3533
};
3634

37-
visit(document.childNodes);
35+
visitNodes(document.childNodes);
3836

3937
if (!head) {
40-
throw new SchematicsException('Head element not found!');
38+
throw new SchematicsException('Head element could not be found!');
4139
}
4240

4341
return {

0 commit comments

Comments
 (0)