[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 250 lines 8.6 kB view raw
1import type { CSSEntries, DynamicMatcher, Preset, RuleContext } from 'unocss' 2import { cornerMap, directionSize, h } from '@unocss/preset-wind4/utils' 3 4type CollectorChecker = (warning: string, rule: string) => void 5 6// Track warnings to avoid duplicates 7const warnedClasses = new Set<string>() 8 9function warnOnce(message: string, key: string) { 10 if (!warnedClasses.has(key)) { 11 warnedClasses.add(key) 12 // oxlint-disable-next-line no-console -- warn logging 13 console.warn(message) 14 } 15} 16 17/** Reset warning state (for testing) */ 18export function resetRtlWarnings() { 19 warnedClasses.clear() 20} 21 22function reportWarning(match: string, suggestedClass: string, checker?: CollectorChecker) { 23 const message = `${checker ? 'a' : 'A'}void using '${match}', use '${suggestedClass}' instead, or 'force-${match}' to keep physical direction.` 24 if (checker) { 25 checker(message, match) 26 } else { 27 warnOnce(`[RTL] ${message}`, match) 28 } 29} 30 31const directionMap: Record<string, string[]> = { 32 'l': ['-left'], 33 'r': ['-right'], 34 't': ['-top'], 35 'b': ['-bottom'], 36 's': ['-inline-start'], 37 'e': ['-inline-end'], 38 'x': ['-left', '-right'], 39 'y': ['-top', '-bottom'], 40 '': [''], 41 'bs': ['-block-start'], 42 'be': ['-block-end'], 43 'is': ['-inline-start'], 44 'ie': ['-inline-end'], 45 'block': ['-block-start', '-block-end'], 46 'inline': ['-inline-start', '-inline-end'], 47} 48 49function directionSizeRTL( 50 propertyPrefix: string, 51 prefixMap?: { l: string; r: string }, 52 checker?: CollectorChecker, 53): DynamicMatcher { 54 const matcher = directionSize(propertyPrefix) 55 return ([match, direction, size], context) => { 56 if (!size) return undefined 57 const defaultMap = { l: 'is', r: 'ie' } 58 const map = prefixMap || defaultMap 59 const replacement = map[direction as 'l' | 'r'] 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 68 return matcher([match, replacement, size], context) 69 } 70} 71 72function handlerRounded( 73 [, a = '', s = 'DEFAULT']: string[], 74 { theme }: RuleContext<any>, 75): CSSEntries | undefined { 76 const corners = cornerMap[a] 77 if (!corners) return undefined 78 79 if (s === 'full') return corners.map(i => [`border${i}-radius`, 'calc(infinity * 1px)']) 80 81 const _v = theme.radius?.[s] ?? h.bracket?.cssvar?.global?.fraction?.rem?.(s) 82 if (_v != null) { 83 return corners.map(i => [`border${i}-radius`, _v]) 84 } 85} 86 87function handlerBorderSize([, a = '', b = '1']: string[]): CSSEntries | undefined { 88 const v = h.bracket?.cssvar?.global?.px?.(b) 89 const directions = directionMap[a] 90 if (directions && v != null) return directions.map(i => [`border${i}-width`, v]) 91} 92 93function handlerForceDirectionSize( 94 propertyPrefix: string, 95 [, direction, size]: string[], 96 { theme }: RuleContext<any>, 97): CSSEntries | undefined { 98 const v = 99 theme.spacing?.[size || 'DEFAULT'] ?? h.bracket?.cssvar?.global?.auto?.fraction?.rem?.(size!) 100 const directions = directionMap[direction!] 101 102 if (v != null && directions) { 103 return directions.map(i => [`${propertyPrefix}${i}`, v]) 104 } 105} 106 107/** 108 * CSS RTL support to detect, replace and warn wrong left/right usages. 109 */ 110export function presetRtl(checker?: CollectorChecker): Preset { 111 return { 112 name: 'rtl-preset', 113 shortcuts: [ 114 ['text-left', 'text-start x-rtl-start'], 115 ['text-right', 'text-end x-rtl-end'], 116 ], 117 rules: [ 118 // Force physical directions (bypass RTL logic) 119 [ 120 /^force-p([rl])-(.+)?$/, 121 (match, context) => handlerForceDirectionSize('padding', match, context), 122 { autocomplete: 'force-p(l|r)-<num>' }, 123 ], 124 [ 125 /^force-m([rl])-(.+)?$/, 126 (match, context) => handlerForceDirectionSize('margin', match, context), 127 { autocomplete: 'force-m(l|r)-<num>' }, 128 ], 129 [ 130 /^force-(?:position-|pos-)?(left|right)-(.+)$/, 131 ([_, direction, size], context) => { 132 // Map 'left'/'right' to 'l'/'r' for directionMap lookup if needed, 133 // but directionMap has 'left'/'right' keys? No, it has 'l'/'r'. 134 // Wait, directionMap keys are 'l', 'r'. 135 // But inset usually uses 'left', 'right' properties directly. 136 // Let's use a custom handler for inset to be safe. 137 const v = 138 (context.theme as unknown as any).spacing?.[size || 'DEFAULT'] ?? 139 h.bracket?.cssvar?.global?.auto?.fraction?.rem?.(size!) 140 if (v != null) { 141 return [[direction === 'left' ? 'left' : 'right', v]] 142 } 143 }, 144 { autocomplete: 'force-(left|right)-<num>' }, 145 ], 146 [ 147 /^force-text-(left|right)$/, 148 ([, direction]) => ({ 'text-align': direction }), 149 { autocomplete: 'force-text-(left|right)' }, 150 ], 151 [ 152 /^force-rounded-([rl])(?:-(.+))?$/, 153 ([, direction, size], context) => 154 handlerRounded(['', direction!, size ?? 'DEFAULT'], context), 155 { autocomplete: 'force-rounded-(l|r)-<num>' }, 156 ], 157 [ 158 /^force-border-([rl])(?:-(.+))?$/, 159 ([, direction, size]) => handlerBorderSize(['', direction!, size || '1']), 160 { autocomplete: 'force-border-(l|r)-<num>' }, 161 ], 162 163 // RTL overrides 164 // We need to move the dash out of the capturing group to avoid capturing it in the direction 165 [ 166 /^p([rl])-(.+)?$/, 167 directionSizeRTL('padding', { l: 's', r: 'e' }, checker), 168 { autocomplete: '(m|p)<directions>-<num>' }, 169 ], 170 [ 171 /^m([rl])-(.+)?$/, 172 directionSizeRTL('margin', { l: 's', r: 'e' }, checker), 173 { autocomplete: '(m|p)<directions>-<num>' }, 174 ], 175 [ 176 /^(?:position-|pos-)?(left|right)-(.+)$/, 177 ([match, direction, size], context) => { 178 if (!size) return undefined 179 const replacement = direction === 'left' ? 'inset-is' : 'inset-ie' 180 181 const fullClass = context.rawSelector || match 182 // match is 'left-4' or 'position-left-4' 183 // replacement is 'inset-is' or 'inset-ie' 184 // We want 'inset-is-4' 185 const suggestedBase = `${replacement}-${size}` 186 const suggestedClass = fullClass.replace(match, suggestedBase) 187 188 reportWarning(fullClass, suggestedClass, checker) 189 190 return directionSize('inset')(['', direction === 'left' ? 'is' : 'ie', size], context) 191 }, 192 { autocomplete: '(left|right)-<num>' }, 193 ], 194 [ 195 /^x-rtl-(start|end)$/, 196 ([match, direction], context) => { 197 const originalClass = context.rawSelector || match 198 199 const suggestedClass = originalClass.replace( 200 direction === 'start' ? 'left' : 'right', 201 direction!, 202 ) 203 204 reportWarning(originalClass, suggestedClass, checker) 205 206 // Return a cssvar with the warning message to satisfy UnoCSS 207 // and avoid "unmatched utility" warning. 208 return { 209 [`--x-rtl-${direction!}`]: `"${originalClass} -> ${suggestedClass}"`, 210 } 211 }, 212 { autocomplete: 'text-(left|right)' }, 213 ], 214 [ 215 /^rounded-([rl])(?:-(.+))?$/, 216 ([match, direction, size], context) => { 217 if (!direction) return undefined 218 const replacementMap: Record<string, string> = { 219 l: 'is', 220 r: 'ie', 221 } 222 const replacement = replacementMap[direction] 223 if (!replacement) return undefined 224 225 const fullClass = context.rawSelector || match 226 const suggestedBase = match.replace(`rounded-${direction!}`, `rounded-${replacement}`) 227 const suggestedClass = fullClass.replace(match, suggestedBase) 228 229 reportWarning(fullClass, suggestedClass, checker) 230 231 return handlerRounded(['', replacement, size ?? 'DEFAULT'], context) 232 }, 233 ], 234 [ 235 /^border-([rl])(?:-(.+))?$/, 236 ([match, direction, size], context) => { 237 const replacement = direction === 'l' ? 'is' : 'ie' 238 239 const fullClass = context.rawSelector || match 240 const suggestedBase = match.replace(`border-${direction!}`, `border-${replacement}`) 241 const suggestedClass = fullClass.replace(match, suggestedBase) 242 243 reportWarning(fullClass, suggestedClass, checker) 244 245 return handlerBorderSize(['', replacement, size || '1']) 246 }, 247 ], 248 ], 249 } 250}