Skip to content

Commit 1d067b3

Browse files
author
Cem Yılmaz
committed
AST to Classname converter here!
- I think I used too much for loops - at this point I don't know what to do. - I don't even know this library works at all
1 parent 5f0a667 commit 1d067b3

File tree

11 files changed

+385
-175
lines changed

11 files changed

+385
-175
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ It is probably missing many Tailwind classes and might throw some exceptions. Yo
1313
You can install the Tailwindcss Parser via npm:
1414

1515
```bash
16-
npm install tailwindcss-class-parser
16+
npm install @xengine/tailwindcss-class-parser
1717
```
1818

1919
## Usage
@@ -23,7 +23,7 @@ To use the Tailwindcss Parser, you need to import the `parse` function from the
2323
### Example
2424

2525
```javascript
26-
import { parse } from 'tailwindcss-class-parser';
26+
import { parse } from '@xengine/tailwindcss-class-parser';
2727

2828
const ast = parse('lg:hover:text-red-500');
2929

@@ -74,7 +74,22 @@ Parses a given Tailwind CSS class and returns an AST object.
7474
### `className(ast: object): string`
7575

7676
Converts a given Tailwind CSS AST to the original class string.
77-
- TODO
77+
#### Parameters
78+
79+
- `ast` (object): small Tailwind CSS declaration
80+
```
81+
AstDeclaration = {
82+
property: string
83+
value: string
84+
variants?: Variant[]
85+
modifier?: string,
86+
important?: boolean
87+
negative?: boolean,
88+
}
89+
```
90+
#### Returns
91+
92+
- `className` (string): The Tailwind CSS class.
7893

7994
## Contributing
8095

package-lock.json

Lines changed: 13 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@
2929
"devDependencies": {
3030
"@types/lodash": "^4.17.6",
3131
"@types/node": "^20.14.10",
32+
"lodash": "^4.17.21",
3233
"typescript": "^5.2.2",
3334
"vite": "^5.3.1",
3435
"vite-plugin-dts": "^3.9.1",
35-
"vitest": "^2.0.1",
36-
"lodash": "^4.17.21"
36+
"vitest": "^2.0.1"
3737
},
3838
"publishConfig": {
3939
"access": "public"
40+
},
41+
"dependencies": {
42+
"colord": "^2.9.3"
4043
}
4144
}

src/classname.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {expect, test} from 'vitest'
2+
import {type AstDeclaration, classname} from "./classname.ts";
3+
4+
const table = [
5+
[{property: "display", value: "flex"}, "flex"],
6+
[{property: "display", value: "flex", important: true}, "!flex"],
7+
[{property: "backgroundColor", value: "#ff0000"}, "bg-[#ff0000]"],
8+
[{property: "backgroundColor", value: "red-500"}, "bg-red-500"],
9+
[{property: "backgroundColor", value: "#ef4444"}, "bg-red-500"],
10+
[{property: "backgroundColor", value: "#ef44444d"}, "bg-red-500/30"],
11+
[{property: "backgroundColor", value: "rgba(239, 68, 68, 0.3)"}, "bg-red-500/30"],
12+
[{property: "backgroundColor", value: "hsla(0, 84%, 60.1%, 0.3)"}, "bg-red-500/30"],
13+
[{property: "fontSize", value: "1rem"}, "text-base"],
14+
[{property: "fontSize", value: "12px"}, "text-[12px]"],
15+
[{property: "margin", value: "12px"}, "m-[12px]"],
16+
[{property: "marginRight", value: "1rem"}, "mr-4"],
17+
[{property: "marginRight", value: "-1rem"}, "-mr-4"],
18+
[{property: "marginRight", value: "1rem", negative: true}, "-mr-4"],
19+
]
20+
21+
//@ts-ignore
22+
test.each(table)('should parse declaration into tailwindcss classes: "%s" -> "%s"', (input: any, expected: any) => {
23+
expect(classname(input)).toEqual(expected)
24+
})

src/classname.ts

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,140 @@ import {inferDataType} from "./utils/infer-data-type.ts";
44
import {getTailwindTheme} from "./theme.ts";
55
import {isColor} from "./utils/is-color.ts";
66
import {decodeArbitraryValue} from "./utils/decodeArbitraryValue.ts";
7+
import {segment} from "./utils/segment.ts";
8+
import {PluginNotFoundException} from "./exceptions/plugin-not-found-exception.ts";
9+
import {colord} from "colord";
10+
import type {CustomThemeConfig} from "tailwindcss/types/config";
11+
import {StringBuilder} from "./utils/string-builder.ts";
712

813
export type AstDeclaration = {
914
property: string
1015
value: string
1116
variants?: Variant[]
12-
modifiers?: Modifier[],
17+
modifier?: string,
1318
important?: boolean
1419
negative?: boolean,
1520
}
1621

17-
export const classname = (ast: AstDeclaration): string => {
18-
const theme = getTailwindTheme()
19-
const important = ast.important ? "!" : ""
20-
let variants = ast.variants?.length ? buildVariants(ast.variants) : []
22+
export const classname = (ast: AstDeclaration, config?: CustomThemeConfig): string => {
23+
const theme = getTailwindTheme(config)
24+
const stringBuilder = new StringBuilder()
25+
let negative = ast.negative || false
26+
stringBuilder.appendVariants(...ast.variants || [])
2127

22-
/**
23-
* output "sm:hover:lg:!" or "!" or ""
24-
*/
25-
const prefix = `${variants}${important}`
26-
const [namedPluginClassName, namedPluginClassPlugin] = [...namedPlugins.entries()].find(([,plugin]) => plugin.value === ast.value) || []
27-
if(namedPluginClassName){
28-
return `${prefix}${namedPluginClassName}`
28+
if (ast.important) {
29+
stringBuilder.makeImportant()
2930
}
3031

31-
//color is special, we need to find if value is a color
32-
if(isColor(ast.value, theme)){
33-
console.log(decodeArbitraryValue(ast.value))
34-
return `${prefix}text-[${ast.value}]`
32+
if (ast.value[0] === "-") {
33+
ast.value = ast.value.slice(1)
34+
negative = true
3535
}
3636

37-
/*
38-
const [functionalPluginClassName, matchedFunctionalPlugins] = [...functionalPlugins.entries()].find(([,plugin]) => plugin.value === ast.value) || []
39-
if(key){
40-
return `${prefix}${key}`
41-
}*/
37+
const [namedPluginClassName, namedPluginClassPlugin] = [...namedPlugins.entries()].find(([, plugin]) => plugin.value === ast.value) || []
38+
if (namedPluginClassName) {
39+
return stringBuilder.addRoot(namedPluginClassName).toString()
40+
}
41+
42+
const [root, possiblePlugins = []] = [...functionalPlugins.entries()].find(([root, plugins]) => plugins.some(o => o.ns === ast.property)) || []
43+
if (!root) {
44+
throw new PluginNotFoundException(ast.property)
45+
}
46+
47+
stringBuilder.addRoot(root)
48+
//color is special, we need to find if value is a color
49+
if (isColor(ast.value, theme)) {
50+
const matchedPlugin = possiblePlugins.find((plugin) => plugin.type === "color")
51+
if (!matchedPlugin) {
52+
throw new PluginNotFoundException(ast.property)
53+
}
54+
55+
let tailwindThemeColor = ast.value.split('-').reduce((acc, val) => acc[val], theme[matchedPlugin.scaleKey || "colors"] as any)
56+
if (tailwindThemeColor && typeof tailwindThemeColor !== "object") {
57+
//user entered a value like "red-500". we found equivalent tailwind theme color.
58+
//return TW class directly like "bg-red-500" with modifiers and variants
59+
return stringBuilder
60+
.appendModifier(buildModifier(ast.modifier, theme.opacity))
61+
.addValue(ast.value)
62+
.toString()
63+
}
64+
//at this point we know user entered a value like "#ff0000", or just "red" maybe rgba, hsla, etc.
65+
//try to get hex color and check if tailwind has it.
66+
const color = calculateHex(ast.value)
67+
return stringBuilder
68+
.appendModifier(buildModifier(color.alpha || ast.modifier, theme.opacity))
69+
.addValue(findTailwindColorByHex(color.hex, theme[matchedPlugin.scaleKey || "colors"]) || StringBuilder.makeArbitrary(color.hex))
70+
.toString()
71+
}
72+
73+
const matchedPlugin = possiblePlugins.find((plugin) => plugin.ns === ast.property)
74+
if (!matchedPlugin) {
75+
throw new PluginNotFoundException(ast.property)
76+
}
4277

43-
return "dummy"
78+
const possibleValue = findTailwindValueByUnit(ast.value, theme[matchedPlugin.scaleKey])
79+
if (possibleValue) {
80+
stringBuilder.addValue(possibleValue)
81+
82+
//for making the class negative, we are making sure matched TW Plugin supports negative
83+
if (matchedPlugin.supportNegative && negative) {
84+
stringBuilder.makeNegative()
85+
}
86+
}
87+
88+
return stringBuilder.toString()
89+
}
90+
91+
const calculateHex = (input: string): { hex: string, alpha: string | undefined } => {
92+
const color = colord(input)
93+
const alpha = color.alpha()
94+
95+
return {
96+
hex: color.alpha(1).toHex(),
97+
alpha: alpha !== 1 ? alpha.toString() : undefined
98+
}
4499
}
45100

46-
const buildVariants = (variants: Variant[]): string => {
47-
const variantOrder: Variant["type"][] = ["media", 'system', "interaction", "pseudo", "content", "form", "state", "misc"]
48-
const _sortedVariants = variants.sort((a, b) => variantOrder.indexOf(a.type) - variantOrder.indexOf(b.type))
49-
return _sortedVariants.length > 0 ? _sortedVariants.map(x => x.value).join(':') + ':' : ''
101+
const buildModifier = (modifier: string | undefined, opacityScale: any): string => {
102+
if (!modifier) return ""
103+
104+
for (let [key, value] of Object.entries(opacityScale)) {
105+
if (key === modifier || value === modifier) {
106+
return key
107+
}
108+
}
109+
110+
return StringBuilder.makeArbitrary(modifier)
111+
}
112+
113+
const findTailwindColorByHex = (colorInput: string | undefined, colors: any) => {
114+
if (!colorInput) return false
115+
116+
for (let [key, twColors] of Object.entries(colors)) {
117+
for (let [shade, hex] of Object.entries(twColors as [string, string])) {
118+
if (hex === colorInput) {
119+
return `${key}-${shade}`
120+
}
121+
}
122+
}
123+
124+
return false
125+
}
126+
127+
const findTailwindValueByUnit = (unit: string | undefined, scale: any) => {
128+
if (!unit) {
129+
return undefined
130+
}
131+
132+
for (let [key, value] of Object.entries(scale)) {
133+
if (
134+
(Array.isArray(value) && (key === unit || value.includes(unit))) ||
135+
(key === unit || value === unit)
136+
) {
137+
return key !== "DEFAULT" ? key : undefined
138+
}
139+
}
140+
141+
//if unit is not found in tailwind scales, it's probably an arbitrary unit
142+
return StringBuilder.makeArbitrary(unit)
50143
}

src/index.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,8 @@
11
import {parse} from "./parse.ts";
22
import {classname} from "./classname.ts";
33

4-
console.log(parse("sm:!flex"))
5-
console.log(parse("lg:hover:bg-red-500/50"))
4+
export {parse, classname}
65

7-
8-
console.log(classname({
9-
property: "display",
10-
value: "flex",
11-
variants: [
12-
{
13-
"kind": "named",
14-
"type": "interaction",
15-
"value": "hover"
16-
},
17-
{
18-
"kind": "named",
19-
"type": "media",
20-
"value": "sm"
21-
}
22-
],
23-
}))
24-
console.log(classname({
25-
property: "flexGrow",
26-
value: "0",
27-
variants: [
28-
{
29-
"kind": "named",
30-
"type": "interaction",
31-
"value": "hover"
32-
},
33-
{
34-
"kind": "named",
35-
"type": "media",
36-
"value": "sm"
37-
}
38-
],
39-
}))
40-
41-
console.log(classname({
42-
property: "backgroundColor",
43-
value: "#ff0000",
44-
}))
45-
46-
export {parse, classname}
6+
console.log(
7+
classname({property: "marginRight", value: "-1rem"})
8+
)

0 commit comments

Comments
 (0)