@@ -18,12 +18,21 @@ import { toPath } from '../util/toPath'
18
18
import log from '../util/log'
19
19
import negateValue from '../util/negateValue'
20
20
import isValidArbitraryValue from '../util/isValidArbitraryValue'
21
- import { generateRules } from './generateRules'
21
+ import { generateRules , getClassNameFromSelector } from './generateRules'
22
22
import { hasContentChanged } from './cacheInvalidation.js'
23
23
import { Offsets } from './offsets.js'
24
24
import { flagEnabled } from '../featureFlags.js'
25
+ import { finalizeSelector , formatVariantSelector } from '../util/formatVariantSelector'
25
26
26
- let MATCH_VARIANT = Symbol ( )
27
+ const VARIANT_TYPES = {
28
+ AddVariant : Symbol . for ( 'ADD_VARIANT' ) ,
29
+ MatchVariant : Symbol . for ( 'MATCH_VARIANT' ) ,
30
+ }
31
+
32
+ const VARIANT_INFO = {
33
+ Base : 1 << 0 ,
34
+ Dynamic : 1 << 1 ,
35
+ }
27
36
28
37
function prefix ( context , selector ) {
29
38
let prefix = context . tailwindConfig . prefix
@@ -524,7 +533,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
524
533
let result = variantFunction (
525
534
Object . assign (
526
535
{ modifySelectors, container, separator } ,
527
- variantFunction [ MATCH_VARIANT ] && { args, wrap, format }
536
+ options . type === VARIANT_TYPES . MatchVariant && { args, wrap, format }
528
537
)
529
538
)
530
539
@@ -570,33 +579,34 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
570
579
for ( let [ key , value ] of Object . entries ( options ?. values ?? { } ) ) {
571
580
api . addVariant (
572
581
isSpecial ? `${ variant } ${ key } ` : `${ variant } -${ key } ` ,
573
- Object . assign (
574
- ( { args, container } ) =>
575
- variantFn (
576
- value ,
577
- modifiersEnabled ? { modifier : args . modifier , container } : { container }
578
- ) ,
579
- {
580
- [ MATCH_VARIANT ] : true ,
581
- }
582
- ) ,
583
- { ...options , value, id }
584
- )
585
- }
586
-
587
- api . addVariant (
588
- variant ,
589
- Object . assign (
590
582
( { args, container } ) =>
591
583
variantFn (
592
- args . value ,
584
+ value ,
593
585
modifiersEnabled ? { modifier : args . modifier , container } : { container }
594
586
) ,
595
587
{
596
- [ MATCH_VARIANT ] : true ,
588
+ ...options ,
589
+ value,
590
+ id,
591
+ type : VARIANT_TYPES . MatchVariant ,
592
+ variantInfo : VARIANT_INFO . Base ,
597
593
}
598
- ) ,
599
- { ...options , id }
594
+ )
595
+ }
596
+
597
+ api . addVariant (
598
+ variant ,
599
+ ( { args, container } ) =>
600
+ variantFn (
601
+ args . value ,
602
+ modifiersEnabled ? { modifier : args . modifier , container } : { container }
603
+ ) ,
604
+ {
605
+ ...options ,
606
+ id,
607
+ type : VARIANT_TYPES . MatchVariant ,
608
+ variantInfo : VARIANT_INFO . Dynamic ,
609
+ }
600
610
)
601
611
} ,
602
612
}
@@ -948,6 +958,137 @@ function registerPlugins(plugins, context) {
948
958
949
959
return output
950
960
}
961
+
962
+ // Generate a list of available variants with meta information of the type of variant.
963
+ context . getVariants = function getVariants ( ) {
964
+ let result = [ ]
965
+ for ( let [ name , options ] of context . variantOptions . entries ( ) ) {
966
+ if ( options . variantInfo === VARIANT_INFO . Base ) continue
967
+
968
+ result . push ( {
969
+ name,
970
+ isArbitrary : options . type === Symbol . for ( 'MATCH_VARIANT' ) ,
971
+ values : Object . keys ( options . values ?? { } ) ,
972
+ selectors ( { modifier, value } = { } ) {
973
+ let candidate = '__TAILWIND_PLACEHOLDER__'
974
+
975
+ let rule = postcss . rule ( { selector : `.${ candidate } ` } )
976
+ let container = postcss . root ( { nodes : [ rule . clone ( ) ] } )
977
+
978
+ let before = container . toString ( )
979
+
980
+ let fns = ( context . variantMap . get ( name ) ?? [ ] ) . flatMap ( ( [ _ , fn ] ) => fn )
981
+ let formatStrings = [ ]
982
+ for ( let fn of fns ) {
983
+ let localFormatStrings = [ ]
984
+
985
+ let api = {
986
+ args : { modifier, value : options . values ?. [ value ] ?? value } ,
987
+ separator : context . tailwindConfig . separator ,
988
+ modifySelectors ( modifierFunction ) {
989
+ // Run the modifierFunction over each rule
990
+ container . each ( ( rule ) => {
991
+ if ( rule . type !== 'rule' ) {
992
+ return
993
+ }
994
+
995
+ rule . selectors = rule . selectors . map ( ( selector ) => {
996
+ return modifierFunction ( {
997
+ get className ( ) {
998
+ return getClassNameFromSelector ( selector )
999
+ } ,
1000
+ selector,
1001
+ } )
1002
+ } )
1003
+ } )
1004
+
1005
+ return container
1006
+ } ,
1007
+ format ( str ) {
1008
+ localFormatStrings . push ( str )
1009
+ } ,
1010
+ wrap ( wrapper ) {
1011
+ localFormatStrings . push ( `@${ wrapper . name } ${ wrapper . params } { & }` )
1012
+ } ,
1013
+ container,
1014
+ }
1015
+
1016
+ let ruleWithVariant = fn ( api )
1017
+ if ( localFormatStrings . length > 0 ) {
1018
+ formatStrings . push ( localFormatStrings )
1019
+ }
1020
+
1021
+ if ( Array . isArray ( ruleWithVariant ) ) {
1022
+ for ( let variantFunction of ruleWithVariant ) {
1023
+ localFormatStrings = [ ]
1024
+ variantFunction ( api )
1025
+ formatStrings . push ( localFormatStrings )
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ // Reverse engineer the result of the `container`
1031
+ let manualFormatStrings = [ ]
1032
+ let after = container . toString ( )
1033
+
1034
+ if ( before !== after ) {
1035
+ // Figure out all selectors
1036
+ container . walkRules ( ( rule ) => {
1037
+ let modified = rule . selector
1038
+
1039
+ // Rebuild the base selector, this is what plugin authors would do
1040
+ // as well. E.g.: `${variant}${separator}${className}`.
1041
+ // However, plugin authors probably also prepend or append certain
1042
+ // classes, pseudos, ids, ...
1043
+ let rebuiltBase = selectorParser ( ( selectors ) => {
1044
+ selectors . walkClasses ( ( classNode ) => {
1045
+ classNode . value = `${ name } ${ context . tailwindConfig . separator } ${ classNode . value } `
1046
+ } )
1047
+ } ) . processSync ( modified )
1048
+
1049
+ // Now that we know the original selector, the new selector, and
1050
+ // the rebuild part in between, we can replace the part that plugin
1051
+ // authors need to rebuild with `&`, and eventually store it in the
1052
+ // collectedFormats. Similar to what `format('...')` would do.
1053
+ //
1054
+ // E.g.:
1055
+ // variant: foo
1056
+ // selector: .markdown > p
1057
+ // modified (by plugin): .foo .foo\\:markdown > p
1058
+ // rebuiltBase (internal): .foo\\:markdown > p
1059
+ // format: .foo &
1060
+ manualFormatStrings . push ( modified . replace ( rebuiltBase , '&' ) . replace ( candidate , '&' ) )
1061
+ } )
1062
+
1063
+ // Figure out all atrules
1064
+ container . walkAtRules ( ( atrule ) => {
1065
+ manualFormatStrings . push ( `@${ atrule . name } (${ atrule . params } ) { & }` )
1066
+ } )
1067
+ }
1068
+
1069
+ let result = formatStrings . map ( ( formatString ) =>
1070
+ finalizeSelector ( formatVariantSelector ( '&' , ...formatString ) , {
1071
+ selector : `.${ candidate } ` ,
1072
+ candidate,
1073
+ context,
1074
+ isArbitraryVariant : ! ( value in ( options . values ?? { } ) ) ,
1075
+ } )
1076
+ . replace ( `.${ candidate } ` , '&' )
1077
+ . replace ( '{ & }' , '' )
1078
+ . trim ( )
1079
+ )
1080
+
1081
+ if ( manualFormatStrings . length > 0 ) {
1082
+ result . push ( formatVariantSelector ( '&' , ...manualFormatStrings ) )
1083
+ }
1084
+
1085
+ return result
1086
+ } ,
1087
+ } )
1088
+ }
1089
+
1090
+ return result
1091
+ }
951
1092
}
952
1093
953
1094
/**
0 commit comments