[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.

chore: add CSS RTL detector (#539)

authored by

Joaquín Sánchez and committed by
GitHub
ebe40024 95749a4a

+192 -1
+57
test/unit/uno-preset-rtl.spec.ts
··· 1 + import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' 2 + import { presetRtl } from '../../uno-preset-rtl' 3 + import { createGenerator } from 'unocss' 4 + 5 + describe('uno-preset-rtl', () => { 6 + let warnSpy: MockInstance 7 + 8 + beforeEach(() => { 9 + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 10 + }) 11 + 12 + afterEach(() => { 13 + warnSpy.mockRestore() 14 + }) 15 + 16 + it('rtl rules replace css styles correctly', async () => { 17 + const uno = await createGenerator({ 18 + presets: [presetRtl()], 19 + }) 20 + 21 + const { css } = await uno.generate( 22 + 'left-0 right-0 pl-1 ml-1 pr-1 mr-1 text-left text-right border-l border-r rounded-l rounded-r', 23 + ) 24 + 25 + expect(css).toMatchInlineSnapshot(` 26 + "/* layer: default */ 27 + .pl-1{padding-inline-start:calc(var(--spacing) * 1);} 28 + .pr-1{padding-inline-end:calc(var(--spacing) * 1);} 29 + .ml-1{margin-inline-start:calc(var(--spacing) * 1);} 30 + .mr-1{margin-inline-end:calc(var(--spacing) * 1);} 31 + .left-0{inset-inline-start:calc(var(--spacing) * 0);} 32 + .right-0{inset-inline-end:calc(var(--spacing) * 0);} 33 + .text-left{text-align:start;} 34 + .text-right{text-align:end;} 35 + .border-l{border-inline-start-width:1px;} 36 + .border-r{border-inline-end-width:1px;}" 37 + `) 38 + 39 + const warnings = warnSpy.mock.calls.flat() 40 + expect(warnings).toMatchInlineSnapshot(` 41 + [ 42 + "[RTL] Avoid using 'left-0'. Use 'inset-is-0' instead.", 43 + "[RTL] Avoid using 'right-0'. Use 'inset-ie-0' instead.", 44 + "[RTL] Avoid using 'pl-1'. Use 'ps-1' instead.", 45 + "[RTL] Avoid using 'ml-1'. Use 'ms-1' instead.", 46 + "[RTL] Avoid using 'pr-1'. Use 'pe-1' instead.", 47 + "[RTL] Avoid using 'mr-1'. Use 'me-1' instead.", 48 + "[RTL] Avoid using 'text-left'. Use 'text-start' instead.", 49 + "[RTL] Avoid using 'text-right'. Use 'text-end' instead.", 50 + "[RTL] Avoid using 'border-l'. Use 'border-is' instead.", 51 + "[RTL] Avoid using 'border-r'. Use 'border-ie' instead.", 52 + "[RTL] Avoid using 'rounded-l'. Use 'rounded-is' instead.", 53 + "[RTL] Avoid using 'rounded-r'. Use 'rounded-ie' instead.", 54 + ] 55 + `) 56 + }) 57 + })
+131
uno-preset-rtl.ts
··· 1 + import type { CSSEntries, DynamicMatcher, Preset, RuleContext } from 'unocss' 2 + import { cornerMap, directionSize, h } from '@unocss/preset-wind4/utils' 3 + 4 + const directionMap: Record<string, string[]> = { 5 + 'l': ['-left'], 6 + 'r': ['-right'], 7 + 't': ['-top'], 8 + 'b': ['-bottom'], 9 + 's': ['-inline-start'], 10 + 'e': ['-inline-end'], 11 + 'x': ['-left', '-right'], 12 + 'y': ['-top', '-bottom'], 13 + '': [''], 14 + 'bs': ['-block-start'], 15 + 'be': ['-block-end'], 16 + 'is': ['-inline-start'], 17 + 'ie': ['-inline-end'], 18 + 'block': ['-block-start', '-block-end'], 19 + 'inline': ['-inline-start', '-inline-end'], 20 + } 21 + 22 + function directionSizeRTL( 23 + propertyPrefix: string, 24 + prefixMap?: { l: string; r: string }, 25 + ): DynamicMatcher { 26 + const matcher = directionSize(propertyPrefix) 27 + return (args, context) => { 28 + const [match, direction, size] = args 29 + const defaultMap = { l: 'is', r: 'ie' } 30 + const map = prefixMap || defaultMap 31 + const replacement = map[direction as 'l' | 'r'] 32 + // oxlint-disable-next-line no-console -- warn logging 33 + console.warn( 34 + `[RTL] Avoid using '${match}'. Use '${match.replace(direction === 'l' ? 'l' : 'r', replacement)}' instead.`, 35 + ) 36 + return matcher([match, replacement, size], context) 37 + } 38 + } 39 + 40 + function handlerRounded( 41 + [, a = '', s = 'DEFAULT']: string[], 42 + { theme }: RuleContext<any>, 43 + ): CSSEntries | undefined { 44 + if (a in cornerMap) { 45 + if (s === 'full') return cornerMap[a].map(i => [`border${i}-radius`, 'calc(infinity * 1px)']) 46 + 47 + const _v = theme.radius?.[s] ?? h.bracket.cssvar.global.fraction.rem(s) 48 + if (_v != null) { 49 + return cornerMap[a].map(i => [`border${i}-radius`, _v]) 50 + } 51 + } 52 + } 53 + 54 + function handlerBorderSize([, a = '', b = '1']: string[]): CSSEntries | undefined { 55 + const v = h.bracket.cssvar.global.px(b) 56 + if (a in directionMap && v != null) return directionMap[a].map(i => [`border${i}-width`, v]) 57 + } 58 + 59 + /** 60 + * CSS RTL support to detect, replace and warn wrong left/right usages. 61 + * @public 62 + */ 63 + export function presetRtl(): Preset { 64 + return { 65 + name: 'rtl-preset', 66 + rules: [ 67 + // RTL overrides 68 + // We need to move the dash out of the capturing group to avoid capturing it in the direction 69 + [ 70 + /^p([rl])-(.+)?$/, 71 + directionSizeRTL('padding', { l: 's', r: 'e' }), 72 + { autocomplete: '(m|p)<directions>-<num>' }, 73 + ], 74 + [ 75 + /^m([rl])-(.+)?$/, 76 + directionSizeRTL('margin', { l: 's', r: 'e' }), 77 + { autocomplete: '(m|p)<directions>-<num>' }, 78 + ], 79 + [ 80 + /^(?:position-|pos-)?(left|right)-(.+)$/, 81 + ([, direction, size], context) => { 82 + const replacement = direction === 'left' ? 'inset-is' : 'inset-ie' 83 + // oxlint-disable-next-line no-console -- warn logging 84 + console.warn( 85 + `[RTL] Avoid using '${direction}-${size}'. Use '${replacement}-${size}' instead.`, 86 + ) 87 + return directionSize('inset')(['', direction === 'left' ? 'is' : 'ie', size], context) 88 + }, 89 + { autocomplete: '(left|right)-<num>' }, 90 + ], 91 + [ 92 + /^text-(left|right)$/, 93 + ([, direction]) => { 94 + const replacement = direction === 'left' ? 'start' : 'end' 95 + // oxlint-disable-next-line no-console -- warn logging 96 + console.warn(`[RTL] Avoid using 'text-${direction}'. Use 'text-${replacement}' instead.`) 97 + return { 'text-align': replacement } 98 + }, 99 + { autocomplete: 'text-(left|right)' }, 100 + ], 101 + [ 102 + /^rounded-([rl])(?:-(.+))?$/, 103 + (args, context) => { 104 + const [_, direction, size] = args 105 + const replacementMap: Record<string, string> = { 106 + l: 'is', 107 + r: 'ie', 108 + } 109 + const replacement = replacementMap[direction] 110 + // oxlint-disable-next-line no-console -- warn logging 111 + console.warn( 112 + `[RTL] Avoid using 'rounded-${direction}'. Use 'rounded-${replacement}' instead.`, 113 + ) 114 + return handlerRounded(['', replacement, size], context) 115 + }, 116 + ], 117 + [ 118 + /^border-([rl])(?:-(.+))?$/, 119 + args => { 120 + const [_, direction, size] = args 121 + const replacement = direction === 'l' ? 'is' : 'ie' 122 + // oxlint-disable-next-line no-console -- warn logging 123 + console.warn( 124 + `[RTL] Avoid using 'border-${direction}'. Use 'border-${replacement}' instead.`, 125 + ) 126 + return handlerBorderSize(['', replacement, size || '1']) 127 + }, 128 + ], 129 + ], 130 + } 131 + }
+4 -1
uno.config.ts
··· 6 6 transformerVariantGroup, 7 7 } from 'unocss' 8 8 import type { Theme } from '@unocss/preset-wind4/theme' 9 + import { presetRtl } from './uno-preset-rtl' 9 10 10 11 const customIcons = { 11 12 tangled: ··· 23 24 custom: customIcons, 24 25 }, 25 26 }), 26 - ], 27 + // keep this preset last 28 + process.env.CI ? undefined : presetRtl(), 29 + ].filter(Boolean), 27 30 transformers: [transformerDirectives(), transformerVariantGroup()], 28 31 theme: { 29 32 font: {