@@ -3,6 +3,19 @@ import * as path from 'path';
33import { SchematicContext , Tree } from '@angular-devkit/schematics' ;
44import { WorkspaceSchema , WorkspaceProject , ProjectType } from '@schematics/angular/utility/workspace-models' ;
55import { execSync } from 'child_process' ;
6+ import {
7+ Attribute ,
8+ Comment ,
9+ Element ,
10+ Expansion ,
11+ ExpansionCase ,
12+ getHtmlTagDefinition ,
13+ HtmlParser ,
14+ Node ,
15+ Text ,
16+ Visitor
17+ } from '@angular/compiler' ;
18+ import { replaceMatch } from './tsUtils' ;
619
720const configPaths = [ '/.angular.json' , '/angular.json' ] ;
821
@@ -111,3 +124,150 @@ export function tryUninstallPackage(context: SchematicContext, packageManager: s
111124 . warn ( `Could not uninstall ${ pkg } , you may want to uninstall it manually.` , JSON . parse ( e ) ) ;
112125 }
113126}
127+
128+ interface TagOffset {
129+ start : number ;
130+ end : number ;
131+ }
132+
133+ export interface SourceOffset {
134+ startTag : TagOffset ;
135+ endTag : TagOffset ;
136+ file : {
137+ content : string ;
138+ url : string ;
139+ } ;
140+ node ?: Element ;
141+ }
142+
143+
144+ export class FileChange {
145+
146+ constructor (
147+ public position = 0 ,
148+ public text = '' ,
149+ public replaceText = '' ,
150+ public type : 'insert' | 'replace' = 'insert'
151+ ) { }
152+
153+ apply ( content : string ) {
154+ if ( this . type === 'insert' ) {
155+ return `${ content . substring ( 0 , this . position ) } ${ this . text } ${ content . substring ( this . position ) } ` ;
156+ }
157+ return replaceMatch ( content , this . replaceText , this . text , this . position ) ;
158+ }
159+ }
160+
161+ /**
162+ * Parses an Angular template file/content and returns an array of the root nodes of the file.
163+ *
164+ * @param host
165+ * @param filePath
166+ * @param encoding
167+ */
168+ export function parseFile ( host : Tree , filePath : string , encoding = 'utf8' ) {
169+ return new HtmlParser ( ) . parse ( host . read ( filePath ) . toString ( encoding ) , filePath ) . rootNodes ;
170+ }
171+
172+ export function findElementNodes ( root : Node [ ] , tag : string | string [ ] ) : Node [ ] {
173+ const tags = new Set ( Array . isArray ( tag ) ? tag : [ tag ] ) ;
174+ return flatten ( Array . isArray ( root ) ? root : [ root ] )
175+ . filter ( ( node : Element ) => tags . has ( node . name ) ) ;
176+ }
177+
178+ export function hasAttribute ( root : Element , attribute : string | string [ ] ) {
179+ const attrs = Array . isArray ( attribute ) ? attribute : [ attribute ] ;
180+ return ! ! root . attrs . find ( a => attrs . includes ( a . name ) ) ;
181+ }
182+
183+ export function getAttribute ( root : Element , attribute : string | string [ ] ) {
184+ const attrs = Array . isArray ( attribute ) ? attribute : [ attribute ] ;
185+ return root . attrs . filter ( a => attrs . includes ( a . name ) ) ;
186+ }
187+
188+ export function getSourceOffset ( element : Element ) : SourceOffset {
189+ const { startSourceSpan, endSourceSpan } = element ;
190+ return {
191+ startTag : { start : startSourceSpan . start . offset , end : startSourceSpan . end . offset } ,
192+ endTag : { start : endSourceSpan . start . offset , end : endSourceSpan . end . offset } ,
193+ file : {
194+ content : startSourceSpan . start . file . content ,
195+ url : startSourceSpan . start . file . url
196+ } ,
197+ node : element
198+ } ;
199+ }
200+
201+
202+ function isElement ( node : Node | Element ) : node is Element {
203+ return ( node as Element ) . children !== undefined ;
204+ }
205+
206+ /**
207+ * Given an array of `Node` objects, flattens the ast tree to a single array.
208+ * De facto only `Element` type objects have children.
209+ *
210+ * @param list
211+ */
212+ export function flatten ( list : Node [ ] ) {
213+ let node : Node ;
214+ let r : Node [ ] = [ ] ;
215+
216+ for ( let i = 0 ; i < list . length ; i ++ ) {
217+ node = list [ i ] ;
218+ r . push ( node ) ;
219+
220+ if ( isElement ( node ) ) {
221+ r = r . concat ( flatten ( node . children ) ) ;
222+ }
223+ }
224+ return r ;
225+ }
226+
227+ /**
228+ * https://github.com/angular/angular/blob/master/packages/compiler/test/ml_parser/util/util.ts
229+ *
230+ * May be useful for validating the output of our own migrations,
231+ */
232+ class SerializerVisitor implements Visitor {
233+
234+ visitElement ( element : Element , context : any ) : any {
235+ if ( getHtmlTagDefinition ( element . name ) . isVoid ) {
236+ return `<${ element . name } ${ this . _visitAll ( element . attrs , ' ' ) } />` ;
237+ }
238+
239+ return `<${ element . name } ${ this . _visitAll ( element . attrs , ' ' ) } >${ this . _visitAll ( element . children ) } </${ element . name } >` ;
240+ }
241+
242+ visitAttribute ( attribute : Attribute , context : any ) : any {
243+ return attribute . value === '' ? `${ attribute . name } ` : `${ attribute . name } ="${ attribute . value } "` ;
244+ }
245+
246+ visitText ( text : Text , context : any ) : any {
247+ return text . value ;
248+ }
249+
250+ visitComment ( comment : Comment , context : any ) : any {
251+ return `<!--${ comment . value } -->` ;
252+ }
253+
254+ visitExpansion ( expansion : Expansion , context : any ) : any {
255+ return `{${ expansion . switchValue } , ${ expansion . type } ,${ this . _visitAll ( expansion . cases ) } }` ;
256+ }
257+
258+ visitExpansionCase ( expansionCase : ExpansionCase , context : any ) : any {
259+ return ` ${ expansionCase . value } {${ this . _visitAll ( expansionCase . expression ) } }` ;
260+ }
261+
262+ private _visitAll ( nodes : Node [ ] , join : string = '' ) : string {
263+ if ( nodes . length === 0 ) {
264+ return '' ;
265+ }
266+ return join + nodes . map ( a => a . visit ( this , null ) ) . join ( join ) ;
267+ }
268+ }
269+
270+
271+ export function serializeNodes ( nodes : Node [ ] ) : string [ ] {
272+ return nodes . map ( node => node . visit ( new SerializerVisitor ( ) , null ) ) ;
273+ }
0 commit comments