@@ -11,7 +11,10 @@ import {SchematicContext} from '@angular-devkit/schematics';
11
11
import * as postcss from 'postcss' ;
12
12
import * as scss from 'postcss-scss' ;
13
13
import { ComponentMigrator , MIGRATORS , PERMANENT_MIGRATORS } from '.' ;
14
- import { RENAMED_TYPOGRAPHY_LEVELS } from './components/typography-hierarchy/constants' ;
14
+ import {
15
+ COMBINED_TYPOGRAPHY_LEVELS ,
16
+ RENAMED_TYPOGRAPHY_LEVELS ,
17
+ } from './components/typography-hierarchy/constants' ;
15
18
import { StyleMigrator } from './style-migrator' ;
16
19
17
20
const COMPONENTS_MIXIN_NAME = / \. ( [ ^ ( ; ] * ) / ;
@@ -259,23 +262,48 @@ function migrateTypographyConfigs(content: string, namespace: string): string {
259
262
const replacements : { start : number ; end : number ; text : string } [ ] = [ ] ;
260
263
261
264
calls . forEach ( ( { name, args} ) => {
262
- const argContent = content . slice ( args . start , args . end ) ;
263
- replacements . push ( { start : name . start , end : name . end , text : newFunctionName } ) ;
265
+ const parameters = extractNamedParameters ( content , args ) ;
266
+ const addedParameters = new Set < string > ( ) ;
264
267
265
268
RENAMED_TYPOGRAPHY_LEVELS . forEach ( ( newName , oldName ) => {
266
- const pattern = new RegExp ( `\\$(${ oldName } ) *:` , 'g' ) ;
267
- let match : RegExpExecArray | null ;
269
+ const correspondingParam = parameters . get ( oldName ) ;
268
270
269
- // Technically each argument can only match once, but keep going just in case.
270
- while ( ( match = pattern . exec ( argContent ) ) ) {
271
- const start = args . start + match . index + 1 ;
271
+ if ( correspondingParam ) {
272
+ addedParameters . add ( newName ) ;
272
273
replacements . push ( {
273
- start,
274
- end : start + match [ 1 ] . length ,
274
+ start : correspondingParam . key . start + 1 , // + 1 to skip over the $ in the parameter name.
275
+ end : correspondingParam . key . end ,
275
276
text : newName ,
276
277
} ) ;
277
278
}
278
279
} ) ;
280
+
281
+ COMBINED_TYPOGRAPHY_LEVELS . forEach ( ( newName , oldName ) => {
282
+ const correspondingParam = parameters . get ( oldName ) ;
283
+
284
+ if ( correspondingParam ) {
285
+ if ( addedParameters . has ( newName ) ) {
286
+ const fullContent = content . slice (
287
+ correspondingParam . key . start ,
288
+ correspondingParam . value . fullEnd ,
289
+ ) ;
290
+ replacements . push ( {
291
+ start : correspondingParam . key . start ,
292
+ end : correspondingParam . value . fullEnd ,
293
+ text : `/* TODO(mdc-migration): No longer supported. Use \`${ newName } \` instead. ${ fullContent } */` ,
294
+ } ) ;
295
+ } else {
296
+ addedParameters . add ( newName ) ;
297
+ replacements . push ( {
298
+ start : correspondingParam . key . start + 1 , // + 1 to skip over the $ in the parameter name.
299
+ end : correspondingParam . key . end ,
300
+ text : newName ,
301
+ } ) ;
302
+ }
303
+ }
304
+ } ) ;
305
+
306
+ replacements . push ( { start : name . start , end : name . end , text : newFunctionName } ) ;
279
307
} ) ;
280
308
281
309
replacements
@@ -330,3 +358,107 @@ function extractFunctionCalls(name: string, content: string) {
330
358
331
359
return results ;
332
360
}
361
+
362
+ /** Extracts all of the named parameters and their values from a string. */
363
+ function extractNamedParameters ( content : string , argsRange : { start : number ; end : number } ) {
364
+ let escapeCount = 0 ;
365
+
366
+ const args = content
367
+ . slice ( argsRange . start , argsRange . end )
368
+ // The top-level function parameters can contain function calls with named parameters of their
369
+ // own (e.g. `$display-4: mat.define-typography-level($font-family: $foo))` which we don't want to
370
+ // extract. Escape everything between parentheses to make it easier to parse out the value later
371
+ // on. Note that we escape with an equal-length string so that the string indexes remain the same.
372
+ . replace ( / \( .* \) / g, current => ++ escapeCount + '◬' . repeat ( current . length - 1 ) ) ;
373
+
374
+ let colonIndex = args . indexOf ( ':' ) ;
375
+
376
+ const params = new Map <
377
+ string ,
378
+ { key : { start : number ; end : number } ; value : { start : number ; end : number ; fullEnd : number } }
379
+ > ( ) ;
380
+
381
+ while ( colonIndex > - 1 ) {
382
+ const keyRange = extractKeyRange ( args , colonIndex ) ;
383
+ const valueRange = extractValueRange ( args , colonIndex ) ;
384
+
385
+ if ( keyRange && valueRange ) {
386
+ // + 1 to exclude the $ in the key name.
387
+ params . set ( args . slice ( keyRange . start + 1 , keyRange . end ) , {
388
+ // Add the argument start offset since the indexes are relative to the argument string.
389
+ key : { start : keyRange . start + argsRange . start , end : keyRange . end + argsRange . start } ,
390
+ value : {
391
+ start : valueRange . start + argsRange . start ,
392
+ end : valueRange . end + argsRange . start ,
393
+ fullEnd : valueRange . fullEnd + argsRange . start ,
394
+ } ,
395
+ } ) ;
396
+ }
397
+
398
+ colonIndex = args . indexOf ( ':' , colonIndex + 1 ) ;
399
+ }
400
+
401
+ return params ;
402
+ }
403
+
404
+ /**
405
+ * Extracts the text range that contains the key of a named Sass parameter, including the leading $.
406
+ * @param content Text content in which to search.
407
+ * @param colonIndex Index of the colon between the key and value.
408
+ * Used as a starting point for the search.
409
+ */
410
+ function extractKeyRange ( content : string , colonIndex : number ) {
411
+ let index = colonIndex - 1 ;
412
+ let start = - 1 ;
413
+ let end = - 1 ;
414
+
415
+ while ( index > - 1 ) {
416
+ const char = content [ index ] ;
417
+ if ( char !== ' ' && char !== '\n' ) {
418
+ if ( end === - 1 ) {
419
+ end = index + 1 ;
420
+ } else if ( char === '$' ) {
421
+ start = index ;
422
+ break ;
423
+ }
424
+ }
425
+ index -- ;
426
+ }
427
+
428
+ return start > - 1 && end > - 1 ? { start, end} : null ;
429
+ }
430
+
431
+ /**
432
+ * Extracts the text range that contains the value of a named Sass parameter.
433
+ * @param content Text content in which to search.
434
+ * @param colonIndex Index of the colon between the key and value.
435
+ * Used as a starting point for the search.
436
+ */
437
+ function extractValueRange ( content : string , colonIndex : number ) {
438
+ let index = colonIndex + 1 ;
439
+ let start = - 1 ;
440
+ let end = - 1 ;
441
+ let fullEnd = - 1 ; // This is the end including any separators (e.g. commas).
442
+
443
+ while ( index < content . length ) {
444
+ const char = content [ index ] ;
445
+ const isWhitespace = char === ' ' || char === '\n' ;
446
+
447
+ if ( ! isWhitespace && start === - 1 ) {
448
+ start = index ;
449
+ } else if ( start > - 1 && ( isWhitespace || char === ',' ) ) {
450
+ end = index ;
451
+ fullEnd = index + 1 ;
452
+ break ;
453
+ }
454
+
455
+ if ( start > - 1 && index === content . length - 1 ) {
456
+ fullEnd = end = content . length ;
457
+ break ;
458
+ }
459
+
460
+ index ++ ;
461
+ }
462
+
463
+ return start > - 1 && end > - 1 ? { start, end, fullEnd} : null ;
464
+ }
0 commit comments