├── .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 |
--------------------------------------------------------------------------------