├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── fixtures ├── tailwind-2.css └── tailwind-3.css ├── package-lock.json ├── package.json ├── src ├── index.ts └── properties.ts ├── tests ├── classname.test.ts ├── custom-reporter.ts ├── generated.test.ts ├── parse.test.ts ├── print-task-error.ts └── tailwind.config.js └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | cache: 'npm' 16 | - run: npm ci 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | fixtures -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ui-devtools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Utilities to parse and create tailwindcss classnames 5 |

6 | 7 |

8 | Extracted from the source code for UI Devtools and optimised for open source. 9 |

10 | 11 |   12 | 13 | ### Installation 14 | 15 | ``` 16 | yarn add @ui-devtools/tailwind-utils 17 | 18 | or 19 | 20 | npm install @ui-devtools/tailwind-utils 21 | ``` 22 | 23 |   24 | 25 | ### Usage 26 | 27 | [Open demo in codesandbox](https://codesandbox.io/s/tailwind-utils-m0lvu5?expanddevtools=1) 28 | 29 |
30 | 31 | Setup: 32 | 33 | ```ts 34 | import Utils from '@ui-devtools/tailwind-utils'; 35 | import config from './tailwind.config.js'; // your tailwind config file, optional 36 | 37 | const { parse, classname } = Utils(config); 38 | ``` 39 | 40 |
41 | 42 | classname → definition: 43 | 44 | ```ts 45 | const definition = parse('w-48'); 46 | // { property: 'width', value: '12rem' } 47 | 48 | const definition = parse('md:hover:bg-red-200/50'); 49 | /* 50 | { 51 | responsiveModifier: 'md', 52 | pseudoModifier: 'hover', 53 | property: 'backgroundColor' 54 | value: '#fecaca80' 55 | } 56 | */ 57 | ``` 58 |
59 | 60 | definition → classname: 61 | 62 | ```ts 63 | const { className } = classname({ property: 'margin', value: '-16rem' }); 64 | // -m-64 65 | 66 | const { className } = classname({ 67 | responsiveModifier: 'md', 68 | pseudoModifier: 'hover', 69 | property: 'backgroundColor', 70 | value: '#fecaca80' 71 | }); 72 | // md:hover:bg-red-200/50 73 | 74 | const { className, error } = classname({ 75 | responsiveModifier: 'small', 76 | property: 'textColor', 77 | value: '#fecaca80' 78 | }); 79 | /* 80 | { 81 | error: { 82 | responsiveModifier: 'Unidentified responsive modifier, expected one of [sm, md, lg, xl, 2xl], got small' 83 | } 84 | } 85 | */ 86 | ``` 87 | 88 |   89 | 90 | #### like it? 91 | 92 | :star: this repo 93 | 94 |   95 | 96 | #### license 97 | 98 | MIT © [siddharthkp](https://github.com/siddharthkp) 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui-devtools/tailwind-utils", 3 | "version": "0.0.2", 4 | "description": "Utilities to parse and create tailwindcss class names", 5 | "main": "dist/index.js", 6 | "author": "siddharthkp", 7 | "license": "MIT", 8 | "keywords": [ 9 | "tailwindcss", 10 | "parser", 11 | "utils" 12 | ], 13 | "scripts": { 14 | "build": "tsc", 15 | "test": "npm run test:unit && npm run test:build", 16 | "test:build": "npm run build -- --noEmit", 17 | "test:unit": "FORCE_COLOR=3 vitest parse classname --reporter=tests/custom-reporter", 18 | "test:generated": "vitest generated --no-isolate" 19 | }, 20 | "devDependencies": { 21 | "@babel/preset-env": "^7.19.4", 22 | "@babel/preset-typescript": "^7.18.6", 23 | "@vitest/coverage-c8": "^0.24.0", 24 | "jest": "^29.2.1", 25 | "lodash.camelcase": "^4.3.0", 26 | "lodash.uniqby": "^4.7.0", 27 | "tailwindcss": "^3.1.8", 28 | "ts-jest": "^29.0.3", 29 | "ts-node": "^10.9.1", 30 | "typescript": "^4.8.4", 31 | "vitest": "^0.24.0" 32 | }, 33 | "peerDependencies": { 34 | "tailwindcss": "*" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/ui-devtools/tailwind-utils.git" 39 | }, 40 | "homepage": "https://github.com/ui-devtools/tailwind-utils#readme", 41 | "bugs": { 42 | "url": "https://github.com/ui-devtools/tailwind-utils/issues" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import resolveConfig from 'tailwindcss/resolveConfig'; 2 | import type { Config } from 'tailwindcss/types/config'; 3 | import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette'; 4 | import { properties, namedClassProperties } from './properties'; 5 | 6 | const Tailwind = (config?: Config) => { 7 | // @ts-ignore resolveConfig doesn't like empty config but stubs it anyways 8 | const resolvedConfig = resolveConfig(config || {}); 9 | const theme = resolvedConfig.theme || {}; 10 | 11 | // add negative values to scales 12 | Object.keys(properties).forEach((property) => { 13 | const { scale, supportsNegativeValues } = properties[property]; 14 | if (supportsNegativeValues && theme[scale] && !theme[scale].negativeValuesAdded) { 15 | Object.keys(theme[scale]).forEach((key) => { 16 | theme[scale]['-' + key] = '-' + theme[scale][key]; 17 | }); 18 | theme[scale].negativeValuesAdded = true; 19 | } 20 | }); 21 | 22 | // TODO: check source code for this because the types are more flexible than object 23 | const responsiveModifiers = Object.keys(theme.screens || {}); 24 | const pseudoModifiers = resolvedConfig.variantOrder; 25 | 26 | const parse = (className: string = '') => { 27 | if (className.startsWith('.')) className = className.replace('.', ''); 28 | 29 | // format: prefix-value | responsive:prefix-value | pseudo:prefix-value | responsive:pseudo:prefix-value 30 | let responsiveModifier: string | null = null; 31 | let pseudoModifier: string | null = null; 32 | let propertyName: string; 33 | let propertyValue: string | null; 34 | let relatedProperties: { [key: string]: string } = {}; 35 | 36 | let classNameWithoutModifers: string = ''; 37 | 38 | const numberOfModifiers = className.split(':').length - 1; 39 | 40 | if (numberOfModifiers === 0) classNameWithoutModifers = className; 41 | else if (numberOfModifiers === 1) { 42 | const unknownModifier = className.split(':')[0]; 43 | classNameWithoutModifers = className.split(':')[1]; 44 | if (responsiveModifiers.includes(unknownModifier)) responsiveModifier = unknownModifier; 45 | else if (pseudoModifiers.includes(unknownModifier)) pseudoModifier = unknownModifier; 46 | else; // have no idea what this is, TODO: should this ignore or throw an error? 47 | } else if (numberOfModifiers === 2) { 48 | responsiveModifier = className.split(':')[0]; 49 | pseudoModifier = className.split(':')[1]; 50 | classNameWithoutModifers = className.split(':')[2]; 51 | } 52 | 53 | let isNegative = false; 54 | if (classNameWithoutModifers.startsWith('-')) { 55 | isNegative = true; 56 | classNameWithoutModifers = classNameWithoutModifers.replace('-', ''); 57 | } 58 | 59 | // check named classes first 60 | if (namedClassProperties[classNameWithoutModifers]) { 61 | const styles = namedClassProperties[classNameWithoutModifers]; 62 | if (Object.keys(styles).length > 1) { 63 | propertyName = 'composite'; 64 | propertyValue = null; 65 | relatedProperties = styles; 66 | } else { 67 | propertyName = Object.keys(styles)[0]; 68 | propertyValue = styles[propertyName]; 69 | } 70 | } else { 71 | const possiblePropertyNames = Object.keys(properties).filter((name) => { 72 | const property = properties[name]; 73 | if (classNameWithoutModifers === property.prefix) return true; // flex-grow-DEFAULT = flex-grow 74 | if (classNameWithoutModifers.startsWith(property.prefix + '-')) return true; 75 | return false; 76 | }); 77 | 78 | if (possiblePropertyNames.length === 0) { 79 | // no clue what this is then 80 | // TODO: improve error for unhandled properties 81 | propertyName = 'ERROR'; 82 | propertyValue = 'ERROR'; 83 | } else { 84 | // match value to find property 85 | const matchingPropertyName = possiblePropertyNames 86 | .sort((a, b) => properties[b].prefix.length - properties[a].prefix.length) 87 | .find((name) => { 88 | const property = properties[name]; 89 | 90 | // flatten color scales 91 | const scale = 92 | property.scale.includes('color') || property.scale.includes('Color') 93 | ? flattenColorPalette(theme[property.scale]) 94 | : theme[property.scale]; 95 | if (!scale) return false; // couldn't find scale for property, probably unhandled 96 | 97 | const scaleKey = 98 | property.scale === 'colors' 99 | ? // remove opacity modifier 100 | classNameWithoutModifers.split('/')[0].replace(property.prefix + '-', '') 101 | : classNameWithoutModifers.replace(property.prefix + '-', ''); 102 | 103 | if (scale.DEFAULT) scale[property.prefix] = scale.DEFAULT; 104 | 105 | const possibleValue = scale[scaleKey]; 106 | // this could be null if it's not the right property 107 | return Boolean(possibleValue); 108 | }); 109 | 110 | if (matchingPropertyName) { 111 | propertyName = matchingPropertyName; 112 | const property = properties[matchingPropertyName]; 113 | 114 | const scale = 115 | property.scale.includes('color') || property.scale.includes('Color') 116 | ? flattenColorPalette(theme[property.scale]) 117 | : theme[property.scale]; 118 | const scaleKey = 119 | property.scale === 'colors' 120 | ? // remove opacity modifier 121 | classNameWithoutModifers.split('/')[0].replace(property.prefix + '-', '') 122 | : classNameWithoutModifers.replace(property.prefix + '-', ''); 123 | const possibleValue = scale[scaleKey]; 124 | 125 | // fontSize is special 126 | if (propertyName === 'fontSize' && Array.isArray(possibleValue)) { 127 | propertyValue = possibleValue[0]; 128 | relatedProperties = possibleValue[1]; 129 | } else if (property.scale === 'colors') { 130 | const opacity = parseInt(classNameWithoutModifers.split('/')[1]); 131 | propertyValue = possibleValue + (opacity ? percentToHex(opacity) : ''); 132 | } else if (Array.isArray(possibleValue)) { 133 | // true for fontFamily and dropShadow 134 | propertyValue = possibleValue.join(', '); 135 | } else { 136 | propertyValue = possibleValue; 137 | } 138 | } else { 139 | // no clue what this is then 140 | propertyName = 'ERROR'; 141 | propertyValue = 'ERROR'; 142 | } 143 | } 144 | } 145 | 146 | return { 147 | className, 148 | responsiveModifier, 149 | pseudoModifier, 150 | property: propertyName, 151 | value: isNegative ? '-' + propertyValue : propertyValue, 152 | relatedProperties 153 | }; 154 | }; 155 | 156 | const classname = ({ 157 | responsiveModifier, 158 | pseudoModifier, 159 | property: propertyName, 160 | value: propertyValue 161 | }: { 162 | responsiveModifier?: string; 163 | pseudoModifier?: string; 164 | property: string; 165 | value: string; 166 | }) => { 167 | let className: string | undefined = ''; 168 | let error: { 169 | responsiveModifier?: string; 170 | pseudoModifier?: string; 171 | property?: string; 172 | value?: string; 173 | } = {}; 174 | 175 | if (responsiveModifier) { 176 | if (responsiveModifiers.includes(responsiveModifier)) className = responsiveModifier + ':'; 177 | else 178 | error['responsiveModifier'] = `Unidentified responsive modifier, expected one of [${responsiveModifiers.join( 179 | ', ' 180 | )}], got ${responsiveModifier}`; 181 | } 182 | 183 | if (pseudoModifier) { 184 | if (pseudoModifiers.includes(pseudoModifier)) className = className + pseudoModifier + ':'; 185 | else 186 | error['pseudoModifier'] = `Unidentified pseudo modifier, expected one of [${pseudoModifiers.join( 187 | ', ' 188 | )}], got ${pseudoModifier}`; 189 | } 190 | 191 | const matchingProperty = properties[propertyName]; 192 | 193 | if (matchingProperty) { 194 | // flatten color scales 195 | const scale = 196 | matchingProperty.scale.includes('color') || matchingProperty.scale.includes('Color') 197 | ? flattenColorPalette(theme[matchingProperty.scale]) 198 | : theme[matchingProperty.scale]; 199 | 200 | // find value on scale 201 | if (scale) { 202 | let scaleKey; 203 | 204 | if (propertyName === 'fontSize') { 205 | // format: sm: [ '0.875rem', { lineHeight: '1.25rem' } ], 206 | scaleKey = Object.keys(scale).find((key) => scale[key][0] === propertyValue); 207 | } else if (matchingProperty.scale === 'colors') { 208 | if (!propertyValue.startsWith('#')) { 209 | error['value'] = 'Only hex values are supported, example: #fecaca80'; 210 | } else if (![7, 9].includes(propertyValue.length)) { 211 | error['value'] = 212 | 'Shorthand hex values like #0008 are not supported, please pass the full value like #00000080'; 213 | } 214 | 215 | let opacity: number | null = null; 216 | 217 | // example: #fecaca80 218 | if (propertyValue.length === 9) { 219 | opacity = hexToPercent(propertyValue.slice(-2)); 220 | propertyValue = propertyValue.slice(0, -2); 221 | } 222 | 223 | // convert to lowercase for comparison 224 | propertyValue = propertyValue.toLowerCase(); 225 | 226 | scaleKey = Object.keys(scale).find((key) => { 227 | return scale[key] === propertyValue; 228 | }); 229 | 230 | if (scaleKey && opacity) scaleKey = scaleKey + '/' + opacity; 231 | } else { 232 | scaleKey = Object.keys(scale).find((key) => { 233 | // true for dropShadow and fontFamily 234 | if (Array.isArray(scale[key])) return scale[key].join(', ') === propertyValue; 235 | else return scale[key] === propertyValue; 236 | }); 237 | } 238 | 239 | // move - for negative value to prefix 240 | const isNegative = scaleKey?.startsWith('-'); 241 | 242 | if (isNegative) { 243 | className += '-'; 244 | scaleKey = scaleKey.replace('-', ''); 245 | } 246 | 247 | className += matchingProperty.prefix; 248 | 249 | if (scaleKey === 'DEFAULT') { 250 | /* we don't add default */ 251 | } else if (scaleKey) className += '-' + scaleKey; 252 | else if (!error.value) error['value'] = 'UNIDENTIFIED_VALUE'; 253 | } else { 254 | error['property'] = 'UNIDENTIFIED_PROPERTY'; 255 | } 256 | } else { 257 | const namedClassPropertyIndex = Object.values(namedClassProperties).findIndex((styles) => { 258 | if (Object.keys(styles).length > 1) return false; 259 | 260 | const name = Object.keys(styles)[0]; 261 | const value = styles[propertyName]; 262 | return name === propertyName && value === propertyValue; 263 | }); 264 | 265 | if (namedClassPropertyIndex !== -1) { 266 | className = className + Object.keys(namedClassProperties)[namedClassPropertyIndex]; 267 | } else if (propertyName === 'color') { 268 | error['property'] = 'UNIDENTIFIED_PROPERTY, did you mean textColor?'; 269 | } else { 270 | error['property'] = 'UNIDENTIFIED_PROPERTY'; 271 | } 272 | } 273 | 274 | if (Object.keys(error).length > 0) return { error }; 275 | else return { className }; 276 | }; 277 | 278 | return { parse, classname, meta: { responsiveModifiers, pseudoModifiers, resolvedConfig } }; 279 | }; 280 | 281 | export default Tailwind; 282 | 283 | const percentToHex = (percent: number) => { 284 | const intValue = Math.round((percent / 100) * 255); // map percent to nearest integer (0 - 255) 285 | const hexValue = intValue.toString(16); // get hexadecimal representation 286 | return hexValue.padStart(2, '0'); // format with leading 0 and upper case characters 287 | }; 288 | 289 | const hexToPercent = (hex: string) => { 290 | return Math.floor((100 * parseInt(hex, 16)) / 255); 291 | }; 292 | -------------------------------------------------------------------------------- /src/properties.ts: -------------------------------------------------------------------------------- 1 | // TODO: standardise camelcase 2 | // TODO: some of these values are not single properties like paddingX 3 | // TODO: some of these values are not simple classname selectors like divideX 4 | 5 | type Properties = { 6 | [key: string]: { 7 | prefix: string; 8 | scale: string; 9 | supportsNegativeValues?: boolean; 10 | }; 11 | }; 12 | 13 | export const properties: Properties = { 14 | // generated list: 15 | aspectRatio: { prefix: 'aspect', scale: 'aspectRatio' }, 16 | 17 | backgroundImage: { prefix: 'bg', scale: 'backgroundImage' }, 18 | backgroundOpacity: { prefix: 'bg-opacity', scale: 'backgroundOpacity' }, 19 | backgroundPosition: { prefix: 'bg', scale: 'backgroundPosition' }, 20 | backgroundSize: { prefix: 'bg', scale: 'backgroundSize' }, 21 | 22 | borderOpacity: { prefix: 'border-opacity', scale: 'borderOpacity' }, 23 | 24 | columns: { prefix: 'columns', scale: 'columns' }, 25 | cursor: { prefix: 'cursor', scale: 'cursor' }, 26 | flex: { prefix: 'flex', scale: 'flex' }, 27 | flexBasis: { prefix: 'basis', scale: 'flexBasis' }, 28 | fontWeight: { prefix: 'font', scale: 'fontWeight' }, 29 | gridAutoColumns: { prefix: 'auto-cols', scale: 'gridAutoColumns' }, 30 | gridAutoRows: { prefix: 'auto-rows', scale: 'gridAutoRows' }, 31 | gridColumn: { prefix: 'col', scale: 'gridColumn' }, 32 | gridColumnEnd: { prefix: 'col-end', scale: 'gridColumnEnd' }, 33 | gridColumnStart: { prefix: 'col-start', scale: 'gridColumnStart' }, 34 | gridRow: { prefix: 'row', scale: 'gridRow' }, 35 | gridRowEnd: { prefix: 'row-end', scale: 'gridRowEnd' }, 36 | gridRowStart: { prefix: 'row-start', scale: 'gridRowStart' }, 37 | gridTemplateColumns: { prefix: 'grid-cols', scale: 'gridTemplateColumns' }, 38 | gridTemplateRows: { prefix: 'grid-rows', scale: 'gridTemplateRows' }, 39 | height: { prefix: 'h', scale: 'height' }, 40 | letterSpacing: { 41 | prefix: 'tracking', 42 | scale: 'letterSpacing', 43 | supportsNegativeValues: false // set explicitly because it contains non-number values 44 | }, 45 | lineHeight: { prefix: 'leading', scale: 'lineHeight' }, 46 | listStyleType: { prefix: 'list', scale: 'listStyleType' }, 47 | maxHeight: { prefix: 'max-h', scale: 'maxHeight' }, 48 | maxWidth: { prefix: 'max-w', scale: 'maxWidth' }, 49 | minHeight: { prefix: 'min-h', scale: 'minHeight' }, 50 | minWidth: { prefix: 'min-w', scale: 'minWidth' }, 51 | objectPosition: { prefix: 'object', scale: 'objectPosition' }, 52 | opacity: { prefix: 'opacity', scale: 'opacity' }, 53 | order: { 54 | prefix: 'order', 55 | scale: 'order', 56 | supportsNegativeValues: false // set explicitly because it contains non-number values 57 | }, 58 | outlineOffset: { 59 | prefix: 'outline-offset', 60 | scale: 'outlineOffset', 61 | supportsNegativeValues: true 62 | }, 63 | outlineWidth: { prefix: 'outline', scale: 'outlineWidth' }, 64 | 65 | ringColor: { prefix: 'ring', scale: 'colors' }, 66 | ringOffsetColor: { prefix: 'ring-offset', scale: 'colors' }, 67 | ringOffsetWidth: { prefix: 'ring-offset', scale: 'ringOffsetWidth' }, 68 | ringOpacity: { prefix: 'ring-opacity', scale: 'ringOpacity' }, 69 | 70 | strokeWidth: { prefix: 'stroke', scale: 'strokeWidth' }, 71 | textDecorationThickness: { prefix: 'decoration', scale: 'textDecorationThickness' }, 72 | textIndent: { 73 | prefix: 'indent', 74 | scale: 'textIndent', 75 | supportsNegativeValues: true 76 | }, 77 | textOpacity: { prefix: 'text-opacity', scale: 'textOpacity' }, 78 | textUnderlineOffset: { prefix: 'underline-offset', scale: 'textUnderlineOffset' }, 79 | transformOrigin: { prefix: 'origin', scale: 'transformOrigin' }, 80 | transitionDelay: { prefix: 'delay', scale: 'transitionDelay' }, 81 | transitionDuration: { prefix: 'duration', scale: 'transitionDuration' }, 82 | transitionTimingFunction: { prefix: 'ease', scale: 'transitionTimingFunction' }, 83 | width: { prefix: 'w', scale: 'width' }, 84 | willChange: { prefix: 'will-change', scale: 'willChange' }, 85 | zIndex: { prefix: 'z', scale: 'zIndex', supportsNegativeValues: true }, 86 | 87 | // added manually: 88 | accentColor: { scale: 'accentColor', prefix: 'accent' }, 89 | alignSelf: { scale: 'alignSelf', prefix: 'self' }, 90 | animation: { prefix: 'animate', scale: 'animation' }, 91 | backgroundColor: { prefix: 'bg', scale: 'colors' }, 92 | 93 | borderColor: { prefix: 'border', scale: 'colors' }, 94 | borderStyle: { prefix: 'border', scale: 'borderStyle' }, 95 | 96 | borderWidth: { prefix: 'border', scale: 'borderWidth' }, 97 | borderTopWidth: { prefix: 'border-t', scale: 'borderWidth' }, 98 | borderRightWidth: { prefix: 'border-r', scale: 'borderWidth' }, 99 | borderBottomWidth: { prefix: 'border-b', scale: 'borderWidth' }, 100 | borderLeftWidth: { prefix: 'border-l', scale: 'borderWidth' }, 101 | 102 | borderRadius: { prefix: 'rounded', scale: 'borderRadius' }, 103 | borderRadiusTopLeft: { prefix: 'rounded-tl', scale: 'borderRadius' }, 104 | borderRadiusTopRight: { prefix: 'rounded-tr', scale: 'borderRadius' }, 105 | borderRadiusBottomRight: { prefix: 'rounded-br', scale: 'borderRadius' }, 106 | borderRadiusBottomLeft: { prefix: 'rounded-bl', scale: 'borderRadius' }, 107 | 108 | borderSpacing: { prefix: 'border-spacing', scale: 'borderSpacing' }, 109 | borderSpacingX: { prefix: 'border-spacing-x', scale: 'borderSpacing' }, 110 | borderSpacingY: { prefix: 'border-spacing-y', scale: 'borderSpacing' }, 111 | outline: { prefix: 'outline', scale: 'outline' }, 112 | outlineColor: { prefix: 'outline', scale: 'colors' }, 113 | 114 | boxShadow: { prefix: 'shadow', scale: 'boxShadow' }, 115 | boxShadowColor: { prefix: 'shadow', scale: 'colors' }, 116 | caretColor: { prefix: 'caret', scale: 'caretColor' }, 117 | 118 | divideColor: { prefix: 'divide', scale: 'colors' }, 119 | divideOpacity: { prefix: 'divide-opacity', scale: 'divideOpacity' }, 120 | divideX: { prefix: 'divide-x', scale: 'divideWidth' }, 121 | divideY: { prefix: 'divide-y', scale: 'divideWidth' }, 122 | dropShadow: { prefix: 'drop-shadow', scale: 'dropShadow' }, 123 | 124 | // svg 125 | fill: { prefix: 'fill', scale: 'colors' }, 126 | stroke: { prefix: 'stroke', scale: 'colors' }, 127 | 128 | flexGrow: { prefix: 'flex-grow', scale: 'flexGrow' }, 129 | flexShrink: { prefix: 'flex-shrink', scale: 'flexShrink' }, 130 | 131 | // filters, todo: the properties here are just wrong 132 | grayscale: { prefix: 'grayscale', scale: 'grayscale' }, 133 | hueRotate: { prefix: 'hueRotate', scale: 'hueRotate', supportsNegativeValues: true }, 134 | invert: { prefix: 'invert', scale: 'invert' }, 135 | saturate: { prefix: 'saturate', scale: 'saturate' }, 136 | sepia: { prefix: 'sepia', scale: 'sepia' }, 137 | contrast: { prefix: 'contrast', scale: 'contrast' }, 138 | brightness: { prefix: 'brightness', scale: 'brightness' }, 139 | blur: { prefix: 'blur', scale: 'blur' }, 140 | backdropBlur: { prefix: 'backdrop-blur', scale: 'backdropBlur' }, 141 | backdropBrightness: { prefix: 'backdrop-brightness', scale: 'backdropBrightness' }, 142 | backdropContrast: { prefix: 'backdrop-contrast', scale: 'backdropContrast' }, 143 | backdropGrayscale: { prefix: 'backdrop-grayscale', scale: 'backdropGrayscale' }, 144 | backdropHueRotate: { prefix: 'backdrop-hueRotate', scale: 'backdropHueRotate' }, 145 | backdropInvert: { prefix: 'backdrop-invert', scale: 'backdropInvert' }, 146 | backdropOpacity: { prefix: 'backdrop-opacity', scale: 'backdropOpacity' }, 147 | backdropSaturate: { prefix: 'backdrop-saturate', scale: 'backdropSaturate' }, 148 | backdropSepia: { prefix: 'backdrop-sepia', scale: 'backdropSepia' }, 149 | 150 | fontFamily: { prefix: 'font', scale: 'fontFamily' }, 151 | fontSize: { prefix: 'text', scale: 'fontSize' }, 152 | 153 | gap: { prefix: 'gap', scale: 'gap' }, 154 | rowGap: { prefix: 'gap-x', scale: 'gap' }, 155 | columnGap: { prefix: 'gap-y', scale: 'gap' }, 156 | 157 | // gradientColorStops 158 | gradientFrom: { prefix: 'from', scale: 'colors' }, 159 | gradientVia: { prefix: 'via', scale: 'colors' }, 160 | gradientTo: { prefix: 'to', scale: 'colors' }, 161 | 162 | inset: { prefix: 'inset', scale: 'inset', supportsNegativeValues: true }, 163 | insetX: { prefix: 'inset-x', scale: 'inset', supportsNegativeValues: true }, 164 | insetY: { prefix: 'inset-y', scale: 'inset', supportsNegativeValues: true }, 165 | top: { prefix: 'top', scale: 'inset', supportsNegativeValues: true }, 166 | right: { prefix: 'right', scale: 'inset', supportsNegativeValues: true }, 167 | bottom: { prefix: 'bottom', scale: 'inset', supportsNegativeValues: true }, 168 | left: { prefix: 'left', scale: 'inset', supportsNegativeValues: true }, 169 | 170 | margin: { prefix: 'm', scale: 'margin', supportsNegativeValues: true }, 171 | marginTop: { prefix: 'mt', scale: 'margin', supportsNegativeValues: true }, 172 | marginRight: { prefix: 'mr', scale: 'margin', supportsNegativeValues: true }, 173 | marginBottom: { prefix: 'mb', scale: 'margin', supportsNegativeValues: true }, 174 | marginLeft: { prefix: 'ml', scale: 'margin', supportsNegativeValues: true }, 175 | marginX: { prefix: 'mx', scale: 'margin', supportsNegativeValues: true }, 176 | marginY: { prefix: 'my', scale: 'margin', supportsNegativeValues: true }, 177 | 178 | padding: { prefix: 'p', scale: 'padding' }, 179 | paddingTop: { prefix: 'pt', scale: 'padding' }, 180 | paddingRight: { prefix: 'pr', scale: 'padding' }, 181 | paddingBottom: { prefix: 'pb', scale: 'padding' }, 182 | paddingLeft: { prefix: 'pl', scale: 'padding' }, 183 | paddingX: { prefix: 'px', scale: 'padding' }, 184 | paddingY: { prefix: 'py', scale: 'padding' }, 185 | 186 | placeholderColor: { prefix: 'placeholder', scale: 'colors' }, 187 | placeholderOpacity: { prefix: 'placeholder-opacity', scale: 'placeholderOpacity' }, 188 | 189 | ringWidth: { prefix: 'ring', scale: 'ringWidth' }, 190 | rotate: { prefix: 'rotate', scale: 'rotate' }, 191 | scale: { prefix: 'scale', scale: 'scale' }, 192 | 193 | scrollMargin: { prefix: 'scroll-m', scale: 'scrollMargin' }, 194 | scrollMarginTop: { prefix: 'scroll-mt', scale: 'scrollMargin' }, 195 | scrollMarginRight: { prefix: 'scroll-mr', scale: 'scrollMargin' }, 196 | scrollMarginBottom: { prefix: 'scroll-mb', scale: 'scrollMargin' }, 197 | scrollMarginLeft: { prefix: 'scroll-ml', scale: 'scrollMargin' }, 198 | scrollMarginX: { prefix: 'scroll-mx', scale: 'scrollMargin' }, 199 | scrollMarginY: { prefix: 'scroll-my', scale: 'scrollMargin' }, 200 | 201 | scrollPadding: { prefix: 'scroll-p', scale: 'scrollPadding' }, 202 | scrollPaddingTop: { prefix: 'scroll-pt', scale: 'scrollPadding' }, 203 | scrollPaddingRight: { prefix: 'scroll-pr', scale: 'scrollPadding' }, 204 | scrollPaddingBottom: { prefix: 'scroll-pb', scale: 'scrollPadding' }, 205 | scrollPaddingLeft: { prefix: 'scroll-pl', scale: 'scrollPadding' }, 206 | scrollPaddingX: { prefix: 'scroll-px', scale: 'scrollPadding' }, 207 | scrollPaddingY: { prefix: 'scroll-py', scale: 'scrollPadding' }, 208 | 209 | skew: { prefix: 'skew', scale: 'skew' }, 210 | spaceX: { prefix: 'space-x', scale: 'space' }, 211 | spaceY: { prefix: 'space-y', scale: 'space' }, 212 | 213 | textColor: { prefix: 'text', scale: 'colors' }, 214 | textDecorationColor: { prefix: 'decoration', scale: 'textDecorationColor' }, 215 | 216 | transitionProperty: { prefix: 'transition', scale: 'transitionProperty' }, 217 | translate: { prefix: 'translate', scale: 'translate', supportsNegativeValues: true }, 218 | translateX: { prefix: 'translate-x', scale: 'translate', supportsNegativeValues: true }, 219 | translateY: { prefix: 'translate-y', scale: 'translate', supportsNegativeValues: true } 220 | }; 221 | 222 | // .sort((a, b) => b.prefix.length - a.prefix.length); 223 | // TODO: we sort by lengh of prefix so that text-opacity- is matched before text- 224 | 225 | export const namedClassProperties = { 226 | absolute: { position: 'absolute' }, 227 | 'align-baseline': { 'vertical-align': 'baseline' }, 228 | 'align-bottom': { 'vertical-align': 'bottom' }, 229 | 'align-middle': { 'vertical-align': 'middle' }, 230 | 'align-sub': { 'vertical-align': 'sub' }, 231 | 'align-super': { 'vertical-align': 'super' }, 232 | 'align-text-bottom': { 'vertical-align': 'text-bottom' }, 233 | 'align-text-top': { 'vertical-align': 'text-top' }, 234 | 'align-top': { 'vertical-align': 'top' }, 235 | antialiased: { 236 | '-webkit-font-smoothing': 'antialiased', 237 | '-moz-osx-font-smoothing': 'grayscale' 238 | }, 239 | 'appearance-none': { appearance: 'none' }, 240 | 'backdrop-filter-none': { 'backdrop-filter': 'none' }, 241 | 'bg-blend-color': { 'background-blend-mode': 'color' }, 242 | 'bg-blend-color-burn': { 'background-blend-mode': 'color-burn' }, 243 | 'bg-blend-color-dodge': { 'background-blend-mode': 'color-dodge' }, 244 | 'bg-blend-darken': { 'background-blend-mode': 'darken' }, 245 | 'bg-blend-difference': { 'background-blend-mode': 'difference' }, 246 | 'bg-blend-exclusion': { 'background-blend-mode': 'exclusion' }, 247 | 'bg-blend-hard-light': { 'background-blend-mode': 'hard-light' }, 248 | 'bg-blend-hue': { 'background-blend-mode': 'hue' }, 249 | 'bg-blend-lighten': { 'background-blend-mode': 'lighten' }, 250 | 'bg-blend-luminosity': { 'background-blend-mode': 'luminosity' }, 251 | 'bg-blend-multiply': { 'background-blend-mode': 'multiply' }, 252 | 'bg-blend-normal': { 'background-blend-mode': 'normal' }, 253 | 'bg-blend-overlay': { 'background-blend-mode': 'overlay' }, 254 | 'bg-blend-saturation': { 'background-blend-mode': 'saturation' }, 255 | 'bg-blend-screen': { 'background-blend-mode': 'screen' }, 256 | 'bg-blend-soft-light': { 'background-blend-mode': 'soft-light' }, 257 | 'bg-clip-border': { 'background-clip': 'border-box' }, 258 | 'bg-clip-content': { 'background-clip': 'content-box' }, 259 | 'bg-clip-padding': { 'background-clip': 'padding-box' }, 260 | 'bg-clip-text': { 'background-clip': 'text' }, 261 | 'bg-fixed': { 'background-attachment': 'fixed' }, 262 | 'bg-local': { 'background-attachment': 'local' }, 263 | 'bg-no-repeat': { 'background-repeat': 'no-repeat' }, 264 | 'bg-origin-border': { 'background-origin': 'border-box' }, 265 | 'bg-origin-content': { 'background-origin': 'content-box' }, 266 | 'bg-origin-padding': { 'background-origin': 'padding-box' }, 267 | 'bg-repeat': { 'background-repeat': 'repeat' }, 268 | 'bg-repeat-round': { 'background-repeat': 'round' }, 269 | 'bg-repeat-space': { 'background-repeat': 'space' }, 270 | 'bg-repeat-x': { 'background-repeat': 'repeat-x' }, 271 | 'bg-repeat-y': { 'background-repeat': 'repeat-y' }, 272 | 'bg-scroll': { 'background-attachment': 'scroll' }, 273 | block: { display: 'block' }, 274 | 'border-collapse': { 'border-collapse': 'collapse' }, 275 | 'border-dashed': { 'border-style': 'dashed' }, 276 | 'border-dotted': { 'border-style': 'dotted' }, 277 | 'border-double': { 'border-style': 'double' }, 278 | 'border-hidden': { 'border-style': 'hidden' }, 279 | 'border-none': { 'border-style': 'none' }, 280 | 'border-separate': { 'border-collapse': 'separate' }, 281 | 'border-solid': { 'border-style': 'solid' }, 282 | 'box-border': { 'box-sizing': 'border-box' }, 283 | 'box-content': { 'box-sizing': 'content-box' }, 284 | 'box-decoration-clone': { 'box-decoration-break': 'clone' }, 285 | 'box-decoration-slice': { 'box-decoration-break': 'slice' }, 286 | 'break-after-all': { 'break-after': 'all' }, 287 | 'break-after-auto': { 'break-after': 'auto' }, 288 | 'break-after-avoid': { 'break-after': 'avoid' }, 289 | 'break-after-avoid-page': { 'break-after': 'avoid-page' }, 290 | 'break-after-column': { 'break-after': 'column' }, 291 | 'break-after-left': { 'break-after': 'left' }, 292 | 'break-after-page': { 'break-after': 'page' }, 293 | 'break-after-right': { 'break-after': 'right' }, 294 | 'break-all': { 'word-break': 'break-all' }, 295 | 'break-before-all': { 'break-before': 'all' }, 296 | 'break-before-auto': { 'break-before': 'auto' }, 297 | 'break-before-avoid': { 'break-before': 'avoid' }, 298 | 'break-before-avoid-page': { 'break-before': 'avoid-page' }, 299 | 'break-before-column': { 'break-before': 'column' }, 300 | 'break-before-left': { 'break-before': 'left' }, 301 | 'break-before-page': { 'break-before': 'page' }, 302 | 'break-before-right': { 'break-before': 'right' }, 303 | 'break-inside-auto': { 'break-inside': 'auto' }, 304 | 'break-inside-avoid': { 'break-inside': 'avoid' }, 305 | 'break-inside-avoid-column': { 'break-inside': 'avoid-column' }, 306 | 'break-inside-avoid-page': { 'break-inside': 'avoid-page' }, 307 | 'break-keep': { 'word-break': 'keep-all' }, 308 | 'break-normal': { 'overflow-wrap': 'normal', 'word-break': 'normal' }, 309 | 'break-words': { 'overflow-wrap': 'break-word' }, 310 | capitalize: { 'text-transform': 'capitalize' }, 311 | 'clear-both': { clear: 'both' }, 312 | 'clear-left': { clear: 'left' }, 313 | 'clear-none': { clear: 'none' }, 314 | 'clear-right': { clear: 'right' }, 315 | collapse: { visibility: 'collapse' }, 316 | 'content-around': { 'align-content': 'space-around' }, 317 | 'content-baseline': { 'align-content': 'baseline' }, 318 | 'content-between': { 'align-content': 'space-between' }, 319 | 'content-center': { 'align-content': 'center' }, 320 | 'content-end': { 'align-content': 'flex-end' }, 321 | 'content-evenly': { 'align-content': 'space-evenly' }, 322 | 'content-start': { 'align-content': 'flex-start' }, 323 | contents: { display: 'contents' }, 324 | 'content-none': { content: 'none' }, 325 | 'decoration-clone': { 'box-decoration-break': 'clone' }, 326 | 'decoration-slice': { 'box-decoration-break': 'slice' }, 327 | 'decoration-dashed': { 'text-decoration-style': 'dashed' }, 328 | 'decoration-dotted': { 'text-decoration-style': 'dotted' }, 329 | 'decoration-double': { 'text-decoration-style': 'double' }, 330 | 'decoration-solid': { 'text-decoration-style': 'solid' }, 331 | 'decoration-wavy': { 'text-decoration-style': 'wavy' }, 332 | 'diagonal-fractions': { 'font-variant-numeric': 'diagonal-fractions' }, 333 | 334 | 'divide-solid': { 'border-style': 'solid' }, 335 | 'divide-dashed': { 'border-style': 'dashed' }, 336 | 'divide-dotted': { 'border-style': 'dotted' }, 337 | 'divide-double': { 'border-style': 'double' }, 338 | 'divide-none': { 'border-style': 'none' }, 339 | 340 | 'filter-none': { filter: 'none' }, 341 | fixed: { position: 'fixed' }, 342 | flex: { display: 'flex' }, 343 | 'flex-col': { 'flex-direction': 'column' }, 344 | 'flex-col-reverse': { 'flex-direction': 'column-reverse' }, 345 | 'flex-nowrap': { 'flex-wrap': 'nowrap' }, 346 | 'flex-row': { 'flex-direction': 'row' }, 347 | 'flex-row-reverse': { 'flex-direction': 'row-reverse' }, 348 | 'flex-wrap': { 'flex-wrap': 'wrap' }, 349 | 'flex-wrap-reverse': { 'flex-wrap': 'wrap-reverse' }, 350 | 'float-left': { float: 'left' }, 351 | 'float-none': { float: 'none' }, 352 | 'float-right': { float: 'right' }, 353 | 'flow-root': { display: 'flow-root' }, 354 | grid: { display: 'grid' }, 355 | 'grid-flow-col': { gridAutoFlow: 'column' }, 356 | 'grid-flow-col-dense': { gridAutoFlow: 'column dense' }, 357 | 'grid-flow-dense': { gridAutoFlow: 'dense' }, 358 | 'grid-flow-row': { gridAutoFlow: 'row' }, 359 | 'grid-flow-row-dense': { gridAutoFlow: 'row dense' }, 360 | hidden: { display: 'none' }, 361 | inline: { display: 'inline' }, 362 | 'inline-block': { display: 'inline-block' }, 363 | 'inline-flex': { display: 'inline-flex' }, 364 | 'inline-grid': { display: 'inline-grid' }, 365 | 'inline-table': { display: 'inline-table' }, 366 | invisible: { visibility: 'hidden' }, 367 | isolate: { isolation: 'isolate' }, 368 | 'isolation-auto': { isolation: 'auto' }, 369 | italic: { 'font-style': 'italic' }, 370 | 'items-baseline': { 'align-items': 'baseline' }, 371 | 'items-center': { 'align-items': 'center' }, 372 | 'items-end': { 'align-items': 'flex-end' }, 373 | 'items-start': { 'align-items': 'flex-start' }, 374 | 'items-stretch': { 'align-items': 'stretch' }, 375 | 'justify-around': { 'justify-content': 'space-around' }, 376 | 'justify-between': { 'justify-content': 'space-between' }, 377 | 'justify-center': { 'justify-content': 'center' }, 378 | 'justify-end': { 'justify-content': 'flex-end' }, 379 | 'justify-evenly': { 'justify-content': 'space-evenly' }, 380 | 'justify-items-center': { 'justify-items': 'center' }, 381 | 'justify-items-end': { 'justify-items': 'end' }, 382 | 'justify-items-start': { 'justify-items': 'start' }, 383 | 'justify-items-stretch': { 'justify-items': 'stretch' }, 384 | 'justify-self-auto': { 'justify-self': 'auto' }, 385 | 'justify-self-center': { 'justify-self': 'center' }, 386 | 'justify-self-end': { 'justify-self': 'end' }, 387 | 'justify-self-start': { 'justify-self': 'start' }, 388 | 'justify-self-stretch': { 'justify-self': 'stretch' }, 389 | 'justify-start': { 'justify-content': 'flex-start' }, 390 | 'line-through': { 'text-decoration-line': 'line-through' }, 391 | 'lining-nums': { 'font-variant-numeric': 'lining-nums' }, 392 | 'list-inside': { 'list-style-position': 'inside' }, 393 | 'list-item': { display: 'list-item' }, 394 | 'list-outside': { 'list-style-position': 'outside' }, 395 | lowercase: { 'text-transform': 'lowercase' }, 396 | 'mix-blend-color': { 'mix-blend-mode': 'color' }, 397 | 'mix-blend-color-burn': { 'mix-blend-mode': 'color-burn' }, 398 | 'mix-blend-color-dodge': { 'mix-blend-mode': 'color-dodge' }, 399 | 'mix-blend-darken': { 'mix-blend-mode': 'darken' }, 400 | 'mix-blend-difference': { 'mix-blend-mode': 'difference' }, 401 | 'mix-blend-exclusion': { 'mix-blend-mode': 'exclusion' }, 402 | 'mix-blend-hard-light': { 'mix-blend-mode': 'hard-light' }, 403 | 'mix-blend-hue': { 'mix-blend-mode': 'hue' }, 404 | 'mix-blend-lighten': { 'mix-blend-mode': 'lighten' }, 405 | 'mix-blend-luminosity': { 'mix-blend-mode': 'luminosity' }, 406 | 'mix-blend-multiply': { 'mix-blend-mode': 'multiply' }, 407 | 'mix-blend-normal': { 'mix-blend-mode': 'normal' }, 408 | 'mix-blend-overlay': { 'mix-blend-mode': 'overlay' }, 409 | 'mix-blend-plus-lighter': { 'mix-blend-mode': 'plus-lighter' }, 410 | 'mix-blend-saturation': { 'mix-blend-mode': 'saturation' }, 411 | 'mix-blend-screen': { 'mix-blend-mode': 'screen' }, 412 | 'mix-blend-soft-light': { 'mix-blend-mode': 'soft-light' }, 413 | 'no-underline': { 'text-decoration-line': 'none' }, 414 | 'normal-case': { 'text-transform': 'none' }, 415 | 'normal-nums': { 'font-variant-numeric': 'normal' }, 416 | 'not-italic': { 'font-style': 'normal' }, 417 | 'not-sr-only': { 418 | position: 'static', 419 | width: 'auto', 420 | height: 'auto', 421 | padding: '0', 422 | margin: '0', 423 | overflow: 'visible', 424 | clip: 'auto', 425 | whiteSpace: 'normal' 426 | }, 427 | 'object-contain': { 'object-fit': 'contain' }, 428 | 'object-cover': { 'object-fit': 'cover' }, 429 | 'object-fill': { 'object-fit': 'fill' }, 430 | 'object-none': { 'object-fit': 'none' }, 431 | 'object-scale-down': { 'object-fit': 'scale-down' }, 432 | 'oldstyle-nums': { 'font-variant-numeric': 'oldstyle-nums' }, 433 | ordinal: { 'font-variant-numeric': 'ordinal' }, 434 | outline: { 'outline-style': 'solid' }, 435 | 'outline-dashed': { 'outline-style': 'dashed' }, 436 | 'outline-dotted': { 'outline-style': 'dotted' }, 437 | 'outline-double': { 'outline-style': 'double' }, 438 | 'outline-none': { outline: '2px solid transparent', 'outline-offset': '2px' }, 439 | 'overflow-auto': { overflow: 'auto' }, 440 | 'overflow-clip': { overflow: 'clip' }, 441 | 'overflow-ellipsis': { 'text-overflow': 'ellipsis' }, 442 | 'overflow-hidden': { overflow: 'hidden' }, 443 | 'overflow-scroll': { overflow: 'scroll' }, 444 | 'overflow-visible': { overflow: 'visible' }, 445 | 'overflow-x-auto': { 'overflow-x': 'auto' }, 446 | 'overflow-x-clip': { 'overflow-x': 'clip' }, 447 | 'overflow-x-hidden': { 'overflow-x': 'hidden' }, 448 | 'overflow-x-scroll': { 'overflow-x': 'scroll' }, 449 | 'overflow-x-visible': { 'overflow-x': 'visible' }, 450 | 'overflow-y-auto': { 'overflow-y': 'auto' }, 451 | 'overflow-y-clip': { 'overflow-y': 'clip' }, 452 | 'overflow-y-hidden': { 'overflow-y': 'hidden' }, 453 | 'overflow-y-scroll': { 'overflow-y': 'scroll' }, 454 | 'overflow-y-visible': { 'overflow-y': 'visible' }, 455 | overline: { 'text-decoration-line': 'overline' }, 456 | 'overscroll-auto': { 'overscroll-behavior': 'auto' }, 457 | 'overscroll-contain': { 'overscroll-behavior': 'contain' }, 458 | 'overscroll-none': { 'overscroll-behavior': 'none' }, 459 | 'overscroll-x-auto': { 'overscroll-behavior-x': 'auto' }, 460 | 'overscroll-x-contain': { 'overscroll-behavior-x': 'contain' }, 461 | 'overscroll-x-none': { 'overscroll-behavior-x': 'none' }, 462 | 'overscroll-y-auto': { 'overscroll-behavior-y': 'auto' }, 463 | 'overscroll-y-contain': { 'overscroll-behavior-y': 'contain' }, 464 | 'overscroll-y-none': { 'overscroll-behavior-y': 'none' }, 465 | 'place-content-around': { 'place-content': 'space-around' }, 466 | 'place-content-baseline': { 'place-content': 'baseline' }, 467 | 'place-content-between': { 'place-content': 'space-between' }, 468 | 'place-content-center': { 'place-content': 'center' }, 469 | 'place-content-end': { 'place-content': 'end' }, 470 | 'place-content-evenly': { 'place-content': 'space-evenly' }, 471 | 'place-content-start': { 'place-content': 'start' }, 472 | 'place-content-stretch': { 'place-content': 'stretch' }, 473 | 'place-items-baseline': { 'place-items': 'baseline' }, 474 | 'place-items-center': { 'place-items': 'center' }, 475 | 'place-items-end': { 'place-items': 'end' }, 476 | 'place-items-start': { 'place-items': 'start' }, 477 | 'place-items-stretch': { 'place-items': 'stretch' }, 478 | 'place-self-auto': { 'place-self': 'auto' }, 479 | 'place-self-center': { 'place-self': 'center' }, 480 | 'place-self-end': { 'place-self': 'end' }, 481 | 'place-self-start': { 'place-self': 'start' }, 482 | 'place-self-stretch': { 'place-self': 'stretch' }, 483 | 'pointer-events-auto': { 'pointer-events': 'auto' }, 484 | 'pointer-events-none': { 'pointer-events': 'none' }, 485 | 'proportional-nums': { 'font-variant-numeric': 'proportional-nums' }, 486 | relative: { position: 'relative' }, 487 | resize: { resize: 'both' }, 488 | 'resize-none': { resize: 'none' }, 489 | 'resize-x': { resize: 'horizontal' }, 490 | 'resize-y': { resize: 'vertical' }, 491 | 'ring-inset': { '--tw-ring-inset': 'inset' }, 492 | 'scroll-auto': { 'scroll-behavior': 'auto' }, 493 | 'scroll-smooth': { 'scroll-behavior': 'smooth' }, 494 | 'select-all': { 'user-select': 'all' }, 495 | 'select-auto': { 'user-select': 'auto' }, 496 | 'select-none': { 'user-select': 'none' }, 497 | 'select-text': { 'user-select': 'text' }, 498 | 'self-auto': { 'align-self': 'auto' }, 499 | 'self-baseline': { 'align-self': 'baseline' }, 500 | 'self-center': { 'align-self': 'center' }, 501 | 'self-end': { 'align-self': 'flex-end' }, 502 | 'self-start': { 'align-self': 'flex-start' }, 503 | 'self-stretch': { 'align-self': 'stretch' }, 504 | 'slashed-zero': { 'font-variant-numeric': 'slashed-zero' }, 505 | 506 | 'snap-align-none': { 'scroll-snap-align': 'none' }, 507 | 'snap-always': { 'scroll-snap-stop': 'always' }, 508 | 'snap-center': { 'scroll-snap-align': 'center' }, 509 | 'snap-end': { 'scroll-snap-align': 'end' }, 510 | 'snap-normal': { 'scroll-snap-stop': 'normal' }, 511 | 'snap-start': { 'scroll-snap-align': 'start' }, 512 | 513 | '.snap-mandatory': { '--tw-scroll-snap-strictness': 'mandatory' }, 514 | '.snap-proximity': { '--tw-scroll-snap-strictness': 'proximity' }, 515 | 516 | '.snap-none': { 'scroll-snap-type': 'none' }, 517 | '.snap-x': { 'scroll-snap-type': 'x var(--tw-scroll-snap-strictness)' }, 518 | '.snap-y': { 'scroll-snap-type': 'y var(--tw-scroll-snap-strictness)' }, 519 | '.snap-both': { 'scroll-snap-type': 'both var(--tw-scroll-snap-strictness)' }, 520 | 521 | 'sr-only': { 522 | position: 'absolute', 523 | width: '1px', 524 | height: '1px', 525 | padding: '0', 526 | margin: '-1px', 527 | overflow: 'hidden', 528 | clip: 'rect(0, 0, 0, 0)', 529 | whiteSpace: 'nowrap', 530 | borderWidth: '0' 531 | }, 532 | 'stacked-fractions': { 'font-variant-numeric': 'stacked-fractions' }, 533 | static: { position: 'static' }, 534 | sticky: { position: 'sticky' }, 535 | 'subpixel-antialiased': { 536 | '-webkit-font-smoothing': 'auto', 537 | '-moz-osx-font-smoothing': 'auto' 538 | }, 539 | table: { display: 'table' }, 540 | 'table-auto': { 'table-layout': 'auto' }, 541 | 'table-caption': { display: 'table-caption' }, 542 | 'table-cell': { display: 'table-cell' }, 543 | 'table-column': { display: 'table-column' }, 544 | 'table-column-group': { display: 'table-column-group' }, 545 | 'table-fixed': { 'table-layout': 'fixed' }, 546 | 'table-footer-group': { display: 'table-footer-group' }, 547 | 'table-header-group': { display: 'table-header-group' }, 548 | 'table-row': { display: 'table-row' }, 549 | 'table-row-group': { display: 'table-row-group' }, 550 | 'tabular-nums': { 'font-variant-numeric': 'tabular-nums' }, 551 | 'text-center': { 'text-align': 'center' }, 552 | 'text-clip': { 'text-overflow': 'clip' }, 553 | 'text-ellipsis': { 'text-overflow': 'ellipsis' }, 554 | 'text-end': { 'text-align': 'end' }, 555 | 'text-justify': { 'text-align': 'justify' }, 556 | 'text-left': { 'text-align': 'left' }, 557 | 'text-right': { 'text-align': 'right' }, 558 | 'text-start': { 'text-align': 'start' }, 559 | 560 | 'touch-auto': { 'touch-action': 'auto' }, 561 | 'touch-none': { 'touch-action': 'none' }, 562 | 'touch-pan-x': { 'touch-action': 'pan-x' }, 563 | 'touch-pan-left': { 'touch-action': 'pan-left' }, 564 | 'touch-pan-right': { 'touch-action': 'pan-right' }, 565 | 'touch-pan-y': { 'touch-action': 'pan-y' }, 566 | 'touch-pan-up': { 'touch-action': 'pan-up' }, 567 | 'touch-pan-down': { 'touch-action': 'pan-down' }, 568 | 'touch-pinch-zoom': { 'touch-action': 'pinch-zoom' }, 569 | 'touch-manipulation': { 'touch-action': 'manipulation' }, 570 | 571 | truncate: { 572 | overflow: 'hidden', 573 | 'text-overflow': 'ellipsis', 574 | 'white-space': 'nowrap' 575 | }, 576 | underline: { 'text-decoration-line': 'underline' }, 577 | uppercase: { 'text-transform': 'uppercase' }, 578 | visible: { visibility: 'visible' }, 579 | 'whitespace-normal': { 'white-space': 'normal' }, 580 | 'whitespace-nowrap': { 'white-space': 'nowrap' }, 581 | 'whitespace-pre': { 'white-space': 'pre' }, 582 | 'whitespace-pre-line': { 'white-space': 'pre-line' }, 583 | 'whitespace-pre-wrap': { 'white-space': 'pre-wrap' } 584 | }; 585 | -------------------------------------------------------------------------------- /tests/classname.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, assert } from 'vitest'; 2 | 3 | import Tailwind from '../src/index'; 4 | const config = require('./tailwind.config'); 5 | const { classname } = Tailwind(config); 6 | 7 | describe('classname', () => { 8 | test('m-4', async () => { 9 | assert.deepEqual( 10 | { className: 'm-4' }, 11 | classname({ 12 | property: 'margin', 13 | value: '1rem' 14 | }) 15 | ); 16 | }); 17 | 18 | test('md:w-48', () => { 19 | assert.deepEqual( 20 | { className: 'md:w-48' }, 21 | classname({ 22 | responsiveModifier: 'md', 23 | property: 'width', 24 | value: '12rem' 25 | }) 26 | ); 27 | }); 28 | 29 | test('text-sm', () => { 30 | assert.deepEqual( 31 | { className: 'text-sm' }, 32 | classname({ 33 | property: 'fontSize', 34 | value: '0.875rem' 35 | }) 36 | ); 37 | }); 38 | 39 | test('md:hover:text-blue-600', () => { 40 | assert.deepEqual( 41 | { className: 'md:hover:text-blue-600' }, 42 | classname({ 43 | responsiveModifier: 'md', 44 | pseudoModifier: 'hover', 45 | property: 'textColor', 46 | value: '#2563eb' 47 | }) 48 | ); 49 | }); 50 | 51 | test('color instead of textColor', () => { 52 | assert.deepEqual( 53 | { error: { property: 'UNIDENTIFIED_PROPERTY, did you mean textColor?' } }, 54 | classname({ 55 | responsiveModifier: 'md', 56 | pseudoModifier: 'hover', 57 | property: 'color', 58 | value: '#2563eb' 59 | }) 60 | ); 61 | }); 62 | 63 | test('hover:bg-green-100', () => { 64 | assert.deepEqual( 65 | { className: 'hover:bg-green-100' }, 66 | classname({ 67 | pseudoModifier: 'hover', 68 | property: 'backgroundColor', 69 | value: '#dcfce7' 70 | }) 71 | ); 72 | }); 73 | 74 | test('absolute', () => { 75 | assert.deepEqual( 76 | { className: 'absolute' }, 77 | classname({ 78 | property: 'position', 79 | value: 'absolute' 80 | }) 81 | ); 82 | }); 83 | 84 | test('font-serif', () => { 85 | assert.deepEqual( 86 | { className: 'font-serif' }, 87 | classname({ 88 | property: 'fontFamily', 89 | value: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif' 90 | }) 91 | ); 92 | }); 93 | 94 | test('drop-shadow-md', () => { 95 | assert.deepEqual( 96 | { className: 'drop-shadow-md' }, 97 | classname({ 98 | property: 'dropShadow', 99 | value: '0 4px 3px rgb(0 0 0 / 0.07), 0 2px 2px rgb(0 0 0 / 0.06)' 100 | }) 101 | ); 102 | }); 103 | 104 | test('-m-64', () => { 105 | assert.deepEqual( 106 | { className: '-m-64' }, 107 | classname({ 108 | property: 'margin', 109 | value: '-16rem' 110 | }) 111 | ); 112 | }); 113 | 114 | test('block', () => { 115 | assert.deepEqual( 116 | { className: 'block' }, 117 | classname({ 118 | property: 'display', 119 | value: 'block' 120 | }) 121 | ); 122 | }); 123 | 124 | test('tracking-tighter', () => { 125 | assert.deepEqual( 126 | { className: 'tracking-tighter' }, 127 | classname({ 128 | property: 'letterSpacing', 129 | value: '-0.05em' 130 | }) 131 | ); 132 | }); 133 | 134 | test.todo('composite class', () => { 135 | assert.deepEqual( 136 | { className: 'sr-only' }, 137 | classname({ 138 | property: 'composite', 139 | value: null, 140 | relatedProperties: { 141 | position: 'static', 142 | width: 'auto', 143 | height: 'auto', 144 | padding: '0', 145 | margin: '0', 146 | overflow: 'visible', 147 | clip: 'auto', 148 | whiteSpace: 'normal' 149 | } 150 | }) 151 | ); 152 | }); 153 | 154 | test('bg-red-200/50', () => { 155 | assert.deepEqual( 156 | { className: 'bg-red-200/50' }, 157 | classname({ 158 | property: 'backgroundColor', 159 | value: '#fecaca80' 160 | }) 161 | ); 162 | }); 163 | 164 | test('bg-red-200/50 uppercase', () => { 165 | assert.deepEqual( 166 | { className: 'bg-red-200/50' }, 167 | classname({ 168 | property: 'backgroundColor', 169 | value: '#FECACA80' 170 | }) 171 | ); 172 | }); 173 | 174 | test('unsupported color format', () => { 175 | assert.deepEqual( 176 | { error: { value: 'Only hex values are supported, example: #fecaca80' } }, 177 | classname({ 178 | property: 'backgroundColor', 179 | value: 'rgb(255,255,255)' 180 | }) 181 | ); 182 | }); 183 | 184 | // todo: unhandled color shorthand/longhand 185 | test.todo('bg-black/50 shortform', () => { 186 | assert.deepEqual( 187 | { className: 'bg-black/50' }, 188 | classname({ 189 | property: 'backgroundColor', 190 | value: '#0008' 191 | }) 192 | ); 193 | }); 194 | 195 | // todo: black is stored as #000 in theme instead of full value :/ 196 | test.todo('bg-black', () => { 197 | assert.deepEqual( 198 | { className: 'bg-black' }, 199 | classname({ 200 | property: 'backgroundColor', 201 | value: '#000000' 202 | }) 203 | ); 204 | }); 205 | 206 | // incorrect input 207 | test('incorrect responsive modifier', () => { 208 | assert.deepEqual( 209 | { 210 | error: { 211 | responsiveModifier: 'Unidentified responsive modifier, expected one of [sm, md, lg, xl, 2xl], got small' 212 | } 213 | }, 214 | classname({ 215 | responsiveModifier: 'small', 216 | property: 'backgroundColor', 217 | value: '#dcfce7' 218 | }) 219 | ); 220 | }); 221 | 222 | test('incorrect pseudo modifier', () => { 223 | assert.deepEqual( 224 | { 225 | error: { 226 | pseudoModifier: 227 | 'Unidentified pseudo modifier, expected one of [first, last, odd, even, visited, checked, empty, read-only, group-hover, group-focus, focus-within, hover, focus, focus-visible, active, disabled], got hovers' 228 | } 229 | }, 230 | classname({ 231 | pseudoModifier: 'hovers', 232 | property: 'backgroundColor', 233 | value: '#dcfce7' 234 | }) 235 | ); 236 | }); 237 | 238 | test('incorrect property', () => { 239 | assert.deepEqual( 240 | { error: { property: 'UNIDENTIFIED_PROPERTY' } }, 241 | classname({ 242 | responsiveModifier: 'sm', 243 | 244 | property: 'fontSizes', 245 | value: '1.5rem' 246 | }) 247 | ); 248 | }); 249 | 250 | test('incorrect value', () => { 251 | assert.deepEqual( 252 | { error: { value: 'UNIDENTIFIED_VALUE' } }, 253 | classname({ 254 | responsiveModifier: 'sm', 255 | 256 | property: 'fontSize', 257 | value: '1.5em' // should be rem 258 | }) 259 | ); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /tests/custom-reporter.ts: -------------------------------------------------------------------------------- 1 | // 1. [x] show which test is failing 2 | // 2. create table first before result, switch them on as they come 3 | // 3. watch mode everything again. clear before re-render 4 | // 4. maybe restructure tests for better results on 2 and 3 5 | 6 | import chalk from 'chalk'; 7 | import type { Vitest, Reporter, TaskState, File } from 'vitest'; 8 | import { printTaskErrors } from './print-task-error'; 9 | 10 | const StateMap: { [key in TaskState]: string } = { 11 | pass: chalk.green('▎'), 12 | fail: chalk.redBright('▎'), 13 | skip: chalk.yellow('▎'), 14 | todo: chalk.gray('▎'), 15 | run: chalk.gray('▎'), 16 | only: '' // probably never see this 17 | }; 18 | 19 | const printResults = (results) => { 20 | const printableResult = results.testResults.map((state: TaskState) => { 21 | return StateMap[state]; 22 | }); 23 | 24 | let testColor = chalk.gray; 25 | 26 | if (results.state === 'pass') testColor = chalk.green; 27 | else if (results.state === 'fail') testColor = chalk.redBright; 28 | const line = chalk.gray('│'); 29 | 30 | const perLine = process.stdout.columns - 36 || 100; 31 | let resultLines: string[] = []; 32 | 33 | let nameColumnText = `${testColor(results.name)}`; 34 | if (results.duration) nameColumnText += ` ${chalk.gray(results.duration + 'ms')}`; 35 | 36 | // pad name to fill column 37 | const thisNameColumnSize = results.name.length + (results.duration + 'ms').length + 3; 38 | // TODO: don't hardcode this 39 | const nameColumnSize = 32; 40 | 41 | if (thisNameColumnSize < nameColumnSize) { 42 | const padding = Array(nameColumnSize - thisNameColumnSize - 1) 43 | .fill(' ') 44 | .join(''); 45 | nameColumnText = `${testColor(results.name)} ${padding} ${chalk.gray(results.duration + 'ms')}`; 46 | } 47 | 48 | if (results.testResults.length < perLine) { 49 | resultLines.push(`${line} ${nameColumnText} ${line} ${printableResult.join('')}${line}`); 50 | } else { 51 | const totalRows = Math.ceil(printableResult.length / perLine); 52 | 53 | // first line with name 54 | resultLines.push(`${line} ${nameColumnText} ${line} ${printableResult.slice(0, perLine).join('')}${line}`); 55 | 56 | // not the first and last row 57 | for (let i = 1; i < totalRows - 1; i++) { 58 | // blank line 59 | resultLines.push( 60 | `${line}${Array(nameColumnSize - 1) 61 | .fill(' ') 62 | .join('')} ${line} ${Array(perLine).fill(' ').join('')}${line}` 63 | ); 64 | 65 | resultLines.push( 66 | `${line} ${Array(nameColumnSize - 2) 67 | .fill(' ') 68 | .join('')} ${line} ${printableResult.slice(i * perLine, i * perLine + perLine).join('')}${line}` 69 | ); 70 | } 71 | 72 | // last line 73 | // blank line 74 | resultLines.push( 75 | `${line} ${Array(nameColumnSize - 2) 76 | .fill(' ') 77 | .join('')} ${line} ${Array(perLine).fill(' ').join('')}${line}` 78 | ); 79 | 80 | // fill last line with spaces with extra spaces to make box 81 | resultLines.push( 82 | `${line} ${Array(nameColumnSize - 2) 83 | .fill(' ') 84 | .join('')} ${line} ${printableResult.slice((totalRows - 1) * perLine, printableResult.length).join('')} ${Array( 85 | perLine - 1 - (printableResult.length % perLine) 86 | ) 87 | .fill(' ') 88 | .join('')}${line}` 89 | ); 90 | } 91 | 92 | const header = `┌${Array(nameColumnSize).fill('─').join('')}┬${Array(Math.min(printableResult.length, perLine) + 1) 93 | .fill('─') 94 | .join('')}┐`; 95 | 96 | const footer = `└${Array(nameColumnSize).fill('─').join('')}┴${Array(Math.min(printableResult.length, perLine) + 1) 97 | .fill('─') 98 | .join('')}┘`; 99 | 100 | console.log( 101 | ` 102 | ${chalk.gray(header)} 103 | ${resultLines.join('\n')} 104 | ${chalk.gray(footer)} 105 | `.trim() 106 | ); 107 | }; 108 | 109 | class reporter implements Reporter { 110 | onInit(ctx: Vitest) { 111 | this.ctx = ctx; 112 | clear(100); 113 | } 114 | 115 | // onCollected is called for every file 116 | async onCollected(files?: File[]) { 117 | if (process.env.CI) return; 118 | 119 | files.forEach((file) => { 120 | const results = { 121 | name: file.name, 122 | state: 'run', 123 | duration: 0, 124 | testResults: [] 125 | }; 126 | 127 | results.state = 'run'; // 'pass' | 'fail' | 'run' | 'skip' | 'only' | 'todo' 128 | 129 | // each describe is it's own suite 130 | const suites = file.tasks; 131 | suites.forEach((suite) => { 132 | suite.tasks.forEach((task) => { 133 | if (task.mode !== 'run') results.testResults.push(task.mode); 134 | else results.testResults.push('run'); 135 | }); 136 | }); 137 | 138 | printResults(results); 139 | }); 140 | } 141 | 142 | async onTaskUpdate() { 143 | clear(100); 144 | } 145 | 146 | // onFinished is called once at the end 147 | async onFinished(files: File[] = [], errors?: unknown[]) { 148 | clear(100); 149 | 150 | files.forEach((file) => { 151 | const results = { 152 | name: file.name, 153 | state: 'run', 154 | duration: 0, 155 | testResults: [] 156 | }; 157 | 158 | results.state = file.result!.state; // 'pass' | 'fail' | 'run' | 'skip' | 'only' | 'todo' 159 | results.duration = file.result!.duration; 160 | 161 | // each describe is it's own suite 162 | const suites = file.tasks; 163 | suites.forEach((suite) => { 164 | suite.tasks.forEach((task) => { 165 | if (task.mode !== 'run') results.testResults.push(task.mode); 166 | else results.testResults.push(task.result.state); 167 | }); 168 | }); 169 | 170 | printResults(results); 171 | }); 172 | 173 | let failedTasks = files 174 | .filter((file) => file.result.state === 'fail') 175 | .map((file) => file.tasks) 176 | .flat() 177 | .filter((suite) => suite.result.state === 'fail') 178 | .map((suite) => suite.tasks) 179 | .flat() 180 | .filter((task) => task.result.state === 'fail'); 181 | 182 | console.log('\n'); 183 | printTaskErrors(failedTasks, this.ctx); 184 | } 185 | } 186 | 187 | export default reporter; 188 | 189 | const clear = (linesToClear) => { 190 | if (process.env.CI) return; 191 | 192 | const stream = process.stdout; 193 | stream.cursorTo(0); 194 | 195 | for (let index = 0; index < linesToClear; index++) { 196 | if (index > 0) stream.moveCursor(0, -1); 197 | 198 | stream.clearLine(1); 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /tests/generated.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { describe, test, assert } from 'vitest'; 3 | 4 | import Tailwind from '../src/index'; 5 | const config = require('./tailwind.config'); 6 | const { parse, classname, meta } = Tailwind(config); 7 | 8 | const isPseudoState = (selector) => { 9 | return Boolean( 10 | meta.pseudoModifiers.find((modifier) => { 11 | if (selector.includes(':' + modifier)) return true; 12 | }) 13 | ); 14 | }; 15 | 16 | meta.responsiveModifiers.push('32xl'); 17 | const isResponsive = (selector) => { 18 | return Boolean( 19 | meta.responsiveModifiers.find((modifier) => { 20 | if (selector.includes(modifier + ':')) return true; 21 | }) 22 | ); 23 | }; 24 | 25 | const compositeClassNames = [ 26 | 'container', // no clue what to do with this 27 | 'sr-only', 28 | 'not-sr-only', 29 | 'transform', 30 | 'transform-gpu', 31 | 'scale-*', 32 | 'skew-*', 33 | 'rotate-*', 34 | 'rounded-*', 35 | 'truncate', 36 | 'break-normal', 37 | 'antialiased', 38 | 'outline-none', 39 | 'backdrop-filter', 40 | 'filter', 41 | 'transition-*', 42 | '/[0|5|10|20|25|30|40|50|60|70|75|80|95|100]' // opacity 43 | ]; 44 | 45 | const source = fs.readFileSync('./fixtures/tailwind-2.css', 'utf8'); 46 | const selectors = source.split('}\n').map((code) => code.split('{')[0].trim()); 47 | const classNames = selectors 48 | .filter((selector) => selector.startsWith('.')) 49 | .filter((selector) => !selector.includes(',')) 50 | .filter((selector) => !selector.includes(' ')) 51 | .map((selector) => selector.replace('.', '')) 52 | .map((selector) => selector.replace('\\:', ':')) 53 | .map((selector) => selector.replace('\\.', '.')) 54 | .map((selector) => selector.replace('\\/', '/')) // 1\/2 → 1/2 55 | .map((selector) => selector.replace('::placeholder', '')) 56 | .filter((selector) => !isPseudoState(selector)) 57 | .filter((selector) => !isResponsive(selector)); 58 | 59 | describe('generated suite', () => { 60 | // test.only('debug', async () => { 61 | // const originalClassName = 'sr-only'; 62 | // const definition = parse(originalClassName); 63 | // console.log(definition); 64 | // const { className: generatedClassName, error } = classname(definition); 65 | // console.log({ generatedClassName, error }); 66 | // assert.equal(originalClassName, generatedClassName); 67 | // }); 68 | // return; 69 | 70 | classNames.forEach((fixture) => { 71 | if (compositeClassNames.find((pattern) => fixture.match(pattern))) { 72 | test.skip(fixture); 73 | return; 74 | } 75 | 76 | test(fixture, async () => { 77 | // TODO: how do we test composite values 78 | 79 | const originalClassName = fixture; 80 | const { className, relatedProperties, ...definition } = parse(originalClassName); 81 | const { className: generatedClassName } = classname(definition); 82 | 83 | assert.equal(generatedClassName, getEquivalent(originalClassName)); 84 | }); 85 | }); 86 | }); 87 | 88 | const getEquivalent = (className) => { 89 | if (knownEquals[className]) return knownEquals[className]; 90 | else if (className.includes('blue-gray')) return className.replace('blue-gray', 'slate'); 91 | else if (className.includes('zinc')) return className.replace('zinc', 'neutral'); 92 | else return className; 93 | }; 94 | 95 | const knownEquals = { 96 | 'decoration-clone': 'box-decoration-clone', 97 | 'decoration-slice': 'box-decoration-slice', 98 | 'backdrop-blur-none': 'backdrop-blur-0', 99 | 'blur-none': 'blur-0', 100 | 'ease-in-out': 'ease' 101 | }; 102 | 103 | [ 104 | 'inset', 105 | 'inset-x', 106 | 'inset-y', 107 | 'top', 108 | 'right', 109 | 'bottom', 110 | 'left', 111 | 'h', 112 | 'w', 113 | 'translate', 114 | 'translate-x', 115 | 'translate-y' 116 | ].forEach((property) => { 117 | knownEquals[property + '-' + '2/4'] = property + '-' + '1/2'; 118 | knownEquals['-' + property + '-' + '2/4'] = '-' + property + '-' + '1/2'; 119 | }); 120 | 121 | ['h', 'w'].forEach((property) => { 122 | knownEquals[property + '-2/6'] = property + '-1/3'; 123 | knownEquals['-' + property + '-2/6'] = '-' + property + '-1/3'; 124 | knownEquals[property + '-3/6'] = property + '-1/2'; 125 | knownEquals['-' + property + '-3/6'] = '-' + property + '-1/2'; 126 | knownEquals[property + '-4/6'] = property + '-2/3'; 127 | knownEquals['-' + property + '-4/6'] = '-' + property + '-2/3'; 128 | knownEquals[property + '-3/6'] = property + '-1/2'; 129 | knownEquals['-' + property + '-3/6'] = '-' + property + '-1/2'; 130 | 131 | knownEquals[property + '-2/12'] = property + '-1/6'; 132 | knownEquals['-' + property + '-2/12'] = '-' + property + '-1/6'; 133 | knownEquals[property + '-3/12'] = property + '-1/4'; 134 | knownEquals['-' + property + '-3/12'] = '-' + property + '-1/4'; 135 | knownEquals[property + '-4/12'] = property + '-1/3'; 136 | knownEquals['-' + property + '-4/12'] = '-' + property + '-1/4'; 137 | knownEquals[property + '-6/12'] = property + '-1/2'; 138 | knownEquals['-' + property + '-6/12'] = '-' + property + '-' + '1/2'; 139 | knownEquals[property + '-8/12'] = property + '-2/3'; 140 | knownEquals['-' + property + '-8/12'] = '-' + property + '-' + '2/3'; 141 | knownEquals[property + '-9/12'] = property + '-3/4'; 142 | knownEquals['-' + property + '-9/12'] = '-' + property + '-' + '3/4'; 143 | knownEquals[property + '-10/12'] = property + '-5/6'; 144 | knownEquals['-' + property + '-10/12'] = '-' + property + '-' + '5/6'; 145 | }); 146 | -------------------------------------------------------------------------------- /tests/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, assert } from 'vitest'; 2 | 3 | import Tailwind from '../src/index'; 4 | const config = require('./tailwind.config'); 5 | const { parse } = Tailwind(config); 6 | 7 | describe('parse', () => { 8 | test('m-4', async () => { 9 | assert.deepEqual(parse('m-4'), { 10 | className: 'm-4', 11 | responsiveModifier: null, 12 | pseudoModifier: null, 13 | property: 'margin', 14 | value: '1rem', 15 | relatedProperties: {} 16 | }); 17 | }); 18 | 19 | test('md:w-48', () => { 20 | assert.deepEqual(parse('md:w-48'), { 21 | className: 'md:w-48', 22 | responsiveModifier: 'md', 23 | pseudoModifier: null, 24 | property: 'width', 25 | value: '12rem', 26 | relatedProperties: {} 27 | }); 28 | }); 29 | 30 | test('text-sm', () => { 31 | assert.deepEqual(parse('text-sm'), { 32 | className: 'text-sm', 33 | responsiveModifier: null, 34 | pseudoModifier: null, 35 | property: 'fontSize', 36 | value: '0.875rem', 37 | relatedProperties: { lineHeight: '1.25rem' } 38 | }); 39 | }); 40 | 41 | test('md:hover:text-blue-600', () => { 42 | assert.deepEqual(parse('md:hover:text-blue-600'), { 43 | className: 'md:hover:text-blue-600', 44 | responsiveModifier: 'md', 45 | pseudoModifier: 'hover', 46 | property: 'textColor', 47 | value: '#2563eb', 48 | relatedProperties: {} 49 | }); 50 | }); 51 | 52 | test('hover:bg-green-100', () => { 53 | assert.deepEqual(parse('hover:bg-green-100'), { 54 | className: 'hover:bg-green-100', 55 | responsiveModifier: null, 56 | pseudoModifier: 'hover', 57 | property: 'backgroundColor', 58 | value: '#dcfce7', 59 | relatedProperties: {} 60 | }); 61 | }); 62 | 63 | test('absolute', () => { 64 | assert.deepEqual(parse('absolute'), { 65 | className: 'absolute', 66 | responsiveModifier: null, 67 | pseudoModifier: null, 68 | property: 'position', 69 | value: 'absolute', 70 | relatedProperties: {} 71 | }); 72 | }); 73 | 74 | test('font-serif', () => { 75 | assert.deepEqual(parse('font-serif'), { 76 | className: 'font-serif', 77 | responsiveModifier: null, 78 | pseudoModifier: null, 79 | property: 'fontFamily', 80 | value: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif', 81 | relatedProperties: {} 82 | }); 83 | }); 84 | 85 | test('flex', () => { 86 | assert.deepEqual(parse('flex'), { 87 | className: 'flex', 88 | responsiveModifier: null, 89 | pseudoModifier: null, 90 | property: 'display', 91 | value: 'flex', 92 | relatedProperties: {} 93 | }); 94 | }); 95 | 96 | test('bg-red-200/50', () => { 97 | assert.deepEqual(parse('bg-red-200/50'), { 98 | className: 'bg-red-200/50', 99 | responsiveModifier: null, 100 | pseudoModifier: null, 101 | property: 'backgroundColor', 102 | value: '#fecaca80', 103 | relatedProperties: {} 104 | }); 105 | }); 106 | 107 | test('right-2/4', () => { 108 | assert.deepEqual(parse('right-2/4'), { 109 | className: 'right-2/4', 110 | responsiveModifier: null, 111 | pseudoModifier: null, 112 | property: 'right', 113 | value: '50%', 114 | relatedProperties: {} 115 | }); 116 | }); 117 | 118 | // composite values 119 | test('sr-only', () => { 120 | assert.deepEqual(parse('sr-only'), { 121 | className: 'sr-only', 122 | responsiveModifier: null, 123 | pseudoModifier: null, 124 | property: 'composite', 125 | value: null, 126 | relatedProperties: { 127 | position: 'absolute', 128 | width: '1px', 129 | height: '1px', 130 | padding: '0', 131 | margin: '-1px', 132 | overflow: 'hidden', 133 | clip: 'rect(0, 0, 0, 0)', 134 | whiteSpace: 'nowrap', 135 | borderWidth: '0' 136 | } 137 | }); 138 | }); 139 | 140 | test('block', () => { 141 | assert.deepEqual(parse('block'), { 142 | className: 'block', 143 | responsiveModifier: null, 144 | pseudoModifier: null, 145 | property: 'display', 146 | value: 'block', 147 | relatedProperties: {} 148 | }); 149 | }); 150 | 151 | // incorrect input 152 | test('hovers:bg-green-100', () => { 153 | assert.deepEqual(parse('hovers:bg-green-100'), { 154 | className: 'hovers:bg-green-100', 155 | responsiveModifier: null, 156 | pseudoModifier: null, 157 | property: 'backgroundColor', 158 | value: '#dcfce7', 159 | relatedProperties: {} 160 | }); 161 | }); 162 | 163 | test('bg-green-1000', () => { 164 | assert.deepEqual(parse('bg-green-1000'), { 165 | className: 'bg-green-1000', 166 | responsiveModifier: null, 167 | pseudoModifier: null, 168 | property: 'ERROR', 169 | value: 'ERROR', 170 | relatedProperties: {} 171 | }); 172 | }); 173 | 174 | test('drop-shadow-md', () => { 175 | assert.deepEqual(parse('drop-shadow-md'), { 176 | className: 'drop-shadow-md', 177 | responsiveModifier: null, 178 | pseudoModifier: null, 179 | property: 'dropShadow', 180 | value: '0 4px 3px rgb(0 0 0 / 0.07), 0 2px 2px rgb(0 0 0 / 0.06)', 181 | relatedProperties: {} 182 | }); 183 | }); 184 | 185 | test('sm:-m-64', () => { 186 | assert.deepEqual(parse('sm:-m-64'), { 187 | className: 'sm:-m-64', 188 | responsiveModifier: 'sm', 189 | pseudoModifier: null, 190 | property: 'margin', 191 | value: '-16rem', 192 | relatedProperties: {} 193 | }); 194 | }); 195 | 196 | test('bg-red-200/50', () => { 197 | assert.deepEqual(parse('bg-red-200/50'), { 198 | className: 'bg-red-200/50', 199 | responsiveModifier: null, 200 | pseudoModifier: null, 201 | property: 'backgroundColor', 202 | value: '#fecaca80', 203 | relatedProperties: {} 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /tests/print-task-error.ts: -------------------------------------------------------------------------------- 1 | // copied from https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/reporters/base.ts 2 | 3 | import chalk from 'chalk'; 4 | import type { Vitest, File, Task, ErrorWithDiff } from 'vitest'; 5 | 6 | export async function printTaskErrors(tasks: Task[], ctx: Vitest) { 7 | const errorsQueue: [error: ErrorWithDiff | undefined, tests: Task[]][] = []; 8 | 9 | for (const task of tasks) { 10 | // merge identical errors 11 | const error = task.result?.error; 12 | const errorItem = error?.stackStr && errorsQueue.find((i) => i[0]?.stackStr === error.stackStr); 13 | if (errorItem) errorItem[1].push(task); 14 | else errorsQueue.push([error, [task]]); 15 | } 16 | 17 | for (const [error, tasks] of errorsQueue) { 18 | for (const task of tasks) { 19 | const filepath = (task as File)?.filepath || ''; 20 | let name = getFullName(task); 21 | 22 | if (filepath) name = `${name} ${chalk.dim(`[ ${this.relative(filepath)} ]`)}`; 23 | ctx.logger.error(chalk.red(name)); 24 | ctx.logger.error(); 25 | // ctx.logger.error(`${chalk.red(chalk.bold(chalk.inverse(' FAIL ')))} ${name}`); 26 | } 27 | 28 | await ctx.logger.printError(error); 29 | 30 | // error divider 31 | const divider = '⎯'.repeat(process.stdout.columns || 100); 32 | ctx.logger.error(chalk.red(chalk.dim(divider)) + '\n'); 33 | 34 | await Promise.resolve(); 35 | } 36 | } 37 | 38 | export function getFullName(task: Task) { 39 | return getNames(task).join(chalk.dim(' > ')); 40 | } 41 | 42 | export function getNames(task: Task) { 43 | const names = [task.name]; 44 | let current: Task | undefined = task; 45 | 46 | while (current?.suite || current?.file) { 47 | current = current.suite || current.file; 48 | if (current?.name) names.unshift(current.name); 49 | } 50 | 51 | return names; 52 | } 53 | -------------------------------------------------------------------------------- /tests/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | content: ['./src/**/*.{html,js}'], 5 | safelist: [{pattern: /(.)/}], 6 | theme: { 7 | extend: { 8 | colors: { 9 | white: '#ffffff', 10 | 'blue-gray': colors.slate 11 | }, 12 | width: { 13 | 17: '68rem', 14 | 18: '72rem' 15 | }, 16 | scale: { 17 | '-100': '-1' 18 | } 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/*.ts"], 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "esModuleInterop": true 7 | } 8 | } 9 | --------------------------------------------------------------------------------