Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
98d09ff
commit 33c3277
Showing
4 changed files
with
179 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import {visitElements, parseTemplate, replaceStartTag, replaceEndTag} from './tree-traversal'; | ||
|
||
function runTagNameDuplicationTest(html: string, result: string): void { | ||
visitElements( | ||
parseTemplate(html).nodes, | ||
node => { | ||
html = replaceEndTag(html, node, node.name.repeat(2)); | ||
}, | ||
node => { | ||
html = replaceStartTag(html, node, node.name.repeat(2)); | ||
}, | ||
); | ||
expect(html).toBe(result); | ||
} | ||
|
||
describe('#visitElements', () => { | ||
describe('tag name replacements', () => { | ||
it('should handle basic cases', async () => { | ||
runTagNameDuplicationTest('<a></a>', '<aa></aa>'); | ||
}); | ||
|
||
it('should handle multiple same line', async () => { | ||
runTagNameDuplicationTest('<a></a><b></b>', '<aa></aa><bb></bb>'); | ||
}); | ||
|
||
it('should handle multiple same line nested', async () => { | ||
runTagNameDuplicationTest('<a><b></b></a>', '<aa><bb></bb></aa>'); | ||
}); | ||
|
||
it('should handle multiple same line nested and unnested', async () => { | ||
runTagNameDuplicationTest('<a><b></b><c></c></a>', '<aa><bb></bb><cc></cc></aa>'); | ||
}); | ||
|
||
it('should handle multiple multi-line', async () => { | ||
runTagNameDuplicationTest( | ||
` | ||
<a></a> | ||
<b></b> | ||
`, | ||
` | ||
<aa></aa> | ||
<bb></bb> | ||
`, | ||
); | ||
}); | ||
|
||
it('should handle multiple multi-line nested', async () => { | ||
runTagNameDuplicationTest( | ||
` | ||
<a> | ||
<b></b> | ||
</a> | ||
`, | ||
` | ||
<aa> | ||
<bb></bb> | ||
</aa> | ||
`, | ||
); | ||
}); | ||
|
||
it('should handle multiple multi-line nested and unnested', async () => { | ||
runTagNameDuplicationTest( | ||
` | ||
<a> | ||
<b></b> | ||
<c></c> | ||
</a> | ||
`, | ||
` | ||
<aa> | ||
<bb></bb> | ||
<cc></cc> | ||
</aa> | ||
`, | ||
); | ||
}); | ||
}); | ||
}); |
97 changes: 97 additions & 0 deletions
97
src/material/schematics/ng-generate/mdc-migration/rules/tree-traversal.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* @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 * as compiler from '@angular/compiler'; | ||
|
||
/** | ||
* Traverses the given tree of nodes and runs the given callbacks for each Element node encountered. | ||
* | ||
* Note that updates to the start tags of html element should be done in the postorder callback, | ||
* and updates to the end tags of html elements should be done in the preorder callback to avoid | ||
* issues with line collisions. | ||
* | ||
* @param nodes The nodes of the ast from a parsed template. | ||
* @param preorderCallback A function that gets run for each Element node in a preorder traversal. | ||
* @param postorderCallback A function that gets run for each Element node in a postorder traversal. | ||
*/ | ||
export function visitElements( | ||
nodes: compiler.TmplAstNode[], | ||
preorderCallback: (node: compiler.TmplAstElement) => void = () => {}, | ||
postorderCallback: (node: compiler.TmplAstElement) => void = () => {}, | ||
): void { | ||
nodes.reverse(); | ||
for (let i = 0; i < nodes.length; i++) { | ||
const node = nodes[i]; | ||
if (node instanceof compiler.TmplAstElement) { | ||
preorderCallback(node); | ||
visitElements(node.children, preorderCallback, postorderCallback); | ||
postorderCallback(node); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A wrapper for the Angular compilers parseTemplate, which passes the correct options to ensure | ||
* the parsed template is accurate. | ||
* | ||
* For more details, see https://github.com/angular/angular/blob/4332897baa2226ef246ee054fdd5254e3c129109/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts#L230. | ||
* | ||
* @param html text of the template to parse | ||
* @param filePath URL to use for source mapping of the parsed template | ||
* @returns the updated template html. | ||
*/ | ||
export function parseTemplate(template: string, templateUrl: string = ''): compiler.ParsedTemplate { | ||
return compiler.parseTemplate(template, templateUrl, { | ||
preserveWhitespaces: true, | ||
preserveLineEndings: true, | ||
leadingTriviaChars: [], | ||
}); | ||
} | ||
|
||
/** | ||
* Replaces the start tag of the given Element node inside of the html document with a new tag name. | ||
* | ||
* @param html The full html document. | ||
* @param node The Element node to be updated. | ||
* @param tag A new tag name. | ||
* @returns an updated html document. | ||
*/ | ||
export function replaceStartTag(html: string, node: compiler.TmplAstElement, tag: string): string { | ||
return replaceAt(html, node.startSourceSpan.start.offset + 1, node.name, tag); | ||
} | ||
|
||
/** | ||
* Replaces the end tag of the given Element node inside of the html document with a new tag name. | ||
* | ||
* @param html The full html document. | ||
* @param node The Element node to be updated. | ||
* @param tag A new tag name. | ||
* @returns an updated html document. | ||
*/ | ||
export function replaceEndTag(html: string, node: compiler.TmplAstElement, tag: string): string { | ||
if (!node.endSourceSpan) { | ||
return html; | ||
} | ||
return replaceAt(html, node.endSourceSpan.start.offset + 2, node.name, tag); | ||
} | ||
|
||
/** | ||
* Replaces a substring of a given string starting at some offset index. | ||
* | ||
* @param str A string to be updated. | ||
* @param offset An offset index to start at. | ||
* @param oldSubstr The old substring to be replaced. | ||
* @param newSubstr A new substring. | ||
* @returns the updated string. | ||
*/ | ||
function replaceAt(str: string, offset: number, oldSubstr: string, newSubstr: string): string { | ||
const index = offset; | ||
const prefix = str.slice(0, index); | ||
const suffix = str.slice(index + oldSubstr.length); | ||
return prefix + newSubstr + suffix; | ||
} |