11import type { CSSEntries , DynamicMatcher , Preset , RuleContext } from 'unocss'
22import { cornerMap , directionSize , h } from '@unocss/preset-wind4/utils'
33
4+ export type CollectorChecker = ( warning : string , rule : string ) => void
5+
46// Track warnings to avoid duplicates
57const warnedClasses = new Set < string > ( )
68
@@ -17,6 +19,15 @@ export function resetRtlWarnings() {
1719 warnedClasses . clear ( )
1820}
1921
22+ function reportWarning ( match : string , suggestedClass : string , checker ?: CollectorChecker ) {
23+ const message = `${ checker ? 'a' : 'A' } void using '${ match } ', use '${ suggestedClass } ' instead.`
24+ if ( checker ) {
25+ checker ( message , match )
26+ } else {
27+ warnOnce ( `[RTL] ${ message } ` , match )
28+ }
29+ }
30+
2031const directionMap : Record < string , string [ ] > = {
2132 'l' : [ '-left' ] ,
2233 'r' : [ '-right' ] ,
@@ -38,18 +49,22 @@ const directionMap: Record<string, string[]> = {
3849function directionSizeRTL (
3950 propertyPrefix : string ,
4051 prefixMap ?: { l : string ; r : string } ,
52+ checker ?: CollectorChecker ,
4153) : DynamicMatcher {
4254 const matcher = directionSize ( propertyPrefix )
43- return ( args , context ) => {
44- const [ match , direction , size ] = args
55+ return ( [ match , direction , size ] , context ) => {
4556 if ( ! size ) return undefined
4657 const defaultMap = { l : 'is' , r : 'ie' }
4758 const map = prefixMap || defaultMap
4859 const replacement = map [ direction as 'l' | 'r' ]
49- warnOnce (
50- `[RTL] Avoid using '${ match } '. Use '${ match . replace ( direction === 'l' ? 'l' : 'r' , replacement ) } ' instead.` ,
51- match ,
52- )
60+
61+ const fullClass = context . rawSelector || match
62+ const prefix = match . substring ( 0 , 1 ) // 'p' or 'm'
63+ const suggestedBase = match . replace ( `${ prefix } ${ direction ! } ` , `${ prefix } ${ replacement } ` )
64+ const suggestedClass = fullClass . replace ( match , suggestedBase )
65+
66+ reportWarning ( fullClass , suggestedClass , checker )
67+
5368 return matcher ( [ match , replacement , size ] , context )
5469 }
5570}
@@ -78,74 +93,96 @@ function handlerBorderSize([, a = '', b = '1']: string[]): CSSEntries | undefine
7893/**
7994 * CSS RTL support to detect, replace and warn wrong left/right usages.
8095 */
81- export function presetRtl ( ) : Preset {
96+ export function presetRtl ( checker ?: CollectorChecker ) : Preset {
8297 return {
8398 name : 'rtl-preset' ,
99+ shortcuts : [
100+ [ 'text-left' , 'text-start x-rtl-start' ] ,
101+ [ 'text-right' , 'text-end x-rtl-end' ] ,
102+ ] ,
84103 rules : [
85104 // RTL overrides
86105 // We need to move the dash out of the capturing group to avoid capturing it in the direction
87106 [
88107 / ^ p ( [ r l ] ) - ( .+ ) ? $ / ,
89- directionSizeRTL ( 'padding' , { l : 's' , r : 'e' } ) ,
108+ directionSizeRTL ( 'padding' , { l : 's' , r : 'e' } , checker ) ,
90109 { autocomplete : '(m|p)<directions>-<num>' } ,
91110 ] ,
92111 [
93112 / ^ m ( [ r l ] ) - ( .+ ) ? $ / ,
94- directionSizeRTL ( 'margin' , { l : 's' , r : 'e' } ) ,
113+ directionSizeRTL ( 'margin' , { l : 's' , r : 'e' } , checker ) ,
95114 { autocomplete : '(m|p)<directions>-<num>' } ,
96115 ] ,
97116 [
98117 / ^ (?: p o s i t i o n - | p o s - ) ? ( l e f t | r i g h t ) - ( .+ ) $ / ,
99- ( [ , direction , size ] , context ) => {
118+ ( [ match , direction , size ] , context ) => {
100119 if ( ! size ) return undefined
101120 const replacement = direction === 'left' ? 'inset-is' : 'inset-ie'
102- warnOnce (
103- `[RTL] Avoid using '${ direction } -${ size } '. Use '${ replacement } -${ size } ' instead.` ,
104- `${ direction } -${ size } ` ,
105- )
121+
122+ const fullClass = context . rawSelector || match
123+ // match is 'left-4' or 'position-left-4'
124+ // replacement is 'inset-is' or 'inset-ie'
125+ // We want 'inset-is-4'
126+ const suggestedBase = `${ replacement } -${ size } `
127+ const suggestedClass = fullClass . replace ( match , suggestedBase )
128+
129+ reportWarning ( fullClass , suggestedClass , checker )
130+
106131 return directionSize ( 'inset' ) ( [ '' , direction === 'left' ? 'is' : 'ie' , size ] , context )
107132 } ,
108133 { autocomplete : '(left|right)-<num>' } ,
109134 ] ,
110135 [
111- / ^ t e x t - ( l e f t | r i g h t ) $ / ,
112- ( [ , direction ] ) => {
113- const replacement = direction === 'left' ? 'start' : 'end'
114- warnOnce (
115- `[RTL] Avoid using 'text-${ direction } '. Use 'text-${ replacement } ' instead.` ,
116- `text-${ direction } ` ,
136+ / ^ x - r t l - ( s t a r t | e n d ) $ / ,
137+ ( [ match , direction ] , context ) => {
138+ const originalClass = context . rawSelector || match
139+
140+ const suggestedClass = originalClass . replace (
141+ direction === 'start' ? 'left' : 'right' ,
142+ direction ! ,
117143 )
118- return { 'text-align' : replacement }
144+
145+ reportWarning ( originalClass , suggestedClass , checker )
146+
147+ // Return a cssvar with the warning message to satisfy UnoCSS
148+ // and avoid "unmatched utility" warning.
149+ return {
150+ [ `--x-rtl-${ direction ! } ` ] : `"${ originalClass } -> ${ suggestedClass } "` ,
151+ }
119152 } ,
120153 { autocomplete : 'text-(left|right)' } ,
121154 ] ,
122155 [
123156 / ^ r o u n d e d - ( [ r l ] ) (?: - ( .+ ) ) ? $ / ,
124- ( args , context ) => {
125- const [ _ , direction , size ] = args
157+ ( [ match , direction , size ] , context ) => {
126158 if ( ! direction ) return undefined
127159 const replacementMap : Record < string , string > = {
128160 l : 'is' ,
129161 r : 'ie' ,
130162 }
131163 const replacement = replacementMap [ direction ]
132164 if ( ! replacement ) return undefined
133- warnOnce (
134- `[RTL] Avoid using 'rounded-${ direction } '. Use 'rounded-${ replacement } ' instead.` ,
135- `rounded-${ direction } ` ,
136- )
165+
166+ const fullClass = context . rawSelector || match
167+ const suggestedBase = match . replace ( `rounded-${ direction ! } ` , `rounded-${ replacement } ` )
168+ const suggestedClass = fullClass . replace ( match , suggestedBase )
169+
170+ reportWarning ( fullClass , suggestedClass , checker )
171+
137172 return handlerRounded ( [ '' , replacement , size ?? 'DEFAULT' ] , context )
138173 } ,
139174 ] ,
140175 [
141176 / ^ b o r d e r - ( [ r l ] ) (?: - ( .+ ) ) ? $ / ,
142- args => {
143- const [ _ , direction , size ] = args
177+ ( [ match , direction , size ] , context ) => {
144178 const replacement = direction === 'l' ? 'is' : 'ie'
145- warnOnce (
146- `[RTL] Avoid using 'border-${ direction } '. Use 'border-${ replacement } ' instead.` ,
147- `border-${ direction } ` ,
148- )
179+
180+ const fullClass = context . rawSelector || match
181+ const suggestedBase = match . replace ( `border-${ direction ! } ` , `border-${ replacement } ` )
182+ const suggestedClass = fullClass . replace ( match , suggestedBase )
183+
184+ reportWarning ( fullClass , suggestedClass , checker )
185+
149186 return handlerBorderSize ( [ '' , replacement , size || '1' ] )
150187 } ,
151188 ] ,
0 commit comments