@@ -75,18 +75,28 @@ module.exports = {
7575
7676 // These are shorthand candidates that do not share the same parent type
7777 const complexEquivalences = [
78- [ [ "overflow-hidden" , "text-ellipsis" , "whitespace-nowrap" ] , "truncate" ]
79- ]
78+ {
79+ needles : [ 'overflow-hidden' , 'text-ellipsis' , 'whitespace-nowrap' ] ,
80+ shorthand : 'truncate' ,
81+ mode : 'exact' ,
82+ } ,
83+ {
84+ needles : [ 'w-' , 'h-' ] ,
85+ shorthand : 'size-' ,
86+ mode : 'value' ,
87+ } ,
88+ ] ;
8089
8190 // Init assets
8291 const targetProperties = {
8392 Layout : [ 'Overflow' , 'Overscroll Behavior' , 'Top / Right / Bottom / Left' ] ,
8493 'Flexbox & Grid' : [ 'Gap' ] ,
8594 Spacing : [ 'Padding' , 'Margin' ] ,
95+ Sizing : [ 'Width' , 'Height' ] ,
8696 Borders : [ 'Border Radius' , 'Border Width' , 'Border Color' ] ,
8797 Tables : [ 'Border Spacing' ] ,
8898 Transforms : [ 'Scale' ] ,
89- Typography : [ 'Text Overflow' , 'Whitespace' ]
99+ Typography : [ 'Text Overflow' , 'Whitespace' ] ,
90100 } ;
91101
92102 // We don't want to affect other rules by object reference
@@ -219,53 +229,79 @@ module.exports = {
219229 const validated = [ ] ;
220230
221231 // Handle sets of classnames with different parent types
222- let remaining = parsed
223- for ( const [ inputSet , outputClassname ] of complexEquivalences ) {
232+ let remaining = parsed ;
233+ for ( const { needles : inputSet , shorthand : outputClassname , mode } of complexEquivalences ) {
224234 if ( remaining . length < inputSet . length ) {
225- continue
226- }
227-
228- const parsedElementsInInputSet = remaining . filter ( remainingClass => inputSet . some ( inputClass => remainingClass . name . includes ( inputClass ) ) )
229-
230- // Make sure all required classes for the shorthand are present
231- if ( parsedElementsInInputSet . length !== inputSet . length ) {
232- continue
235+ continue ;
233236 }
234237
235- // Make sure the classes share all the same variants
236- if ( new Set ( parsedElementsInInputSet . map ( p => p . variants ) ) . size !== 1 ) {
237- continue
238- }
239-
240- // Make sure the classes share all the same importance
241- if ( new Set ( parsedElementsInInputSet . map ( p => p . important ) ) . size !== 1 ) {
242- continue
243- }
238+ // Matching classes
239+ const parsedElementsInInputSet = remaining . filter ( ( remainingClass ) => {
240+ if ( mode === 'exact' ) {
241+ // Test if the name contains the target class, eg. 'text-ellipsis' inside 'md:text-ellipsis'...
242+ return inputSet . some ( ( inputClass ) => remainingClass . name . includes ( inputClass ) ) ;
243+ }
244+ // Test if the body of the class matches, eg. 'h-' inside 'h-10'
245+ if ( mode === 'value' ) {
246+ return inputSet . some ( ( inputClassPattern ) => inputClassPattern === remainingClass . body ) ;
247+ }
248+ } ) ;
244249
245- const index = parsedElementsInInputSet [ 0 ] . index
246- const variants = parsedElementsInInputSet [ 0 ] . variants
247- const important = parsedElementsInInputSet [ 0 ] . important ? "!" : ""
250+ const variantGroups = new Map ( ) ;
251+ parsedElementsInInputSet . forEach ( ( o ) => {
252+ const val = mode === 'value' ? o . value : '' ;
253+ const v = `${ o . variants } ${ o . important ? '!' : '' } ${ val } ` ;
254+ if ( ! variantGroups . has ( v ) ) {
255+ variantGroups . set (
256+ v ,
257+ parsedElementsInInputSet . filter (
258+ ( c ) => c . variants === o . variants && c . important === o . important && ( val === '' || c . value === val )
259+ )
260+ ) ;
261+ }
262+ } ) ;
263+ const validKeys = new Set ( ) ;
264+ variantGroups . forEach ( ( classes , key ) => {
265+ let skip = false ;
266+ // Make sure all required classes for the shorthand are present
267+ if ( classes . length < inputSet . length ) {
268+ skip = true ;
269+ }
270+ // Make sure the classes share all the single/shared/same value
271+ if ( mode === 'value' && new Set ( classes . map ( ( p ) => p . value ) ) . size !== 1 ) {
272+ skip = true ;
273+ }
274+ if ( ! skip ) {
275+ validKeys . add ( key ) ;
276+ }
277+ } ) ;
278+ validKeys . forEach ( ( k ) => {
279+ const candidates = variantGroups . get ( k ) ;
280+ const index = candidates [ 0 ] . index ;
281+ const variants = candidates [ 0 ] . variants ;
282+ const important = candidates [ 0 ] . important ? '!' : '' ;
283+ const classValue = mode === 'value' ? candidates [ 0 ] . value : '' ;
248284
249- const patchedClassname = `${ variants } ${ important } ${ mergedConfig . prefix } ${ outputClassname } `
250- troubles . push ( [ parsedElementsInInputSet . map ( ( c ) => `${ c . name } ` ) , patchedClassname ] ) ;
285+ const patchedClassname = `${ variants } ${ important } ${ mergedConfig . prefix } ${ outputClassname } ${ classValue } ` ;
286+ troubles . push ( [ candidates . map ( ( c ) => `${ c . name } ` ) , patchedClassname ] ) ;
251287
252- const validatedClassname = groupUtil . parseClassname ( patchedClassname , targetGroups , mergedConfig , index )
253- validated . push ( validatedClassname ) ;
288+ const validatedClassname = groupUtil . parseClassname ( patchedClassname , targetGroups , mergedConfig , index ) ;
289+ validated . push ( validatedClassname ) ;
254290
255- remaining = remaining . filter ( p => ! parsedElementsInInputSet . includes ( p ) )
291+ remaining = remaining . filter ( ( p ) => ! candidates . includes ( p ) ) ;
292+ } ) ;
256293 }
257294
258295 // Handle sets of classnames with the same parent type
259-
260296 // Each group parentType
261297 const checkedGroups = [ ] ;
262- remaining . forEach ( ( classname ) => {
298+ remaining . forEach ( ( classname , idx , arr ) => {
263299 // Valid candidate
264300 if ( classname . parentType === '' ) {
265301 validated . push ( classname ) ;
266302 } else if ( ! checkedGroups . includes ( classname . parentType ) ) {
267303 checkedGroups . push ( classname . parentType ) ;
268- const sameType = parsed . filter ( ( cls ) => cls . parentType === classname . parentType ) ;
304+ const sameType = remaining . filter ( ( cls ) => cls . parentType === classname . parentType ) ;
269305 // Comparing same parentType classnames
270306 const checkedVariantsValue = [ ] ;
271307 sameType . forEach ( ( cls ) => {
@@ -404,27 +440,22 @@ module.exports = {
404440 }
405441 }
406442
407- troubles
408- . filter ( ( trouble ) => {
409- // Only valid issue if there are classes to replace
410- return trouble [ 0 ] . length ;
411- } )
412- . forEach ( ( issue ) => {
413- if ( originalClassNamesValue !== validatedClassNamesValue ) {
414- validatedClassNamesValue = prefix + validatedClassNamesValue + suffix ;
415- context . report ( {
416- node : node ,
417- messageId : 'shorthandCandidateDetected' ,
418- data : {
419- classnames : issue [ 0 ] . join ( ', ' ) ,
420- shorthand : issue [ 1 ] ,
421- } ,
422- fix : function ( fixer ) {
423- return fixer . replaceTextRange ( [ start , end ] , validatedClassNamesValue ) ;
424- } ,
425- } ) ;
426- }
427- } ) ;
443+ troubles . forEach ( ( issue ) => {
444+ if ( originalClassNamesValue !== validatedClassNamesValue ) {
445+ validatedClassNamesValue = prefix + validatedClassNamesValue + suffix ;
446+ context . report ( {
447+ node : node ,
448+ messageId : 'shorthandCandidateDetected' ,
449+ data : {
450+ classnames : issue [ 0 ] . join ( ', ' ) ,
451+ shorthand : issue [ 1 ] ,
452+ } ,
453+ fix : function ( fixer ) {
454+ return fixer . replaceTextRange ( [ start , end ] , validatedClassNamesValue ) ;
455+ } ,
456+ } ) ;
457+ }
458+ } ) ;
428459 } ;
429460
430461 //----------------------------------------------------------------------
0 commit comments