@@ -14,15 +14,35 @@ import {ComponentMigrator, MIGRATORS} from '.';
14
14
15
15
const COMPONENTS_MIXIN_NAME = / \. ( [ ^ ( ; ] * ) / ;
16
16
17
+ /**
18
+ * Mapping between the renamed legacy typography levels and their new non-legacy names. Based on
19
+ * the mappings in `private-typography-to-2018-config` from `core/typography/_typography.scss`.
20
+ */
21
+ const RENAMED_TYPOGRAPHY_LEVELS = new Map ( [
22
+ [ 'display-4' , 'headline-1' ] ,
23
+ [ 'display-3' , 'headline-2' ] ,
24
+ [ 'display-2' , 'headline-3' ] ,
25
+ [ 'display-1' , 'headline-4' ] ,
26
+ [ 'headline' , 'headline-5' ] ,
27
+ [ 'title' , 'headline-6' ] ,
28
+ [ 'subheading-2' , 'subtitle-1' ] ,
29
+ [ 'body-2' , 'subtitle-2' ] ,
30
+ [ 'subheading-1' , 'body-1' ] ,
31
+ [ 'body-1' , 'body-2' ] ,
32
+ ] ) ;
33
+
17
34
export class ThemingStylesMigration extends Migration < ComponentMigrator [ ] , SchematicContext > {
18
35
enabled = true ;
19
- namespace : string ;
36
+ private _namespace : string ;
20
37
21
38
override visitStylesheet ( stylesheet : ResolvedResource ) {
22
- const migratedContent = this . migrate ( stylesheet . content , stylesheet . filePath ) . replace (
23
- new RegExp ( `${ this . namespace } .define-legacy-typography-config\\(` , 'g' ) ,
24
- `${ this . namespace } .define-typography-config(` ,
25
- ) ;
39
+ let migratedContent = this . migrate ( stylesheet . content , stylesheet . filePath ) ;
40
+
41
+ // Note: needs to run after `migrate` so that the `namespace` has been resolved.
42
+ if ( this . _namespace ) {
43
+ migratedContent = migrateTypographyConfigs ( migratedContent , this . _namespace ) ;
44
+ }
45
+
26
46
this . fileSystem
27
47
. edit ( stylesheet . filePath )
28
48
. remove ( stylesheet . start , stylesheet . content . length )
@@ -52,16 +72,16 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
52
72
53
73
atUseHandler ( atRule : postcss . AtRule ) {
54
74
if ( isAngularMaterialImport ( atRule ) ) {
55
- this . namespace = parseNamespace ( atRule ) ;
75
+ this . _namespace = parseNamespace ( atRule ) ;
56
76
}
57
77
}
58
78
59
79
atIncludeHandler ( atRule : postcss . AtRule ) {
60
80
const migrator = this . upgradeData . find ( m => {
61
- return m . styles . isLegacyMixin ( this . namespace , atRule ) ;
81
+ return m . styles . isLegacyMixin ( this . _namespace , atRule ) ;
62
82
} ) ;
63
83
if ( migrator ) {
64
- const mixinChange = migrator . styles . getMixinChange ( this . namespace , atRule ) ;
84
+ const mixinChange = migrator . styles . getMixinChange ( this . _namespace , atRule ) ;
65
85
if ( mixinChange ) {
66
86
if ( mixinChange . new ) {
67
87
replaceAtRuleWithMultiple ( atRule , mixinChange . old , mixinChange . new ) ;
@@ -79,14 +99,14 @@ export class ThemingStylesMigration extends Migration<ComponentMigrator[], Schem
79
99
return ;
80
100
}
81
101
}
82
- replaceCrossCuttingMixin ( atRule , this . namespace ) ;
102
+ replaceCrossCuttingMixin ( atRule , this . _namespace ) ;
83
103
}
84
104
}
85
105
86
106
isCrossCuttingMixin ( mixinText : string ) {
87
107
return [
88
- `${ this . namespace } \\.all-legacy-component-` ,
89
- `${ this . namespace } \\.legacy-core([^-]|$)` ,
108
+ `${ this . _namespace } \\.all-legacy-component-` ,
109
+ `${ this . _namespace } \\.legacy-core([^-]|$)` ,
90
110
] . some ( r => new RegExp ( r ) . test ( mixinText ) ) ;
91
111
}
92
112
@@ -235,3 +255,86 @@ function replaceAtRuleWithMultiple(
235
255
}
236
256
atRule . remove ( ) ;
237
257
}
258
+
259
+ /**
260
+ * Migrates all of the `define-legacy-typography-config` calls within a file.
261
+ * @param content Content of the file to be migrated.
262
+ * @param namespace Namespace under which `define-legacy-typography-config` is being used.
263
+ */
264
+ function migrateTypographyConfigs ( content : string , namespace : string ) : string {
265
+ const calls = extractFunctionCalls ( `${ namespace } .define-legacy-typography-config` , content ) ;
266
+ const newFunctionName = `${ namespace } .define-typography-config` ;
267
+ const replacements : { start : number ; end : number ; text : string } [ ] = [ ] ;
268
+
269
+ calls . forEach ( ( { name, args} ) => {
270
+ const argContent = content . slice ( args . start , args . end ) ;
271
+ replacements . push ( { start : name . start , end : name . end , text : newFunctionName } ) ;
272
+
273
+ RENAMED_TYPOGRAPHY_LEVELS . forEach ( ( newName , oldName ) => {
274
+ const pattern = new RegExp ( `\\$(${ oldName } ) *:` , 'g' ) ;
275
+ let match : RegExpExecArray | null ;
276
+
277
+ // Technically each argument can only match once, but keep going just in case.
278
+ while ( ( match = pattern . exec ( argContent ) ) ) {
279
+ const start = args . start + match . index + 1 ;
280
+ replacements . push ( {
281
+ start,
282
+ end : start + match [ 1 ] . length ,
283
+ text : newName ,
284
+ } ) ;
285
+ }
286
+ } ) ;
287
+ } ) ;
288
+
289
+ replacements
290
+ . sort ( ( a , b ) => b . start - a . start )
291
+ . forEach (
292
+ ( { start, end, text} ) => ( content = content . slice ( 0 , start ) + text + content . slice ( end ) ) ,
293
+ ) ;
294
+
295
+ return content ;
296
+ }
297
+
298
+ /**
299
+ * Extracts the spans of all calls of a specific Sass function within a file.
300
+ * @param name Name of the function to look for.
301
+ * @param content Content of the file being searched.
302
+ */
303
+ function extractFunctionCalls ( name : string , content : string ) {
304
+ const results : { name : { start : number ; end : number } ; args : { start : number ; end : number } } [ ] = [ ] ;
305
+ const callString = name + '(' ;
306
+ let index = content . indexOf ( callString ) ;
307
+
308
+ // This would be much simpler with a regex, but it can be fragile when it comes to nested
309
+ // parentheses. We use this manual parsing which should be more reliable.
310
+ while ( index > - 1 ) {
311
+ let openParens = 0 ;
312
+ let endIndex = - 1 ;
313
+ let nameEnd = index + callString . length - 1 ; // -1 to exclude the opening paren.
314
+
315
+ for ( let i = nameEnd ; i < content . length ; i ++ ) {
316
+ const char = content [ i ] ;
317
+
318
+ if ( char === '(' ) {
319
+ openParens ++ ;
320
+ } else if ( char === ')' ) {
321
+ openParens -- ;
322
+
323
+ if ( openParens === 0 ) {
324
+ endIndex = i ;
325
+ break ;
326
+ }
327
+ }
328
+ }
329
+
330
+ // Invalid call, skip over it.
331
+ if ( endIndex === - 1 ) {
332
+ index = content . indexOf ( callString , nameEnd + 1 ) ;
333
+ } else {
334
+ results . push ( { name : { start : index , end : nameEnd } , args : { start : nameEnd + 1 , end : endIndex } } ) ;
335
+ index = content . indexOf ( callString , endIndex ) ;
336
+ }
337
+ }
338
+
339
+ return results ;
340
+ }
0 commit comments