@@ -29,11 +29,20 @@ interface ExtraSymbols {
29
29
variables ?: Record < string , string > ;
30
30
}
31
31
32
+ /** Possible pairs of comment characters in a Sass file. */
33
+ const commentPairs = new Map < string , string > ( [ [ '/*' , '*/' ] , [ '//' , '\n' ] ] ) ;
34
+
35
+ /** Prefix for the placeholder that will be used to escape comments. */
36
+ const commentPlaceholderStart = '__<<ngThemingMigrationEscapedComment' ;
37
+
38
+ /** Suffix for the comment escape placeholder. */
39
+ const commentPlaceholderEnd = '>>__' ;
40
+
32
41
/**
33
42
* Migrates the content of a file to the new theming API. Note that this migration is using plain
34
43
* string manipulation, rather than the AST from PostCSS and the schematics string manipulation
35
44
* APIs, because it allows us to run it inside g3 and to avoid introducing new dependencies.
36
- * @param content Content of the file.
45
+ * @param fileContent Content of the file.
37
46
* @param oldMaterialPrefix Prefix with which the old Material imports should start.
38
47
* Has to end with a slash. E.g. if `@import '~@angular/material/theming'` should be
39
48
* matched, the prefix would be `~@angular/material/`.
@@ -44,21 +53,22 @@ interface ExtraSymbols {
44
53
* @param newCdkImportPath New import to the CDK Sass APIs (e.g. `~@angular/cdk`).
45
54
* @param excludedImports Pattern that can be used to exclude imports from being processed.
46
55
*/
47
- export function migrateFileContent ( content : string ,
56
+ export function migrateFileContent ( fileContent : string ,
48
57
oldMaterialPrefix : string ,
49
58
oldCdkPrefix : string ,
50
59
newMaterialImportPath : string ,
51
60
newCdkImportPath : string ,
52
61
extraMaterialSymbols : ExtraSymbols = { } ,
53
62
excludedImports ?: RegExp ) : string {
63
+ let { content, placeholders} = escapeComments ( fileContent ) ;
54
64
const materialResults = detectImports ( content , oldMaterialPrefix , excludedImports ) ;
55
65
const cdkResults = detectImports ( content , oldCdkPrefix , excludedImports ) ;
56
66
57
67
// Try to migrate the symbols even if there are no imports. This is used
58
68
// to cover the case where the Components symbols were used transitively.
59
- content = migrateCdkSymbols ( content , newCdkImportPath , cdkResults ) ;
69
+ content = migrateCdkSymbols ( content , newCdkImportPath , placeholders , cdkResults ) ;
60
70
content = migrateMaterialSymbols (
61
- content , newMaterialImportPath , materialResults , extraMaterialSymbols ) ;
71
+ content , newMaterialImportPath , materialResults , placeholders , extraMaterialSymbols ) ;
62
72
content = replaceRemovedVariables ( content , removedMaterialVariables ) ;
63
73
64
74
// We can assume that the migration has taken care of any Components symbols that were
@@ -73,7 +83,7 @@ export function migrateFileContent(content: string,
73
83
content = removeStrings ( content , cdkResults . imports ) ;
74
84
}
75
85
76
- return content ;
86
+ return restoreComments ( content , placeholders ) ;
77
87
}
78
88
79
89
/**
@@ -121,6 +131,7 @@ function detectImports(content: string, prefix: string,
121
131
/** Migrates the Material symbols in a file. */
122
132
function migrateMaterialSymbols ( content : string , importPath : string ,
123
133
detectedImports : DetectImportResult ,
134
+ commentPlaceholders : Record < string , string > ,
124
135
extraMaterialSymbols : ExtraSymbols = { } ) : string {
125
136
const initialContent = content ;
126
137
const namespace = 'mat' ;
@@ -142,14 +153,15 @@ function migrateMaterialSymbols(content: string, importPath: string,
142
153
143
154
if ( content !== initialContent ) {
144
155
// Add an import to the new API only if any of the APIs were being used.
145
- content = insertUseStatement ( content , importPath , namespace ) ;
156
+ content = insertUseStatement ( content , importPath , namespace , commentPlaceholders ) ;
146
157
}
147
158
148
159
return content ;
149
160
}
150
161
151
162
/** Migrates the CDK symbols in a file. */
152
163
function migrateCdkSymbols ( content : string , importPath : string ,
164
+ commentPlaceholders : Record < string , string > ,
153
165
detectedImports : DetectImportResult ) : string {
154
166
const initialContent = content ;
155
167
const namespace = 'cdk' ;
@@ -165,7 +177,7 @@ function migrateCdkSymbols(content: string, importPath: string,
165
177
// Previously the CDK symbols were exposed through `material/theming`, but now we have a
166
178
// dedicated entrypoint for the CDK. Only add an import for it if any of the symbols are used.
167
179
if ( content !== initialContent ) {
168
- content = insertUseStatement ( content , importPath , namespace ) ;
180
+ content = insertUseStatement ( content , importPath , namespace , commentPlaceholders ) ;
169
181
}
170
182
171
183
return content ;
@@ -203,7 +215,8 @@ function renameSymbols(content: string,
203
215
}
204
216
205
217
/** Inserts an `@use` statement in a string. */
206
- function insertUseStatement ( content : string , importPath : string , namespace : string ) : string {
218
+ function insertUseStatement ( content : string , importPath : string , namespace : string ,
219
+ commentPlaceholders : Record < string , string > ) : string {
207
220
// If the content already has the `@use` import, we don't need to add anything.
208
221
if ( new RegExp ( `@use +['"]${ importPath } ['"]` , 'g' ) . test ( content ) ) {
209
222
return content ;
@@ -217,9 +230,15 @@ function insertUseStatement(content: string, importPath: string, namespace: stri
217
230
let newImportIndex = 0 ;
218
231
219
232
// One special case is if the file starts with a license header which we want to preserve on top.
220
- if ( content . trim ( ) . startsWith ( '/*' ) ) {
221
- const commentEndIndex = content . indexOf ( '*/' , content . indexOf ( '/*' ) ) ;
222
- newImportIndex = content . indexOf ( '\n' , commentEndIndex ) + 1 ;
233
+ if ( content . trim ( ) . startsWith ( commentPlaceholderStart ) ) {
234
+ const commentStartIndex = content . indexOf ( commentPlaceholderStart ) ;
235
+ newImportIndex = content . indexOf ( commentPlaceholderEnd , commentStartIndex + 1 ) +
236
+ commentPlaceholderEnd . length ;
237
+ // If the leading comment doesn't end with a newline,
238
+ // we need to insert the import at the next line.
239
+ if ( ! commentPlaceholders [ content . slice ( commentStartIndex , newImportIndex ) ] . endsWith ( '\n' ) ) {
240
+ newImportIndex = Math . max ( newImportIndex , content . indexOf ( '\n' , newImportIndex ) + 1 ) ;
241
+ }
223
242
}
224
243
225
244
return content . slice ( 0 , newImportIndex ) + `@use '${ importPath } ' as ${ namespace } ;\n` +
@@ -327,3 +346,48 @@ function replaceRemovedVariables(content: string, variables: Record<string, stri
327
346
328
347
return content ;
329
348
}
349
+
350
+
351
+ /**
352
+ * Replaces all of the comments in a Sass file with placeholders and
353
+ * returns the list of placeholders so they can be restored later.
354
+ */
355
+ function escapeComments ( content : string ) : { content : string , placeholders : Record < string , string > } {
356
+ const placeholders : Record < string , string > = { } ;
357
+ let commentCounter = 0 ;
358
+ let [ openIndex , closeIndex ] = findComment ( content ) ;
359
+
360
+ while ( openIndex > - 1 && closeIndex > - 1 ) {
361
+ const placeholder = commentPlaceholderStart + ( commentCounter ++ ) + commentPlaceholderEnd ;
362
+ placeholders [ placeholder ] = content . slice ( openIndex , closeIndex ) ;
363
+ content = content . slice ( 0 , openIndex ) + placeholder + content . slice ( closeIndex ) ;
364
+ [ openIndex , closeIndex ] = findComment ( content ) ;
365
+ }
366
+
367
+ return { content, placeholders} ;
368
+ }
369
+
370
+ /** Finds the start and end index of a comment in a file. */
371
+ function findComment ( content : string ) : [ openIndex : number , closeIndex : number ] {
372
+ // Add an extra new line at the end so that we can correctly capture single-line comments
373
+ // at the end of the file. It doesn't really matter that the end index will be out of bounds,
374
+ // because `String.prototype.slice` will clamp it to the string length.
375
+ content += '\n' ;
376
+
377
+ for ( const [ open , close ] of commentPairs . entries ( ) ) {
378
+ const openIndex = content . indexOf ( open ) ;
379
+
380
+ if ( openIndex > - 1 ) {
381
+ const closeIndex = content . indexOf ( close , openIndex + 1 ) ;
382
+ return closeIndex > - 1 ? [ openIndex , closeIndex + close . length ] : [ - 1 , - 1 ] ;
383
+ }
384
+ }
385
+
386
+ return [ - 1 , - 1 ] ;
387
+ }
388
+
389
+ /** Restores the comments that have been escaped by `escapeComments`. */
390
+ function restoreComments ( content : string , placeholders : Record < string , string > ) : string {
391
+ Object . keys ( placeholders ) . forEach ( key => content = content . replace ( key , placeholders [ key ] ) ) ;
392
+ return content ;
393
+ }
0 commit comments