@@ -11,7 +11,10 @@ import {SchematicContext} from '@angular-devkit/schematics';
1111import * as postcss from 'postcss' ;
1212import * as scss from 'postcss-scss' ;
1313import { 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' ;
1518import { StyleMigrator } from './style-migrator' ;
1619
1720const COMPONENTS_MIXIN_NAME = / \. ( [ ^ ( ; ] * ) / ;
@@ -259,23 +262,48 @@ function migrateTypographyConfigs(content: string, namespace: string): string {
259262 const replacements : { start : number ; end : number ; text : string } [ ] = [ ] ;
260263
261264 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 > ( ) ;
264267
265268 RENAMED_TYPOGRAPHY_LEVELS . forEach ( ( newName , oldName ) => {
266- const pattern = new RegExp ( `\\$(${ oldName } ) *:` , 'g' ) ;
267- let match : RegExpExecArray | null ;
269+ const correspondingParam = parameters . get ( oldName ) ;
268270
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 ) ;
272273 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 ,
275276 text : newName ,
276277 } ) ;
277278 }
278279 } ) ;
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 } ) ;
279307 } ) ;
280308
281309 replacements
@@ -330,3 +358,107 @@ function extractFunctionCalls(name: string, content: string) {
330358
331359 return results ;
332360}
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