From a7afc2ac9c6d0c16b958d519d84615868b917ba5 Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Wed, 5 Nov 2025 11:06:08 -0300 Subject: [PATCH 1/7] fixing things - working --- .gitignore | 3 +- package.json | 2 +- src/classname.js | 104 +++++++++++++++++++ src/classname.ts | 119 ---------------------- src/index.ts | 6 +- src/parse-variant.ts | 9 +- src/parse.ts | 41 ++++---- src/theme.ts | 14 +-- src/utils/build-modifier.js | 38 +++++++ src/utils/build-modifier.ts | 38 ------- src/utils/calculate-hex-from-string.ts | 10 +- src/utils/find-tailwind-color-from-hex.js | 19 ++++ src/utils/find-tailwind-color-from-hex.ts | 20 ---- src/utils/is-color.ts | 5 +- src/utils/string-builder.js | 60 +++++++++++ src/utils/string-builder.ts | 66 ------------ 16 files changed, 262 insertions(+), 292 deletions(-) create mode 100644 src/classname.js delete mode 100644 src/classname.ts create mode 100644 src/utils/build-modifier.js delete mode 100644 src/utils/build-modifier.ts create mode 100644 src/utils/find-tailwind-color-from-hex.js delete mode 100644 src/utils/find-tailwind-color-from-hex.ts create mode 100644 src/utils/string-builder.js delete mode 100644 src/utils/string-builder.ts diff --git a/.gitignore b/.gitignore index 54f07af..9054e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? +yarn.lock \ No newline at end of file diff --git a/package.json b/package.json index 8c5d902..c44becb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@xengine/tailwindcss-class-parser", + "name": "inan-tailwind-parser", "private": false, "version": "1.1.20", "type": "module", diff --git a/src/classname.js b/src/classname.js new file mode 100644 index 0000000..f3377b1 --- /dev/null +++ b/src/classname.js @@ -0,0 +1,104 @@ +import get from 'lodash/get' + +import { PluginNotFoundException } from './exceptions/plugin-not-found-exception' +import { functionalPlugins, namedPlugins } from './plugins' +import { getTailwindTheme } from './theme' +import { buildModifier } from './utils/build-modifier' +import { calculateHexFromString } from './utils/calculate-hex-from-string' +import { findTailwindColorFromHex } from './utils/find-tailwind-color-from-hex' +import { isColor } from './utils/is-color' +import { StringBuilder } from './utils/string-builder' + +export const EMPTY_CLASSNAME = '' + +export const classname = (ast, config) => { + const theme = getTailwindTheme(config) + const stringBuilder = new StringBuilder() + let negative = ast.negative || false + stringBuilder.appendVariants(...ast.variants || []) + + if (ast.important) { + stringBuilder.makeImportant() + } + + if (ast.value[0] === '-') { + ast.value = ast.value.slice(1) + negative = true + } + + const [namedPluginClassName] = [...namedPlugins.entries()] + .filter(([, plugin]) => plugin.ns === ast.property) + .find(([, plugin]) => plugin.value === ast.value) || [] + + if (namedPluginClassName) { + return stringBuilder.addRoot(namedPluginClassName).toString() + } + + const [root, possiblePlugins = []] = [...functionalPlugins.entries()].find(([, plugins]) => plugins.some(o => o.ns === ast.property)) || [] + if (!root) { + throw new PluginNotFoundException(ast.property) + } + + stringBuilder.addRoot(root) + if (isColor(ast.value, theme)) { + const matchedPlugin = possiblePlugins.find(plugin => plugin.type === 'color') + if (!matchedPlugin) { + throw new PluginNotFoundException(ast.property) + } + + const tailwindThemeColor = get(theme[matchedPlugin.scaleKey || 'colors'], ast.value.split('-').join('.')) + if (tailwindThemeColor && typeof tailwindThemeColor !== 'object') { + return stringBuilder + .appendModifier(buildModifier(ast.modifier, theme.opacity)) + .addValue(ast.value) + .toString() + } + + const isRgba = ast.value.includes('rgba') + if (isRgba) { + return stringBuilder + .addValue(findTailwindValueByUnit(ast.value)) + .toString() + } + + const color = calculateHexFromString(ast.value) + if (!color) { + return EMPTY_CLASSNAME + } + + return stringBuilder + .appendModifier(buildModifier(color.alpha || ast.modifier, theme.opacity)) + .addValue( + findTailwindColorFromHex(color.hex, theme[matchedPlugin.scaleKey || 'colors']) || StringBuilder.makeArbitrary(color.hex), + ) + .toString() + } + + const matchedPlugin = possiblePlugins.find(plugin => plugin.ns === ast.property) + if (!matchedPlugin) { + throw new PluginNotFoundException(ast.property) + } + + const possibleValue = findTailwindValueByUnit(ast.value, matchedPlugin) + if (possibleValue) { + stringBuilder.addValue(possibleValue) + + if (matchedPlugin.supportNegative && negative) { + stringBuilder.makeNegative() + } + } + + return stringBuilder.toString() +} + +const findTailwindValueByUnit = (unit, matchedPlugin = {}) => { + if (!unit) { + return undefined + } + + if (matchedPlugin.type === 'image') { + unit = `url(http://github.com/$%7Bunit%7D)` + } + + return StringBuilder.makeArbitrary(unit) +} diff --git a/src/classname.ts b/src/classname.ts deleted file mode 100644 index b3317ba..0000000 --- a/src/classname.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {functionalPlugins, namedPlugins, type Variant} from "./plugins"; -import {getTailwindTheme} from "./theme"; -import {isColor} from "./utils/is-color"; -import {PluginNotFoundException} from "./exceptions/plugin-not-found-exception"; -import type {Config} from "tailwindcss/types/config"; -import {StringBuilder} from "./utils/string-builder"; -import {CalculateHexFromString} from "./utils/calculate-hex-from-string"; -import {findTailwindColorFromHex} from "./utils/find-tailwind-color-from-hex"; -import {buildModifier} from "./utils/build-modifier"; -import get from "lodash/get"; - -export type AstDeclaration = { - property: string - value: string - variants?: Variant[] - modifier?: string, - important?: boolean - negative?: boolean, -} - -export const EMPTY_CLASSNAME = "" - -export const classname = (ast: AstDeclaration, config?: Config): string => { - const theme = getTailwindTheme(config) - const stringBuilder = new StringBuilder() - let negative = ast.negative || false - stringBuilder.appendVariants(...ast.variants || []) - - if (ast.important) { - stringBuilder.makeImportant() - } - - if (ast.value[0] === "-") { - ast.value = ast.value.slice(1) - negative = true - } - - const [namedPluginClassName] = [...namedPlugins.entries()] - .filter(([, plugin]) => plugin.ns === ast.property) - .find(([, plugin]) => plugin.value === ast.value) - || [] - - if (namedPluginClassName) { - return stringBuilder.addRoot(namedPluginClassName).toString() - } - - const [root, possiblePlugins = []] = [...functionalPlugins.entries()].find(([, plugins]) => plugins.some(o => o.ns === ast.property)) || [] - if (!root) { - throw new PluginNotFoundException(ast.property) - } - - stringBuilder.addRoot(root) - //color is special, we need to find if value is a color - if (isColor(ast.value, theme)) { - const matchedPlugin = possiblePlugins.find((plugin) => plugin.type === "color") - if (!matchedPlugin) { - throw new PluginNotFoundException(ast.property) - } - - let tailwindThemeColor = get(theme[matchedPlugin.scaleKey || "colors"] as any, ast.value.split('-').join('.')) - if (tailwindThemeColor && typeof tailwindThemeColor !== "object") { - //user entered a value like "red-500". we found equivalent tailwind theme color. - //return TW class directly like "bg-red-500" with modifiers and variants - return stringBuilder - .appendModifier(buildModifier(ast.modifier, theme.opacity)) - .addValue(ast.value) - .toString() - } - //at this point we know user entered a value like "#ff0000", or rgba, hsla, etc. - //try to get hex color and check if tailwind has it. - const color = CalculateHexFromString(ast.value) - if(!color) { - return EMPTY_CLASSNAME - } - - return stringBuilder - .appendModifier(buildModifier(color.alpha || ast.modifier, theme.opacity)) - .addValue( - findTailwindColorFromHex(color.hex, theme[matchedPlugin.scaleKey || "colors"]) || - StringBuilder.makeArbitrary(color.hex) - ) - .toString() - } - - const matchedPlugin = possiblePlugins.find((plugin) => plugin.ns === ast.property) - if (!matchedPlugin) { - throw new PluginNotFoundException(ast.property) - } - - const possibleValue = findTailwindValueByUnit(ast.value, theme[matchedPlugin.scaleKey]) - if (possibleValue) { - stringBuilder.addValue(possibleValue) - - //for making the class negative, we are making sure matched TW Plugin supports negative - if (matchedPlugin.supportNegative && negative) { - stringBuilder.makeNegative() - } - } - - return stringBuilder.toString() -} - -const findTailwindValueByUnit = (unit: string | undefined, scale: any) => { - if (!unit) { - return undefined - } - - for (let [key, value] of Object.entries(scale)) { - if ( - (Array.isArray(value) && (key === unit || value.includes(unit))) || - (key === unit || value === unit) - ) { - return key !== "DEFAULT" ? key : undefined - } - } - - //if unit is not found in tailwind scales, it's probably an arbitrary unit - return StringBuilder.makeArbitrary(unit) -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a8dcf5e..493bd6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {parse} from "./parse"; -import {classname} from "./classname"; +import { classname } from './classname'; +import { parse } from './parse'; -export {parse, classname} \ No newline at end of file +export { parse, classname } diff --git a/src/parse-variant.ts b/src/parse-variant.ts index 4a8772f..7fa16de 100644 --- a/src/parse-variant.ts +++ b/src/parse-variant.ts @@ -1,8 +1,7 @@ -import {decodeArbitraryValue} from "./utils/decodeArbitraryValue"; -import {type Variant, variants} from "./plugins"; -import type {ScreensConfig} from "tailwindcss/types/config"; +import { decodeArbitraryValue } from "./utils/decodeArbitraryValue"; +import { type Variant, variants } from "./plugins"; -export function parseVariant(variant: string, screens: ScreensConfig): Variant { +export function parseVariant(variant: string, screens: any): Variant { if (variant[0] === '[' && variant[variant.length - 1] === ']') { let arbitraryValue = variant.slice(1, -1) @@ -19,7 +18,7 @@ export function parseVariant(variant: string, screens: ScreensConfig): Variant } } - if(Object.keys(screens).includes(variant)) { + if (Object.keys(screens).includes(variant)) { return { kind: 'named', type: 'media', diff --git a/src/parse.ts b/src/parse.ts index 944a005..7c527f3 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,16 +1,15 @@ -import {segment} from "./utils/segment"; -import {findRoot} from "./find-root"; -import {type FunctionalPlugin, functionalPlugins, namedPlugins, type Variant} from "./plugins"; -import {parseVariant} from "./parse-variant"; -import {inferDataType} from "./utils/infer-data-type"; -import {getValue, type Value} from "./utils/value"; -import type {Config, ScreensConfig} from "tailwindcss/types/config"; -import {getTailwindTheme} from "./theme"; -import {CalculateHexFromString} from "./utils/calculate-hex-from-string"; -import {findTailwindColorFromHex} from "./utils/find-tailwind-color-from-hex"; -import {buildModifier} from "./utils/build-modifier"; -import {isColor} from "./utils/is-color"; -import {decodeArbitraryValue} from "./utils/decodeArbitraryValue"; +import { segment } from "./utils/segment"; +import { findRoot } from "./find-root"; +import { type FunctionalPlugin, functionalPlugins, namedPlugins, type Variant } from "./plugins"; +import { parseVariant } from "./parse-variant"; +import { inferDataType } from "./utils/infer-data-type"; +import { getValue, type Value } from "./utils/value"; +import { getTailwindTheme } from "./theme"; +import { CalculateHexFromString } from "./utils/calculate-hex-from-string"; +import { findTailwindColorFromHex } from "./utils/find-tailwind-color-from-hex"; +import { buildModifier } from "./utils/build-modifier"; +import { isColor } from "./utils/is-color"; +import { decodeArbitraryValue } from "./utils/decodeArbitraryValue"; export type State = { important: boolean @@ -36,8 +35,8 @@ export type Error = { message: string } -export const parse = (input: string, config?: Config): AST | Error => { - if(!input) { +export const parse = (input: string, config?: any): AST | Error => { + if (!input) { return { root: "", kind: "error", @@ -56,7 +55,7 @@ export const parse = (input: string, config?: Config): AST | Error => { let parsedCandidateVariants: Variant[] = [] for (let i = variants.length - 1; i >= 0; --i) { - let parsedVariant = parseVariant(variants[i], theme.screens as ScreensConfig) + let parsedVariant = parseVariant(variants[i], theme.screens as any) if (parsedVariant !== null) parsedCandidateVariants.push(parsedVariant) } @@ -112,12 +111,12 @@ export const parse = (input: string, config?: Config): AST | Error => { if (valueWithoutModifier && valueWithoutModifier[0] === '[' && valueWithoutModifier[valueWithoutModifier.length - 1] === ']') { let arbitraryValue = valueWithoutModifier.slice(1, -1) - const unitType = inferDataType(arbitraryValue, [...availablePlugins.values()].map(({type}) => type)) + const unitType = inferDataType(arbitraryValue, [...availablePlugins.values()].map(({ type }) => type)) let associatedPluginByType = availablePlugins!.find(plugin => plugin.type === unitType) if (unitType === "color") { const color = CalculateHexFromString(arbitraryValue) - if(!color){ + if (!color) { return { root: base, kind: "error", @@ -125,10 +124,10 @@ export const parse = (input: string, config?: Config): AST | Error => { } } valueWithoutModifier = findTailwindColorFromHex(color.hex, theme[associatedPluginByType?.scaleKey || "colors"]) || color.hex - }else{ + } else { //It's not color, but it's still an arbitrary. We are just going to do parse it //The result might not be correct, but it's better than nothing and even tailwind will parse it anyway - if(availablePlugins.length > 0){ + if (availablePlugins.length > 0) { associatedPluginByType = availablePlugins.find(x => x.type === unitType) || availablePlugins.find(x => x.type !== "color") } } @@ -159,7 +158,7 @@ export const parse = (input: string, config?: Config): AST | Error => { } //check value against each scale of available plugins - let matchedPlugin = availablePlugins.find(({scaleKey}) => value.split('-')[0] in theme[scaleKey] || valueWithoutModifier in theme[scaleKey]) + let matchedPlugin = availablePlugins.find(({ scaleKey }) => value.split('-')[0] in theme[scaleKey] || valueWithoutModifier in theme[scaleKey]) if (!matchedPlugin) { return { root: base, diff --git a/src/theme.ts b/src/theme.ts index 279a2be..a135147 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,12 +1,6 @@ -import type {Config} from "tailwindcss/types/config" -import resolveConfig from 'tailwindcss/resolveConfig'; import memoize from 'lodash/memoize' - -// fuck you TS. I don't want to deal with this, this return a Theme instance but fuck the interface they've put into it. -// for now this ANY will do. -// @ts-ignore IT STUBS ANYWAY -export const getTailwindTheme = memoize((config: Config | undefined = {}) : any => { - const parsedConfig = resolveConfig(config || {}) - return parsedConfig.theme -}) \ No newline at end of file +export const getTailwindTheme = memoize((config: any | undefined = {}): any => { + const parsedConfig = config || {} + return parsedConfig.theme || {} +}) diff --git a/src/utils/build-modifier.js b/src/utils/build-modifier.js new file mode 100644 index 0000000..33c8f6b --- /dev/null +++ b/src/utils/build-modifier.js @@ -0,0 +1,38 @@ +import { StringBuilder } from './string-builder'; + +/* + * possible modifiers + * 80% 0.80 80 32 + * ^^ this is not valid + * value must either between 0-1 or between 0-100 + * with multiples of 5 or has % at the end. + */ + +export const buildModifier = (modifier, opacityScale = {}) => { + if (!modifier) return '' + + if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') { + modifier = modifier.slice(1, -1) + } + + if (modifier[modifier.length - 1] === '%') { + return StringBuilder.makeArbitrary(modifier) + } + + for (const [key, value] of Object.entries(opacityScale)) { + if (key === modifier || value === modifier) { + return key + } + } + + if ((Number(modifier) === 0 || Number(modifier) >= 1) && Number(modifier) <= 100) { + return StringBuilder.makeArbitrary(`${modifier}%`) + } + + if (Number(modifier) >= 0 && Number(modifier) <= 1) { + // we have number between 0-1 but it's not in the scale just make it arbitrary. + return StringBuilder.makeArbitrary(modifier) + } + + return '' +} diff --git a/src/utils/build-modifier.ts b/src/utils/build-modifier.ts deleted file mode 100644 index 0553600..0000000 --- a/src/utils/build-modifier.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {StringBuilder} from "./string-builder"; - -/* - * possible modifiers - * 80% 0.80 80 32 - * ^^ this is not valid - * value must either between 0-1 or between 0-100 - * with multiples of 5 or has % at the end. - */ - -export const buildModifier = (modifier: string | undefined, opacityScale: any): string => { - if (!modifier) return "" - - if(modifier[0] === "[" && modifier[modifier.length - 1] === "]") { - modifier = modifier.slice(1, -1) - } - - if(modifier[modifier.length - 1] === '%'){ - return StringBuilder.makeArbitrary(modifier) - } - - for (let [key, value] of Object.entries(opacityScale)) { - if (key == modifier || value == modifier) { - return key - } - } - - if((Number(modifier) === 0 || Number(modifier) >= 1) && Number(modifier) <= 100) { - return StringBuilder.makeArbitrary(modifier + "%") - } - - if( Number(modifier) >= 0 && Number(modifier) <= 1) { - // we have number between 0-1 but it's not in the scale just make it arbitrary. - return StringBuilder.makeArbitrary(modifier) - } - - return "" -} \ No newline at end of file diff --git a/src/utils/calculate-hex-from-string.ts b/src/utils/calculate-hex-from-string.ts index 10a3ac2..966ab96 100644 --- a/src/utils/calculate-hex-from-string.ts +++ b/src/utils/calculate-hex-from-string.ts @@ -1,15 +1,15 @@ -import {colord} from "colord"; +import { colord } from 'colord'; -export const CalculateHexFromString = (input: string): { hex: string, alpha: string | undefined } | undefined => { +export const calculateHexFromString = (input: string): { hex: string, alpha: string | undefined } | undefined => { const color = colord(input) const alpha = color.alpha() - if(!color.isValid()){ + if (!color.isValid()) { return undefined } return { hex: color.alpha(1).toHex(), - alpha: alpha !== 1 ? alpha.toString() : undefined + alpha: alpha !== 1 ? alpha.toString() : undefined, } -} \ No newline at end of file +} diff --git a/src/utils/find-tailwind-color-from-hex.js b/src/utils/find-tailwind-color-from-hex.js new file mode 100644 index 0000000..b485013 --- /dev/null +++ b/src/utils/find-tailwind-color-from-hex.js @@ -0,0 +1,19 @@ +import { colord } from 'colord'; + +export const findTailwindColorFromHex = (colorInput, colors = {}) => { + if (!colorInput) return false + + for (const [key, twColors] of Object.entries(colors)) { + if ((twColors === '#fff' || twColors === '#000') && colord(colorInput).isEqual(twColors)) { + return key + } else { + for (const [shade, hex] of Object.entries(twColors)) { + if (hex === colorInput) { + return `${key}-${shade}` + } + } + } + } + + return false +} diff --git a/src/utils/find-tailwind-color-from-hex.ts b/src/utils/find-tailwind-color-from-hex.ts deleted file mode 100644 index b28445a..0000000 --- a/src/utils/find-tailwind-color-from-hex.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {colord} from "colord"; - -export const findTailwindColorFromHex = (colorInput: string | undefined, colors: any) => { - if (!colorInput) return false - - //#fff, #000 or long white and black hexes safe words - for (let [key, twColors] of Object.entries(colors)) { - if((twColors === "#fff" || twColors === "#000") && colord(colorInput).isEqual(twColors)) { - return key - }else { - for (let [shade, hex] of Object.entries(twColors as [string, string])) { - if (hex === colorInput) { - return `${key}-${shade}` - } - } - } - } - - return false -} \ No newline at end of file diff --git a/src/utils/is-color.ts b/src/utils/is-color.ts index ff76277..aa11b35 100644 --- a/src/utils/is-color.ts +++ b/src/utils/is-color.ts @@ -1,5 +1,4 @@ -import type {CustomThemeConfig} from "tailwindcss/types/config" -import {segment} from "./segment"; +import { segment } from "./segment"; import get from "lodash/get"; const HASH = 0x23 @@ -201,7 +200,7 @@ const NAMED_COLORS = new Set([ const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix)\(/i -export function isColor(value: string, theme?: CustomThemeConfig): boolean { +export function isColor(value: string, theme?: any): boolean { if (!value) return false let isThemeColor = false diff --git a/src/utils/string-builder.js b/src/utils/string-builder.js new file mode 100644 index 0000000..238e1e2 --- /dev/null +++ b/src/utils/string-builder.js @@ -0,0 +1,60 @@ +export class StringBuilder { + _classRoot = '' + _classValue = '' + _modifier = '' + _variants = [] + important = false + negative = false + + addRoot(str) { + this._classRoot = str + return this + } + + addValue(str) { + this._classValue = str + return this + } + + appendModifier(modifier) { + if (modifier) { + this._modifier = `/${modifier}` + } + return this + } + + appendVariants(...variants) { + for (const variant of variants) { + this._variants.push(variant) + } + return this + } + + makeImportant() { + this.important = true + return this + } + + makeNegative() { + this.negative = true + return this + } + + toString() { + const variantOrder = ['media', 'system', 'interaction', 'pseudo', 'content', 'form', 'state', 'misc'] + const _sortedVariants = this._variants.sort((a, b) => variantOrder.indexOf(a.type) - variantOrder.indexOf(b.type)) + + return (_sortedVariants.length > 0 ? `${_sortedVariants.map(x => x.value).join(':')}:` : '') + (this.important ? '!' : '') + (this.negative ? '-' : '') + this._classRoot + (this._classValue ? `-${this._classValue}` : '') + this._modifier + } + + static makeInputArbitraryFormat(input) { + return input.replace(/\s/g, '_') + } + + static makeArbitrary(input) { + if (input.includes('url') || /[0-9]/.test(input) && (!input.includes('-') || !input.indexOf('-'))) { + return `[${StringBuilder.makeInputArbitraryFormat(input)}]` + } + return input + } +} diff --git a/src/utils/string-builder.ts b/src/utils/string-builder.ts deleted file mode 100644 index 7399950..0000000 --- a/src/utils/string-builder.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type {Variant} from "../plugins"; - -export class StringBuilder { - private _classRoot: string = "" - private _classValue: string = "" - private _modifier: string = "" - private _variants: Variant[] = [] - private important: boolean = false - private negative: boolean = false - - public addRoot(str: string) { - this._classRoot = str - return this - } - - public addValue(str: string) { - this._classValue = str - return this - } - - public appendModifier(modifier: string) { - if (modifier) { - this._modifier = "/" + modifier - } - return this - } - - public appendVariants(...variants: Variant[]) { - for (const variant of variants) { - this._variants.push(variant) - } - return this - } - - public makeImportant() { - this.important = true - return this - } - - public makeNegative() { - this.negative = true - return this - } - - public toString() { - const variantOrder: Variant["type"][] = ["media", 'system', "interaction", "pseudo", "content", "form", "state", "misc"] - const _sortedVariants = this._variants.sort((a, b) => variantOrder.indexOf(a.type) - variantOrder.indexOf(b.type)) - - return (_sortedVariants.length > 0 ? _sortedVariants.map(x => x.value).join(':') + ':' : '') + - (this.important ? "!" : "") + - (this.negative ? "-" : "") + - this._classRoot + - (this._classValue ? "-" + this._classValue : "") + - this._modifier - - } - - static makeInputArbitraryFormat(input: string): string { - //replace whitespaces with underscores - return input.replace(/\s/g, '_') - } - - static makeArbitrary(input: string) { - return "[" + StringBuilder.makeInputArbitraryFormat(input) + "]" - } -} \ No newline at end of file From 38e057baa8bd2080eb6a79c3ef7db16b2a5204ec Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Wed, 5 Nov 2025 16:13:03 -0300 Subject: [PATCH 2/7] remove theme config from classname --- package.json | 2 +- src/classname.js | 27 ++++++++++----------------- src/parse.ts | 4 ++-- src/utils/is-color.ts | 11 ++--------- src/utils/string-builder.js | 2 +- 5 files changed, 16 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index c44becb..3e45b9b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "inan-tailwind-parser", + "name": "tailwind-parser", "private": false, "version": "1.1.20", "type": "module", diff --git a/src/classname.js b/src/classname.js index f3377b1..208d322 100644 --- a/src/classname.js +++ b/src/classname.js @@ -1,8 +1,6 @@ -import get from 'lodash/get' - import { PluginNotFoundException } from './exceptions/plugin-not-found-exception' import { functionalPlugins, namedPlugins } from './plugins' -import { getTailwindTheme } from './theme' + import { buildModifier } from './utils/build-modifier' import { calculateHexFromString } from './utils/calculate-hex-from-string' import { findTailwindColorFromHex } from './utils/find-tailwind-color-from-hex' @@ -11,8 +9,11 @@ import { StringBuilder } from './utils/string-builder' export const EMPTY_CLASSNAME = '' -export const classname = (ast, config) => { - const theme = getTailwindTheme(config) +export const classname = (ast) => { + if ([null, undefined, ''].includes(ast.value)) { + return EMPTY_CLASSNAME + } + const stringBuilder = new StringBuilder() let negative = ast.negative || false stringBuilder.appendVariants(...ast.variants || []) @@ -40,20 +41,12 @@ export const classname = (ast, config) => { } stringBuilder.addRoot(root) - if (isColor(ast.value, theme)) { + if (isColor(ast.value)) { const matchedPlugin = possiblePlugins.find(plugin => plugin.type === 'color') if (!matchedPlugin) { throw new PluginNotFoundException(ast.property) } - const tailwindThemeColor = get(theme[matchedPlugin.scaleKey || 'colors'], ast.value.split('-').join('.')) - if (tailwindThemeColor && typeof tailwindThemeColor !== 'object') { - return stringBuilder - .appendModifier(buildModifier(ast.modifier, theme.opacity)) - .addValue(ast.value) - .toString() - } - const isRgba = ast.value.includes('rgba') if (isRgba) { return stringBuilder @@ -67,9 +60,9 @@ export const classname = (ast, config) => { } return stringBuilder - .appendModifier(buildModifier(color.alpha || ast.modifier, theme.opacity)) + .appendModifier(buildModifier(color.alpha || ast.modifier)) .addValue( - findTailwindColorFromHex(color.hex, theme[matchedPlugin.scaleKey || 'colors']) || StringBuilder.makeArbitrary(color.hex), + findTailwindColorFromHex(color.hex) || StringBuilder.makeArbitrary(color.hex), ) .toString() } @@ -96,7 +89,7 @@ const findTailwindValueByUnit = (unit, matchedPlugin = {}) => { return undefined } - if (matchedPlugin.type === 'image') { + if (matchedPlugin.type === 'image' && !unit.includes('url')) { unit = `url(http://github.com/$%7Bunit%7D)` } diff --git a/src/parse.ts b/src/parse.ts index 7c527f3..b0a50f8 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,7 +5,7 @@ import { parseVariant } from "./parse-variant"; import { inferDataType } from "./utils/infer-data-type"; import { getValue, type Value } from "./utils/value"; import { getTailwindTheme } from "./theme"; -import { CalculateHexFromString } from "./utils/calculate-hex-from-string"; +import { calculateHexFromString } from "./utils/calculate-hex-from-string"; import { findTailwindColorFromHex } from "./utils/find-tailwind-color-from-hex"; import { buildModifier } from "./utils/build-modifier"; import { isColor } from "./utils/is-color"; @@ -115,7 +115,7 @@ export const parse = (input: string, config?: any): AST | Error => { let associatedPluginByType = availablePlugins!.find(plugin => plugin.type === unitType) if (unitType === "color") { - const color = CalculateHexFromString(arbitraryValue) + const color = calculateHexFromString(arbitraryValue) if (!color) { return { root: base, diff --git a/src/utils/is-color.ts b/src/utils/is-color.ts index aa11b35..76184b7 100644 --- a/src/utils/is-color.ts +++ b/src/utils/is-color.ts @@ -200,17 +200,10 @@ const NAMED_COLORS = new Set([ const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix)\(/i -export function isColor(value: string, theme?: any): boolean { +export function isColor(value: string): boolean { if (!value) return false - let isThemeColor = false - - if (theme) { - const [trueValue,] = segment(value, '/') - isThemeColor = !!get(theme.colors as any, trueValue.split('-').join('.')) - } - return ( - value.charCodeAt(0) === HASH || IS_COLOR_FN.test(value) || NAMED_COLORS.has(value.toLowerCase()) || isThemeColor + value.charCodeAt(0) === HASH || IS_COLOR_FN.test(value) || NAMED_COLORS.has(value.toLowerCase()) ) } diff --git a/src/utils/string-builder.js b/src/utils/string-builder.js index 238e1e2..a7e4ba5 100644 --- a/src/utils/string-builder.js +++ b/src/utils/string-builder.js @@ -52,7 +52,7 @@ export class StringBuilder { } static makeArbitrary(input) { - if (input.includes('url') || /[0-9]/.test(input) && (!input.includes('-') || !input.indexOf('-'))) { + if (input.includes('(') || /[0-9]/.test(input) && (!input.includes('-') || !input.indexOf('-'))) { return `[${StringBuilder.makeInputArbitraryFormat(input)}]` } return input From 720401ceb0652e45dbe119e7f3330b2d46a86e0b Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Tue, 11 Nov 2025 13:47:34 -0300 Subject: [PATCH 3/7] review --- package.json | 18 +- .../incorrect-value-type-exception.ts | 6 - src/exceptions/unmatched-value-exception.ts | 6 - src/find-root.ts | 40 -- src/index.ts | 4 - src/parse-variant.ts | 35 -- src/parse.ts | 184 ------ src/plugins.ts | 561 ------------------ src/{ => tailwind}/classname.js | 22 +- .../exceptions/plugin-not-found-exception.js} | 4 +- .../exceptions/unmatched-value-exception.js | 6 + src/tailwind/index.js | 2 + src/tailwind/parse.js | 137 +++++ src/tailwind/plugins.js | 537 +++++++++++++++++ src/tailwind/utils/build-modifier.js | 30 + .../utils/calculate-hex-from-string.js} | 2 +- src/tailwind/utils/decode-arbitrary-value.js | 30 + src/tailwind/utils/find-root.js | 23 + .../utils/find-tailwind-color-from-hex.js | 19 + src/tailwind/utils/infer-data-type.js | 235 ++++++++ .../utils/is-color.js} | 9 +- src/tailwind/utils/math-operators.js | 104 ++++ src/tailwind/utils/parse-variant.js | 28 + src/tailwind/utils/segment.js | 75 +++ src/{ => tailwind}/utils/string-builder.js | 0 src/theme.ts | 6 - src/utils/build-modifier.js | 38 -- src/utils/decodeArbitraryValue.ts | 43 -- src/utils/find-tailwind-color-from-hex.js | 19 - src/utils/infer-data-type.ts | 323 ---------- src/utils/math-operators.ts | 154 ----- src/utils/segment.ts | 102 ---- src/utils/value.ts | 43 -- 33 files changed, 1241 insertions(+), 1604 deletions(-) delete mode 100644 src/exceptions/incorrect-value-type-exception.ts delete mode 100644 src/exceptions/unmatched-value-exception.ts delete mode 100644 src/find-root.ts delete mode 100644 src/index.ts delete mode 100644 src/parse-variant.ts delete mode 100644 src/parse.ts delete mode 100644 src/plugins.ts rename src/{ => tailwind}/classname.js (81%) rename src/{exceptions/plugin-not-found-exception.ts => tailwind/exceptions/plugin-not-found-exception.js} (89%) create mode 100644 src/tailwind/exceptions/unmatched-value-exception.js create mode 100644 src/tailwind/index.js create mode 100644 src/tailwind/parse.js create mode 100644 src/tailwind/plugins.js create mode 100644 src/tailwind/utils/build-modifier.js rename src/{utils/calculate-hex-from-string.ts => tailwind/utils/calculate-hex-from-string.js} (70%) create mode 100644 src/tailwind/utils/decode-arbitrary-value.js create mode 100644 src/tailwind/utils/find-root.js create mode 100644 src/tailwind/utils/find-tailwind-color-from-hex.js create mode 100644 src/tailwind/utils/infer-data-type.js rename src/{utils/is-color.ts => tailwind/utils/is-color.js} (94%) create mode 100644 src/tailwind/utils/math-operators.js create mode 100644 src/tailwind/utils/parse-variant.js create mode 100644 src/tailwind/utils/segment.js rename src/{ => tailwind}/utils/string-builder.js (100%) delete mode 100644 src/theme.ts delete mode 100644 src/utils/build-modifier.js delete mode 100644 src/utils/decodeArbitraryValue.ts delete mode 100644 src/utils/find-tailwind-color-from-hex.js delete mode 100644 src/utils/infer-data-type.ts delete mode 100644 src/utils/math-operators.ts delete mode 100644 src/utils/segment.ts delete mode 100644 src/utils/value.ts diff --git a/package.json b/package.json index 3e45b9b..40221ab 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,8 @@ { "name": "tailwind-parser", - "private": false, - "version": "1.1.20", + "version": "0.1.0", + "private": true, "type": "module", - "author": "T. Cem Yılmaz ", - "license": "WTFPL", - "keywords": [ - "tailwindcss", - "parser", - "utils", - "ast", - "tailwind js object" - ], - "homepage": "https://github.com/XEngine/tailwindcss-class-parser", "scripts": { "dev": "vite", "build": "vite build", @@ -32,9 +22,7 @@ "tailwindcss": "*" }, "devDependencies": { - "@types/lodash": "^4.17.12", "@types/node": "^22.7.7", - "lodash": "^4.17.21", "typescript": "^5.6.3", "vite": "^5.4.9", "vite-plugin-dts": "^4.2.4", @@ -46,4 +34,4 @@ "dependencies": { "colord": "^2.9.3" } -} +} \ No newline at end of file diff --git a/src/exceptions/incorrect-value-type-exception.ts b/src/exceptions/incorrect-value-type-exception.ts deleted file mode 100644 index 05ada8c..0000000 --- a/src/exceptions/incorrect-value-type-exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class IncorrectValueTypeException extends Error { - constructor(matchedPluginNamespace: string, type: string) { - super(`The specified type is not valid for the matched plugins: ${matchedPluginNamespace}. Please ensure the type ${type || "undefined"} is one of the supported types for the plugins.`); - this.name = 'PluginNotFoundException'; - } -} \ No newline at end of file diff --git a/src/exceptions/unmatched-value-exception.ts b/src/exceptions/unmatched-value-exception.ts deleted file mode 100644 index 6bf57c8..0000000 --- a/src/exceptions/unmatched-value-exception.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class UnmatchedValueException extends Error { - constructor(plugin: string, value: string) { - super(`Found a matched plugin \`${plugin}\`, but the value \`${value}\` could not matched. This could be due to a misspelling or the given value is not covered by tailwind.config file.`); - this.name = 'PluginNotFoundException'; - } -} \ No newline at end of file diff --git a/src/find-root.ts b/src/find-root.ts deleted file mode 100644 index 88a32ab..0000000 --- a/src/find-root.ts +++ /dev/null @@ -1,40 +0,0 @@ -export function findRoot( - input: string, - lookup: { has: (input: string) => boolean }, -): [string | null, string | null] { - // If the lookup has an exact match, then that's the root. - if (lookup.has(input)) return [input, null] - - // Otherwise test every permutation of the input by iteratively removing - // everything after the last dash. - let idx = input.lastIndexOf('-') - if (idx === -1) { - // Variants starting with `@` are special because they don't need a `-` - // after the `@` (E.g.: `@-lg` should be written as `@lg`). - if (input[0] === '@' && lookup.has('@')) { - return ['@', input.slice(1)] - } - - return [null, null] - } - - // Determine the root and value by testing permutations of the incoming input - // against the lookup table. - // - // In case of a candidate like `bg-red-500`, this looks like: - // - // `bg-red-500` -> No match - // `bg-red` -> No match - // `bg` -> Match - do { - let maybeRoot = input.slice(0, idx) - if (lookup.has(maybeRoot)) { - - return [maybeRoot, input.slice(idx + 1)] - } - - idx = input.lastIndexOf('-', idx - 1) - } while (idx > 0) - - return [null, null] -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 493bd6a..0000000 --- a/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { classname } from './classname'; -import { parse } from './parse'; - -export { parse, classname } diff --git a/src/parse-variant.ts b/src/parse-variant.ts deleted file mode 100644 index 7fa16de..0000000 --- a/src/parse-variant.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { decodeArbitraryValue } from "./utils/decodeArbitraryValue"; -import { type Variant, variants } from "./plugins"; - -export function parseVariant(variant: string, screens: any): Variant { - if (variant[0] === '[' && variant[variant.length - 1] === ']') { - let arbitraryValue = variant.slice(1, -1) - - if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') { - arbitraryValue = `var(${arbitraryValue})` - } else { - arbitraryValue = decodeArbitraryValue(arbitraryValue) - } - - return { - kind: 'arbitrary', - type: 'misc', - value: arbitraryValue, - } - } - - if (Object.keys(screens).includes(variant)) { - return { - kind: 'named', - type: 'media', - value: variant - } - } - - const matchedVariantType = variants.get(variant) - return { - kind: 'named', - type: matchedVariantType || 'misc', - value: variant, - } -} \ No newline at end of file diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index b0a50f8..0000000 --- a/src/parse.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { segment } from "./utils/segment"; -import { findRoot } from "./find-root"; -import { type FunctionalPlugin, functionalPlugins, namedPlugins, type Variant } from "./plugins"; -import { parseVariant } from "./parse-variant"; -import { inferDataType } from "./utils/infer-data-type"; -import { getValue, type Value } from "./utils/value"; -import { getTailwindTheme } from "./theme"; -import { calculateHexFromString } from "./utils/calculate-hex-from-string"; -import { findTailwindColorFromHex } from "./utils/find-tailwind-color-from-hex"; -import { buildModifier } from "./utils/build-modifier"; -import { isColor } from "./utils/is-color"; -import { decodeArbitraryValue } from "./utils/decodeArbitraryValue"; - -export type State = { - important: boolean - negative: boolean -} - -export type AST = { - root: string - kind: "named" | "functional" - property: string - value: string - valueDef: Value - variants: Variant[] - modifier: string | null, - important: boolean - negative: boolean, - arbitrary: boolean -} - -export type Error = { - root: string - kind: "error" - message: string -} - -export const parse = (input: string, config?: any): AST | Error => { - if (!input) { - return { - root: "", - kind: "error", - message: "Empty input" - } - } - - const theme = getTailwindTheme(config) - let state: State = { - important: false, - negative: false - } - const variants = segment(input, ':') - let base = variants.pop()! - - let parsedCandidateVariants: Variant[] = [] - - for (let i = variants.length - 1; i >= 0; --i) { - let parsedVariant = parseVariant(variants[i], theme.screens as any) - if (parsedVariant !== null) - parsedCandidateVariants.push(parsedVariant) - } - - if (base[0] === '!') { - state.important = true - base = base.slice(1) - } - - if (base[0] === '-') { - state.negative = true - base = base.slice(1) - } - - const namedPlugin = namedPlugins.get(base) - if (namedPlugin) { - return { - root: base, - kind: "named", - property: namedPlugin!.ns, - value: namedPlugin.value, - valueDef: { - class: namedPlugin.class, - raw: base, - kind: "named", - value: namedPlugin.value, - }, - variants: parsedCandidateVariants, - modifier: null, - important: state.important, - negative: state.negative, - arbitrary: false - } - } - - let [root, value] = findRoot(base, functionalPlugins) - - if (!root) { - //throw new PluginNotFoundException(base) - return { - root: base, - kind: "error", - message: "Tailwindcss core plugin not found", - } - } - - const availablePlugins = functionalPlugins.get(root) as FunctionalPlugin[] - let modifier: string | null = null - let [valueWithoutModifier, modifierSegment = null] = segment(value || "", '/') - if (modifierSegment && isColor(valueWithoutModifier.replace(/[\[\]]/g, ""), theme)) { - modifier = buildModifier(modifierSegment, theme.opacity) - } - - if (valueWithoutModifier && valueWithoutModifier[0] === '[' && valueWithoutModifier[valueWithoutModifier.length - 1] === ']') { - let arbitraryValue = valueWithoutModifier.slice(1, -1) - const unitType = inferDataType(arbitraryValue, [...availablePlugins.values()].map(({ type }) => type)) - let associatedPluginByType = availablePlugins!.find(plugin => plugin.type === unitType) - - if (unitType === "color") { - const color = calculateHexFromString(arbitraryValue) - if (!color) { - return { - root: base, - kind: "error", - message: "Color is not correct", - } - } - valueWithoutModifier = findTailwindColorFromHex(color.hex, theme[associatedPluginByType?.scaleKey || "colors"]) || color.hex - } else { - //It's not color, but it's still an arbitrary. We are just going to do parse it - //The result might not be correct, but it's better than nothing and even tailwind will parse it anyway - if (availablePlugins.length > 0) { - associatedPluginByType = availablePlugins.find(x => x.type === unitType) || availablePlugins.find(x => x.type !== "color") - } - } - - arbitraryValue = decodeArbitraryValue(arbitraryValue) - - return { - root: root, - kind: "functional", - property: associatedPluginByType!.ns, - value: arbitraryValue, - valueDef: { - value: arbitraryValue, - class: associatedPluginByType!.class, - raw: valueWithoutModifier, - kind: unitType || "named" - }, - variants: parsedCandidateVariants, - modifier: modifier, - arbitrary: true, - important: state.important, - negative: state.negative - } - } - - if (!value) { - value = 'DEFAULT' - } - - //check value against each scale of available plugins - let matchedPlugin = availablePlugins.find(({ scaleKey }) => value.split('-')[0] in theme[scaleKey] || valueWithoutModifier in theme[scaleKey]) - if (!matchedPlugin) { - return { - root: base, - kind: "error", - message: `found "${availablePlugins.map(x => x.ns).join(', ')}" plugins but unable to determine which one is matched to given value "${value}".`, - } - } - - const val = getValue(matchedPlugin.type === "color" ? valueWithoutModifier : value, matchedPlugin, theme[matchedPlugin.scaleKey]) - - return { - root: root, - kind: "functional", - property: matchedPlugin.ns, - value: val.value, - valueDef: val, - variants: parsedCandidateVariants, - modifier: modifier, - important: state.important, - negative: state.negative, - arbitrary: false, - } -} \ No newline at end of file diff --git a/src/plugins.ts b/src/plugins.ts deleted file mode 100644 index d2ffbfc..0000000 --- a/src/plugins.ts +++ /dev/null @@ -1,561 +0,0 @@ -import type {DataType} from "./utils/infer-data-type"; - -export type Variant = { - kind: 'arbitrary' | 'named', - type: 'opacity' | 'media' | 'pseudo' | 'content' | 'form' | 'state' | 'interaction' | 'misc' | 'system' - value: string -} - -export type FunctionalPlugin = { - scaleKey: string, - class: string[], - ns: string, - type: DataType, - supportNegative?: boolean -} - -export type NamedPlugin = { - value: string - class: string[], - ns: string, -} - -export const functionalPlugins = new Map([ - ["w", [ - {scaleKey: "width", ns: 'width', class: ['width'], type: 'length'}, - ]], - ["min-w", [ - {scaleKey: "minWidth", ns: 'minWidth', class: ['min-width'], type: 'length'}, - ]], - ["max-w", [ - {scaleKey: "maxWidth", ns: 'maxWidth', class: ['max-width'], type: 'length'}, - ]], - ["h", [ - {scaleKey: "height", ns: 'height', class: ['height'], type: 'length'}, - ]], - ["min-h", [ - {scaleKey: "minHeight", ns: 'minHeight', class: ['min-height'], type: 'length'}, - ]], - ["max-h", [ - {scaleKey: "maxHeight", ns: 'maxHeight', class: ['max-height'], type: 'length'}, - ]], - ["size", [ - {scaleKey: "size", ns: 'size', class: ['width', 'height'], type: 'length'}, - ]], - ["m", [ - {scaleKey: "margin", ns: 'margin', class: ['margin'], type: 'length', supportNegative: true}, - ]], - ["mx", [ - {scaleKey: "margin", ns: 'marginX', class: ['margin-left', 'margin-right'], type: 'length', supportNegative: true}, - ]], - ["my", [ - {scaleKey: "margin", ns: 'marginY', class: ['margin-top', 'margin-bottom'], type: 'length', supportNegative: true}, - ]], - ["mt", [ - {scaleKey: "margin", ns: 'marginTop', class: ['margin-top'], type: 'length', supportNegative: true}, - ]], - ["mr", [ - {scaleKey: "margin", ns: 'marginRight', class: ['margin-right'], type: 'length', supportNegative: true}, - ]], - ["mb", [ - {scaleKey: "margin", ns: 'marginBottom', class: ['margin-bottom'], type: 'length', supportNegative: true}, - ]], - ["ml", [ - {scaleKey: "margin", ns: 'marginLeft', class: ['margin-left'], type: 'length', supportNegative: true}, - ]], - ["p", [ - {scaleKey: "padding", ns: 'padding', class: ['padding'], type: 'length'}, - ]], - ["px", [ - {scaleKey: "padding", ns: 'paddingX', class: ['padding-left', 'padding-right'], type: 'length'}, - ]], - ["py", [ - {scaleKey: "padding", ns: 'paddingY', class: ['padding-top', 'padding-bottom'], type: 'length'}, - ]], - ["pt", [ - {scaleKey: "padding", ns: 'paddingTop', class: ['padding-top'], type: 'length'}, - ]], - ["pr", [ - {scaleKey: "padding", ns: 'paddingRight', class: ['padding-right'], type: 'length'}, - ]], - ["pb", [ - {scaleKey: "padding", ns: 'paddingBottom', class: ['padding-bottom'], type: 'length'}, - ]], - ["pl", [ - {scaleKey: "padding", ns: 'paddingLeft', class: ['padding-left'], type: 'length'}, - ]], - ["text", [ - {scaleKey: "fontSize", ns: 'fontSize', class: ['font-size'], type: 'length'}, - {scaleKey: "textColor", ns: 'textColor', class: ['color'], type: 'color'}, - ]], - ["indent", [ - {scaleKey: "textIndent", ns: 'textIndent', class: ['text-indent'], type: 'length', supportNegative: true}, - ]], - ["underline-offset", [ - {scaleKey: "textUnderlineOffset", ns: 'textUnderlineOffset', class: ['text-underline-offset'], type: 'number'}, - ]], - ["decoration", [ - {scaleKey: "textDecorationThickness", ns: 'textDecorationThickness', class: ['text-decoration-thickness'], type: 'number'}, - {scaleKey: "textDecorationColor", ns: 'textDecorationColor', class: ['text-decoration-color'], type: 'color'}, - ]], - ["font", [ - {scaleKey: "fontWeight", ns: 'fontWeight', class: ['font-weight'], type: 'number'}, - {scaleKey: "fontFamily", ns: 'fontFamily', class: ['font-family'], type: 'family-name'}, - ]], - ["leading", [ - {scaleKey: "lineHeight", ns: 'lineHeight', class: ['line-height'], type: 'number'}, - ]], - ["tracking", [ - {scaleKey: "letterSpacing", ns: 'letterSpacing', class: ['letter-spacing'], type: 'length', supportNegative: true}, - ]], - ["bg", [ - {scaleKey: "backgroundImage", ns: 'backgroundImage', class: ['background-image'], type: 'image'}, - {scaleKey: "backgroundPosition", ns: 'backgroundPosition', class: ['background-position'], type: 'position'}, - {scaleKey: "backgroundSize", ns: 'backgroundSize', class: ['background-size'], type: 'bg-size'}, - {scaleKey: "backgroundColor", ns: 'backgroundColor', class: ['background-color'], type: 'color'}, - ]], - ["from", [ - {scaleKey: "gradientColorStops", ns: 'gradientStopsFrom', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'color'}, - {scaleKey: "gradientColorStopPositions", ns: 'gradientStopsFromPosition', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage'}, - ]], - ["to", [ - {scaleKey: "gradientColorStops", ns: 'gradientStopsTo', class: ['--tw-gradient-to', '--tw-gradient-to-position'], type: 'color'}, - {scaleKey: "gradientColorStopPositions", ns: 'gradientStopsToPosition', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage'}, - ]], - ["via", [ - {scaleKey: "gradientColorStops", ns: 'gradientStopsVia', class: ['--tw-gradient-from'], type: 'color'}, - {scaleKey: "gradientColorStopPositions", ns: 'gradientStopsViaPositions', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage'}, - ]], - ["fill", [ - {scaleKey: "fill", ns: 'fillColor', class: ['fill'], type: 'color'}, - ]], - ["stroke", [ - {scaleKey: "strokeWidth", ns: 'strokeWidth', class: ['stroke-width'], type: 'line-width'}, - {scaleKey: "stroke", ns: 'strokeColor', class: ['border-color'], type: 'color'}, - ]], - ["border", [ - {scaleKey: "borderWidth", ns: 'borderWidth', class: ['border-width'], type: 'line-width'}, - {scaleKey: "borderColor", ns: 'borderColor', class: ['border-color'], type: 'color'}, - ]], - ["ring", [ - {scaleKey: "ringWidth", ns: 'ringWidth', class: ['box-shadow'], type: 'line-width'}, - {scaleKey: "ringColor", ns: 'ringColor', class: ['--tw-ring-color'], type: 'color'}, - ]], - ["ring-offset", [ - {scaleKey: "ringOffsetWidth", ns: 'ringOffsetWidth', class: ['--tw-ring-offset-width'], type: 'line-width'}, - {scaleKey: "ringOffsetColor", ns: 'ringOffsetColor', class: ['--tw-ring-offset-color'], type: 'color'}, - ]], - ["rounded", [ - {scaleKey: "borderRadius", ns: 'borderRadius', class: ['border-radius'], type: 'length'}, - ]], - ["rounded-tl", [ - {scaleKey: "borderRadius", ns: 'borderTopLeftRadius', class: ['border-top-left-radius'], type: 'length'}, - ]], - ["rounded-tr", [ - {scaleKey: "borderRadius", ns: 'borderTopRightRadius', class: ['border-top-right-radius'], type: 'length'}, - ]], - ["rounded-br", [ - {scaleKey: "borderRadius", ns: 'borderBottomRightRadius', class: ['border-bottom-right-radius'], type: 'length'}, - ]], - ["rounded-bl", [ - {scaleKey: "borderRadius", ns: 'borderBottomLeftRadius', class: ['border-bottom-left-radius'], type: 'length'}, - ]], - ["flex", [ - {scaleKey: "flex", ns: 'flex', class: ['flex'], type: 'number'}, - ]], - ["basis", [ - {scaleKey: "flexBasis", ns: 'flexBasis', class: ['flex-basis'], type: 'length'}, - ]], - ["grid-cols", [ - {scaleKey: "gridTemplateColumns", ns: 'gridTemplateColumns', class: ['grid-template-columns'], type: 'number'}, - ]], - ["grid-rows", [ - {scaleKey: "gridTemplateRows", ns: 'gridTemplateRows', class: ['grid-template-rows'], type: 'number'}, - ]], - ["gap", [ - {scaleKey: "gap", ns: 'gap', class: ['gap'], type: 'length'}, - ]], - ["gap-x", [ - {scaleKey: "gap", ns: 'gapX', class: ['gap'], type: 'length'}, - ]], - ["gap-y", [ - {scaleKey: "gap", ns: 'gapY', class: ['gap'], type: 'length'}, - ]], - ["divide-x", [ - {scaleKey: "divideWidth", ns: 'divideX', class: ['border-left-width', 'border-right-width'], type: 'length'}, - ]], - ["divide-y", [ - {scaleKey: "divideWidth", ns: 'divideY', class: ['border-top-width', 'border-bottom-width'], type: 'length'}, - ]], - ["divide", [ - {scaleKey: "divideColor", ns: 'divideColor', class: ['--tw-divide-color'], type: 'color'}, - ]], - ["divide-opacity", [ - {scaleKey: "divideOpacity", ns: 'divideOpacity', class: ['--tw-divide-opacity'], type: 'number'}, - ]], - ["col", [ - {scaleKey: "gridColumn", ns: 'gridColumn', class: ['grid-column'], type: 'number'}, - ]], - ["row", [ - {scaleKey: "gridRow", ns: 'gridRow', class: ['grid-row'], type: 'number'}, - ]], - ["space-x", [ - {scaleKey: "spacing", ns: 'spaceX', class: ['margin-left'], type: 'number', supportNegative: true}, - ]], - ["space-y", [ - {scaleKey: "spacing", ns: 'spaceY', class: ['margin-top'], type: 'number', supportNegative: true}, - ]], - ["opacity", [ - {scaleKey: "opacity", ns: 'opacity', class: ['opacity'], type: 'number'}, - ]], - ["shadow", [ - {scaleKey: "boxShadow", ns: 'boxShadow', class: ['box-shadow'], type: 'length'}, - {scaleKey: "boxShadowColor", ns: 'boxShadowColor', class: ['box-shadow'], type: 'color'}, - ]], - ["transition", [ - {scaleKey: "transitionProperty", ns: 'transitionProperty', class: ['transition-property'], type: 'named'}, - ]], - ["duration", [ - {scaleKey: "transitionDuration", ns: 'transitionDuration', class: ['transition-duration'], type: 'number'}, - ]], - ["delay", [ - {scaleKey: "transitionDelay", ns: 'transitionDelay', class: ['transition-delay'], type: 'number'}, - ]], - ["scale", [ - {scaleKey: "scale", ns: 'scale', class: ['scale'], type: 'number', supportNegative: true}, - ]], - ["rotate", [ - {scaleKey: "rotate", ns: 'rotate', class: ['rotate'], type: 'angle', supportNegative: true}, - ]], - ["translate", [ - {scaleKey: "translate", ns: 'translate', class: ['transform'], type: 'length', supportNegative: true}, - ]], - ["translate-y", [ - {scaleKey: "translate", ns: 'translateY', class: ['transform'], type: 'length', supportNegative: true}, - ]], - ["translate-x", [ - {scaleKey: "translate", ns: 'translateX', class: ['transform'], type: 'length', supportNegative: true}, - ]], - ["skew", [ - {scaleKey: "skew", ns: 'skew', class: ['skew'], type: 'angle', supportNegative: true}, - ]], - ["z", [ - {scaleKey: "zIndex", ns: 'zIndex', class: ['z-index'], type: 'number', supportNegative: true}, - ]], - ["inset", [ - {scaleKey: "inset", ns: 'inset', class: ['inset'], type: 'length', supportNegative: true}, - ]], - ["inset-x", [ - {scaleKey: "inset", ns: 'insetX', class: ['left', 'right'], type: 'length', supportNegative: true}, - ]], - ["inset-y", [ - {scaleKey: "inset", ns: 'insetY', class: ['top', 'bottom'], type: 'length', supportNegative: true}, - ]], - ["top", [ - {scaleKey: "inset", ns: 'positionTop', class: ['top'], type: 'length', supportNegative: true}, - ]], - ["right", [ - {scaleKey: "inset", ns: 'positionRight', class: ['right'], type: 'length', supportNegative: true}, - ]], - ["bottom", [ - {scaleKey: "inset", ns: 'positionBottom', class: ['bottom'], type: 'length', supportNegative: true}, - ]], - ["left", [ - {scaleKey: "inset", ns: 'positionLeft', class: ['left'], type: 'length', supportNegative: true}, - ]], - ["start", [ - {scaleKey: "inset", ns: 'insetInlineStart', class: ['inset-inline-start'], type: 'length', supportNegative: true}, - ]], - ["end", [ - {scaleKey: "inset", ns: 'insetInlineEnd', class: ['inset-inline-end'], type: 'length', supportNegative: true}, - ]], - ["order", [ - {scaleKey: "order", ns: 'order', class: ['order'], type: 'length', supportNegative: true}, - ]], - ["blur", [ - {scaleKey: "blur", ns: 'blur', class: ['filter'], type: 'number'}, - ]], - ["brightness", [ - {scaleKey: "brightness", ns: 'brightness', class: ['filter'], type: 'number'}, - ]], - ["contrast", [ - {scaleKey: "contrast", ns: 'contrast', class: ['filter'], type: 'number'}, - ]], - ["drop-shadow", [ - {scaleKey: "dropShadow", ns: 'dropShadow', class: ['filter'], type: 'number'}, - ]], - ["hue-rotate", [ - {scaleKey: "hueRotate", ns: 'hueRotate', class: ['filter'], type: 'number'}, - ]], - ["saturate", [ - {scaleKey: "saturate", ns: 'saturate', class: ['filter'], type: 'number'}, - ]], - ["backdrop-blur", [ - {scaleKey: "blur", ns: 'blur', class: ['filter'], type: 'number'}, - ]], - ["backdrop-brightness", [ - {scaleKey: "backdropBrightness", ns: 'backdropBrightness', class: ['backdrop-filter'], type: 'number'}, - ]], - ["backdrop-contrast", [ - {scaleKey: "backdropContrast", ns: 'backdropContrast', class: ['backdrop-filter'], type: 'number'}, - ]], - ["backdrop-drop-shadow", [ - {scaleKey: "backdropDropShadow", ns: 'backdropDropShadow', class: ['backdrop-filter'], type: 'number'}, - ]], - ["backdrop-hue-rotate", [ - {scaleKey: "backdropHueRotate", ns: 'backdropHueRotate', class: ['backdrop-filter'], type: 'number'}, - ]], - ["backdrop-saturate", [ - {scaleKey: "backdropSaturate", ns: 'backdropSaturate', class: ['backdrop-filter'], type: 'number'}, - ]], -]); - -export const namedPlugins = new Map([ - // Border Styles - ["border-solid", {class: ['border-style'], value: 'solid', ns: 'borderStyle'}], - ["border-dashed", {class: ['border-style'], value: 'dashed', ns: 'borderStyle'}], - ["border-dotted", {class: ['border-style'], value: 'dotted', ns: 'borderStyle'}], - ["border-double", {class: ['border-style'], value: 'double', ns: 'borderStyle'}], - ["border-none", {class: ['border-style'], value: 'none', ns: 'borderStyle'}], - ["ring-inset", {class: ['--tw-ring-inset'], value: 'inset', ns: 'ring'}], - - ["divide-solid", {class: ['divide-style'], value: 'solid', ns: 'divideStyle'}], - ["divide-dashed", {class: ['divide-style'], value: 'dashed', ns: 'divideStyle'}], - ["divide-dotted", {class: ['divide-style'], value: 'dotted', ns: 'divideStyle'}], - ["divide-double", {class: ['divide-style'], value: 'double', ns: 'divideStyle'}], - ["divide-none", {class: ['divide-style'], value: 'none', ns: 'divideStyle'}], - - // Display - ["block", {class: ['display'], value: 'block', ns: 'display'}], - ["inline-block", {class: ['display'], value: 'inline-block', ns: 'display'}], - ["inline", {class: ['display'], value: 'inline', ns: 'display'}], - ["flex", {class: ['display'], value: 'flex', ns: 'display'}], - ["grid", {class: ['display'], value: 'grid', ns: 'display'}], - ["hidden", {class: ['display'], value: 'none', ns: 'display'}], - ["table", {class: ['display'], value: 'table', ns: 'display'}], - ["table-row", {class: ['display'], value: 'table-row', ns: 'display'}], - ["table-cell", {class: ['display'], value: 'table-cell', ns: 'display'}], - ["contents", {class: ['display'], value: 'contents', ns: 'display'}], - ["list-item", {class: ['display'], value: 'list-item', ns: 'display'}], - ["flow-root", {class: ['display'], value: 'flow-root', ns: 'display'}], - - // Visibility - ["visible", {class: ['visibility'], value: 'visible', ns: 'visibility'}], - ["invisible", {class: ['visibility'], value: 'hidden', ns: 'visibility'}], - - // Hyphens - ["hyphens-none", {class: ['hyphens'], value: 'none', ns: 'hyphens'}], - ["hyphens-manual", {class: ['hyphens'], value: 'manual', ns: 'hyphens'}], - ["hyphens-auto", {class: ['hyphens'], value: 'auto', ns: 'hyphens'}], - - // Word Break - ["break-normal", {class: ['word-break', 'overflow-wrap'], value: 'normal', ns: 'wordBreak'}], - ["break-words", {class: ['overflow-wrap'], value: 'break-word', ns: 'wordBreak'}], - ["break-all", {class: ['word-break'], value: 'break-all', ns: 'wordBreak'}], - ["break-keep", {class: ['word-break'], value: 'keep-all', ns: 'wordBreak'}], - - // Whitespace - ["whitespace-normal", {class: ['white-space'], value: 'normal', ns: 'whitespace'}], - ["whitespace-nowrap", {class: ['white-space'], value: 'nowrap', ns: 'whitespace'}], - ["whitespace-pre", {class: ['white-space'], value: 'pre', ns: 'whitespace'}], - ["whitespace-pre-line", {class: ['white-space'], value: 'pre-line', ns: 'whitespace'}], - ["whitespace-pre-wrap", {class: ['white-space'], value: 'pre-wrap', ns: 'whitespace'}], - ["whitespace-break-spaces", {class: ['white-space'], value: 'break-spaces', ns: 'whitespace'}], - - // Position - ["static", {class: ['position'], value: 'static', ns: 'position'}], - ["fixed", {class: ['position'], value: 'fixed', ns: 'position'}], - ["absolute", {class: ['position'], value: 'absolute', ns: 'position'}], - ["relative", {class: ['position'], value: 'relative', ns: 'position'}], - ["sticky", {class: ['position'], value: 'sticky', ns: 'position'}], - - // Overflow - ["overflow-auto", {class: ['overflow'], value: 'auto', ns: 'overflow'}], - ["overflow-hidden", {class: ['overflow'], value: 'hidden', ns: 'overflow'}], - ["overflow-visible", {class: ['overflow'], value: 'visible', ns: 'overflow'}], - ["overflow-scroll", {class: ['overflow'], value: 'scroll', ns: 'overflow'}], - ["overflow-x-auto", {class: ['overflow-x'], value: 'auto', ns: 'overflowX'}], - ["overflow-x-hidden", {class: ['overflow-x'], value: 'hidden', ns: 'overflowX'}], - ["overflow-x-visible", {class: ['overflow-x'], value: 'visible', ns: 'overflowX'}], - ["overflow-x-scroll", {class: ['overflow-x'], value: 'scroll', ns: 'overflowX'}], - ["overflow-y-auto", {class: ['overflow-y'], value: 'auto', ns: 'overflowY'}], - ["overflow-y-hidden", {class: ['overflow-y'], value: 'hidden', ns: 'overflowY'}], - ["overflow-y-visible", {class: ['overflow-y'], value: 'visible', ns: 'overflowY'}], - ["overflow-y-scroll", {class: ['overflow-y'], value: 'scroll', ns: 'overflowY'}], - - // Text Align - ["text-left", {class: ['text-align'], value: 'left', ns: 'textAlign'}], - ["text-center", {class: ['text-align'], value: 'center', ns: 'textAlign'}], - ["text-right", {class: ['text-align'], value: 'right', ns: 'textAlign'}], - ["text-justify", {class: ['text-align'], value: 'justify', ns: 'textAlign'}], - ["text-start", {class: ['text-align'], value: 'start', ns: 'textAlign'}], - ["text-end", {class: ['text-align'], value: 'end', ns: 'textAlign'}], - - // Text Decoration - ["underline", {class: ['text-decoration-line'], value: 'underline', ns: 'textDecoration'}], - ["line-through", {class: ['text-decoration-line'], value: 'line-through', ns: 'textDecoration'}], - ["overline", {class: ['text-decoration-line'], value: 'overline', ns: 'textDecoration'}], - ["no-underline", {class: ['text-decoration-line'], value: 'none', ns: 'textDecoration'}], - - // Text Wrap - ["text-wrap", {class: ['text-wrap'], value: 'wrap', ns: 'textWrap'}], - ["text-nowrap", {class: ['text-wrap'], value: 'nowrap', ns: 'textWrap'}], - ["text-balance", {class: ['text-wrap'], value: 'balance', ns: 'textWrap'}], - ["text-pretty", {class: ['text-wrap'], value: 'pretty', ns: 'textWrap'}], - - // Text Overflow - ["truncate", {class: ['overflow', 'text-overflow', 'white-space'], value: '', ns: 'TextOverflow'}], - ["text-ellipsis", {class: ['text-overflow'], value: 'ellipsis', ns: 'textWrap'}], - ["text-clip", {class: ['text-overflow'], value: 'clip', ns: 'textWrap'}], - - // Text Transform - ["uppercase", {class: ['text-transform'], value: 'uppercase', ns: 'textTransform'}], - ["lowercase", {class: ['text-transform'], value: 'lowercase', ns: 'textTransform'}], - ["capitalize", {class: ['text-transform'], value: 'capitalize', ns: 'textTransform'}], - ["normal-case", {class: ['text-transform'], value: 'none', ns: 'textTransform'}], - - // Font Style - ["italic", {class: ['font-style'], value: 'italic', ns: 'fontStyle'}], - ["not-italic", {class: ['font-style'], value: 'normal', ns: 'fontStyle'}], - - // Background Attachment - ["bg-fixed", {class: ['background-attachment'], value: 'fixed', ns: 'backgroundAttachment'}], - ["bg-local", {class: ['background-attachment'], value: 'local', ns: 'backgroundAttachment'}], - ["bg-scroll", {class: ['background-attachment'], value: 'scroll', ns: 'backgroundAttachment'}], - - // Background Repeat - ["bg-repeat", {class: ['background-repeat'], value: 'repeat', ns: 'backgroundRepeat'}], - ["bg-no-repeat", {class: ['background-repeat'], value: 'no-repeat', ns: 'backgroundRepeat'}], - ["bg-repeat-x", {class: ['background-repeat'], value: 'repeat-x', ns: 'backgroundRepeat'}], - ["bg-repeat-y", {class: ['background-repeat'], value: 'repeat-y', ns: 'backgroundRepeat'}], - ["bg-repeat-round", {class: ['background-repeat'], value: 'round', ns: 'backgroundRepeat'}], - ["bg-repeat-space", {class: ['background-repeat'], value: 'space', ns: 'backgroundRepeat'}], - - // Background Origin - ["bg-origin-border", {class: ['background-origin'], value: 'border', ns: 'backgroundOrigin'}], - ["bg-origin-padding", {class: ['background-origin'], value: 'padding', ns: 'backgroundOrigin'}], - ["bg-origin-content", {class: ['background-origin'], value: 'content', ns: 'backgroundOrigin'}], - - // Flex Direction - ["flex-row", {class: ['flex-direction'], value: 'row', ns: 'flexDirection'}], - ["flex-row-reverse", {class: ['flex-direction'], value: 'row-reverse', ns: 'flexDirection'}], - ["flex-col", {class: ['flex-direction'], value: 'column', ns: 'flexDirection'}], - ["flex-col-reverse", {class: ['flex-direction'], value: 'column-reverse', ns: 'flexDirection'}], - - // Flex Wrap - ["flex-wrap", {class: ['flex-wrap'], value: 'wrap', ns: 'flexWrap'}], - ["flex-wrap-reverse", {class: ['flex-wrap'], value: 'wrap-reverse', ns: 'flexWrap'}], - ["flex-nowrap", {class: ['flex-wrap'], value: 'nowrap', ns: 'flexWrap'}], - - // Align Items - ["items-start", {class: ['align-items'], value: 'flex-start', ns: 'alignItems'}], - ["items-end", {class: ['align-items'], value: 'flex-end', ns: 'alignItems'}], - ["items-center", {class: ['align-items'], value: 'center', ns: 'alignItems'}], - ["items-baseline", {class: ['align-items'], value: 'baseline', ns: 'alignItems'}], - ["items-stretch", {class: ['align-items'], value: 'stretch', ns: 'alignItems'}], - - // Justify Content - ["justify-start", {class: ['justify-content'], value: 'flex-start', ns: 'justifyContent'}], - ["justify-end", {class: ['justify-content'], value: 'flex-end', ns: 'justifyContent'}], - ["justify-center", {class: ['justify-content'], value: 'center', ns: 'justifyContent'}], - ["justify-between", {class: ['justify-content'], value: 'space-between', ns: 'justifyContent'}], - ["justify-around", {class: ['justify-content'], value: 'space-around', ns: 'justifyContent'}], - ["justify-evenly", {class: ['justify-content'], value: 'space-evenly', ns: 'justifyContent'}], - - // Flex Grow & Shrink - ["grow", {class: ['flex-grow'], value: '1', ns: 'flexGrow'}], - ["grow-0", {class: ['flex-grow'], value: '0', ns: 'flexGrow'}], - ["flex-grow", {class: ['flex-grow'], value: '1', ns: 'flexGrow'}], - ["flex-grow-0", {class: ['flex-grow'], value: '0', ns: 'flexGrow'}], - ["shrink", {class: ['flex-shrink'], value: '1', ns: 'flexShrink'}], - ["shrink-0", {class: ['flex-shrink'], value: '0', ns: 'flexShrink'}], - ["flex-shrink", {class: ['flex-shrink'], value: '1', ns: 'flexShrink'}], - ["flex-shrink-0", {class: ['flex-shrink'], value: '0', ns: 'flexShrink'}], - - // Flex Basis - ['basis-auto', { class: ['flex-basis'], value: 'auto', ns: 'flexBasis' }], - ['basis-full', { class: ['flex-basis'], value: '100%', ns: 'flexBasis' }], - - // Filters - ["grayscale", {class: ['filter'], value: 'grayscale(100%)', ns: 'grayScale'}], - ["grayscale-0", {class: ['filter'], value: 'grayscale(0)', ns: 'grayScale'}], - ["invert", {class: ['filter'], value: 'invert(100%)', ns: 'invert'}], - ["invert-0", {class: ['filter'], value: 'invert(0)', ns: 'invert'}], - ["sepia", {class: ['filter'], value: 'sepia(100%)', ns: 'sepia'}], - ["sepia-0", {class: ['filter'], value: 'sepia(0)', ns: 'sepia'}], - ["backdrop-grayscale", {class: ['backdrop-filter'], value: 'backdrop-grayscale(100%)', ns: 'backdropGrayScale'}], - ["backdrop-grayscale-0", {class: ['backdrop-filter'], value: 'backdrop-grayscale(0)', ns: 'backdropGrayScale'}], - ["backdrop-invert", {class: ['backdrop-filter'], value: 'backdrop-invert(100%)', ns: 'backdrop-invert'}], - ["backdrop-invert-0", {class: ['backdrop-filter'], value: 'backdrop-invert(0)', ns: 'backdrop-invert'}], - ["backdrop-sepia", {class: ['backdrop-filter'], value: 'backdrop-sepia(100%)', ns: 'backdrop-sepia'}], - ["backdrop-sepia-0", {class: ['backdrop-filter'], value: 'backdrop-sepia(0)', ns: 'backdrop-sepia'}], - - ["ease-linear", {class: ['transition-timing-function'], value: 'linear', ns: 'transitionTiming'}], - ["ease-in", {class: ['transition-timing-function'], value: 'cubic-bezier(0.4, 0, 1, 1)', ns: 'transitionTiming'}], - ["ease-out", {class: ['transition-timing-function'], value: 'cubic-bezier(0, 0, 0.2, 1)', ns: 'transitionTiming'}], - ["ease-in-out", {class: ['transition-timing-function'], value: 'cubic-bezier(0.4, 0, 0.2, 1)', ns: 'transitionTiming'}], -]) - -export const variants = new Map([ - ['first', 'pseudo'], - ['last', 'pseudo'], - ['only', 'pseudo'], - ['odd', 'pseudo'], - ['even', 'pseudo'], - ['first-of-type', 'pseudo'], - ['last-of-type', 'pseudo'], - ['only-of-type', 'pseudo'], - - // State - ['visited', 'state'], - ['target', 'state'], - ['open', 'state'], - - // Forms - ['default', 'form'], - ['checked', 'form'], - ['indeterminate', 'form'], - ['placeholder-shown', 'form'], - ['autofill', 'form'], - ['optional', 'form'], - ['required', 'form'], - ['valid', 'form'], - ['invalid', 'form'], - ['in-range', 'form'], - ['out-of-range', 'form'], - ['read-only', 'form'], - - // Content - ['empty', 'content'], - - // Interactive - ['focus-within', 'interaction'], - ['hover', 'interaction'], - ['group-hover', 'interaction'], - ['focus', 'interaction'], - ['focus-visible', 'interaction'], - ['active', 'interaction'], - ['enabled', 'interaction'], - ['disabled', 'interaction'], - - ['dark', 'system'] -]) - -export const getPluginsByNs = (ns: string, pluginMap: Map) => { - const filteredMap = new Map() - for(const [key, value] of Object.entries(pluginMap)) { - console.log(value) - - if(Array.isArray(value)){ - for(const plugin of value){ - if(plugin.ns === ns) { - filteredMap.set(key, plugin) - } - } - }else if(value.ns === ns) { - filteredMap.set(key, value) - } - } - - return filteredMap -} \ No newline at end of file diff --git a/src/classname.js b/src/tailwind/classname.js similarity index 81% rename from src/classname.js rename to src/tailwind/classname.js index 208d322..ba2d241 100644 --- a/src/classname.js +++ b/src/tailwind/classname.js @@ -1,15 +1,11 @@ import { PluginNotFoundException } from './exceptions/plugin-not-found-exception' import { functionalPlugins, namedPlugins } from './plugins' - -import { buildModifier } from './utils/build-modifier' -import { calculateHexFromString } from './utils/calculate-hex-from-string' -import { findTailwindColorFromHex } from './utils/find-tailwind-color-from-hex' import { isColor } from './utils/is-color' import { StringBuilder } from './utils/string-builder' export const EMPTY_CLASSNAME = '' -export const classname = (ast) => { +export const classname = ast => { if ([null, undefined, ''].includes(ast.value)) { return EMPTY_CLASSNAME } @@ -27,6 +23,12 @@ export const classname = (ast) => { negative = true } + if (ast.skipParse) { + return stringBuilder + .addValue(ast.value) + .toString() + } + const [namedPluginClassName] = [...namedPlugins.entries()] .filter(([, plugin]) => plugin.ns === ast.property) .find(([, plugin]) => plugin.value === ast.value) || [] @@ -54,16 +56,8 @@ export const classname = (ast) => { .toString() } - const color = calculateHexFromString(ast.value) - if (!color) { - return EMPTY_CLASSNAME - } - return stringBuilder - .appendModifier(buildModifier(color.alpha || ast.modifier)) - .addValue( - findTailwindColorFromHex(color.hex) || StringBuilder.makeArbitrary(color.hex), - ) + .addValue(ast.value) .toString() } diff --git a/src/exceptions/plugin-not-found-exception.ts b/src/tailwind/exceptions/plugin-not-found-exception.js similarity index 89% rename from src/exceptions/plugin-not-found-exception.ts rename to src/tailwind/exceptions/plugin-not-found-exception.js index 5b2e3ab..3404eb3 100644 --- a/src/exceptions/plugin-not-found-exception.ts +++ b/src/tailwind/exceptions/plugin-not-found-exception.js @@ -1,6 +1,6 @@ export class PluginNotFoundException extends Error { - constructor(base: string) { + constructor(base) { super(`Unable to identify the plugin based on ${base}. This could be due to a misspelling, an invalid plugin, or the plugin is not yet included in the supported plugins list.`); this.name = 'PluginNotFoundException'; } -} \ No newline at end of file +} diff --git a/src/tailwind/exceptions/unmatched-value-exception.js b/src/tailwind/exceptions/unmatched-value-exception.js new file mode 100644 index 0000000..e95fb51 --- /dev/null +++ b/src/tailwind/exceptions/unmatched-value-exception.js @@ -0,0 +1,6 @@ +export class UnmatchedValueException extends Error { + constructor(plugin, value) { + super(`Found a matched plugin \`${plugin}\`, but the value \`${value}\` could not matched. This could be due to a misspelling or the given value is not covered by tailwind.config file.`); + this.name = 'PluginNotFoundException'; + } +} diff --git a/src/tailwind/index.js b/src/tailwind/index.js new file mode 100644 index 0000000..03cb421 --- /dev/null +++ b/src/tailwind/index.js @@ -0,0 +1,2 @@ +export { classname } from './classname' +export { parse } from './parse' diff --git a/src/tailwind/parse.js b/src/tailwind/parse.js new file mode 100644 index 0000000..bfe3f9f --- /dev/null +++ b/src/tailwind/parse.js @@ -0,0 +1,137 @@ +import { functionalPlugins, namedPlugins } from './plugins'; +import { buildModifier } from './utils/build-modifier'; +import { calculateHexFromString } from './utils/calculate-hex-from-string'; +import { decodeArbitraryValue } from './utils/decode-arbitrary-value'; +import { findRoot } from './utils/find-root'; +import { findTailwindColorFromHex } from './utils/find-tailwind-color-from-hex'; +import { inferDataType } from './utils/infer-data-type'; +import { isColor } from './utils/is-color'; +import { parseVariant } from './utils/parse-variant'; +import { segment } from './utils/segment'; + +export const parse = input => { + if (!input) { + return { + root: '', + kind: 'error', + message: 'Empty input', + } + } + + const state = { + important: false, + negative: false, + } + const variants = segment(input, ':') + let base = variants.pop() + + const parsedCandidateVariants = [] + + for (let i = variants.length - 1; i >= 0; --i) { + const parsedVariant = parseVariant(variants[i]) + if (parsedVariant !== null) { + parsedCandidateVariants.push(parsedVariant) + } + } + + if (base[0] === '!') { + state.important = true + base = base.slice(1) + } + + if (base[0] === '-') { + state.negative = true + base = base.slice(1) + } + + const namedPlugin = namedPlugins.get(base) + if (namedPlugin) { + return { + root: base, + kind: 'named', + property: namedPlugin.ns, + value: namedPlugin.value, + valueDef: { + class: namedPlugin.class, + raw: base, + kind: 'named', + value: namedPlugin.value, + }, + variants: parsedCandidateVariants, + modifier: null, + important: state.important, + negative: state.negative, + arbitrary: false, + } + } + + const [root, value] = findRoot(base, functionalPlugins) + + if (!root) { + return { + root: base, + kind: 'error', + message: 'Tailwindcss core plugin not found', + } + } + + const availablePlugins = functionalPlugins.get(root) + let modifier = null + let [valueWithoutModifier, modifierSegment = null] = segment(value || '', '/') + if (modifierSegment && isColor(valueWithoutModifier.replace(/[[\]]/g, ''))) { + modifier = buildModifier(modifierSegment) + } + + if (valueWithoutModifier && valueWithoutModifier[0] === '[' && valueWithoutModifier[valueWithoutModifier.length - 1] === ']') { + let arbitraryValue = valueWithoutModifier.slice(1, -1) + const unitType = inferDataType(arbitraryValue, [...availablePlugins.values()].map(({ type }) => type)) + let associatedPluginByType = availablePlugins.find(plugin => plugin.type === unitType) + + if (unitType === 'color') { + const color = calculateHexFromString(arbitraryValue) + if (!color) { + return { + root: base, + kind: 'error', + message: 'Color is not correct', + } + } + valueWithoutModifier = findTailwindColorFromHex(color.hex) || color.hex + } else if (availablePlugins.length > 0) { + associatedPluginByType = availablePlugins.find(x => x.type === unitType) || availablePlugins.find(x => x.type !== 'color') + } + + arbitraryValue = decodeArbitraryValue(arbitraryValue) + + return { + root: root, + kind: 'functional', + property: associatedPluginByType.ns, + value: arbitraryValue, + valueDef: { + value: arbitraryValue, + class: associatedPluginByType.class, + raw: valueWithoutModifier, + kind: unitType || 'named', + }, + variants: parsedCandidateVariants, + modifier: modifier, + arbitrary: true, + important: state.important, + negative: state.negative, + } + } + + return { + root: root, + kind: 'functional', + property: availablePlugins[0].ns, + value, + valueDef: {}, + variants: parsedCandidateVariants, + modifier: modifier, + important: state.important, + negative: state.negative, + arbitrary: false, + } +} diff --git a/src/tailwind/plugins.js b/src/tailwind/plugins.js new file mode 100644 index 0000000..da9fba2 --- /dev/null +++ b/src/tailwind/plugins.js @@ -0,0 +1,537 @@ +export const functionalPlugins = new Map([ + ["w", [ + { scaleKey: "width", ns: 'width', class: ['width'], type: 'length' }, + ]], + ["min-w", [ + { scaleKey: "minWidth", ns: 'minWidth', class: ['min-width'], type: 'length' }, + ]], + ["max-w", [ + { scaleKey: "maxWidth", ns: 'maxWidth', class: ['max-width'], type: 'length' }, + ]], + ["h", [ + { scaleKey: "height", ns: 'height', class: ['height'], type: 'length' }, + ]], + ["min-h", [ + { scaleKey: "minHeight", ns: 'minHeight', class: ['min-height'], type: 'length' }, + ]], + ["max-h", [ + { scaleKey: "maxHeight", ns: 'maxHeight', class: ['max-height'], type: 'length' }, + ]], + ["size", [ + { scaleKey: "size", ns: 'size', class: ['width', 'height'], type: 'length' }, + ]], + ["m", [ + { scaleKey: "margin", ns: 'margin', class: ['margin'], type: 'length', supportNegative: true }, + ]], + ["mx", [ + { scaleKey: "margin", ns: 'marginX', class: ['margin-left', 'margin-right'], type: 'length', supportNegative: true }, + ]], + ["my", [ + { scaleKey: "margin", ns: 'marginY', class: ['margin-top', 'margin-bottom'], type: 'length', supportNegative: true }, + ]], + ["mt", [ + { scaleKey: "margin", ns: 'marginTop', class: ['margin-top'], type: 'length', supportNegative: true }, + ]], + ["mr", [ + { scaleKey: "margin", ns: 'marginRight', class: ['margin-right'], type: 'length', supportNegative: true }, + ]], + ["mb", [ + { scaleKey: "margin", ns: 'marginBottom', class: ['margin-bottom'], type: 'length', supportNegative: true }, + ]], + ["ml", [ + { scaleKey: "margin", ns: 'marginLeft', class: ['margin-left'], type: 'length', supportNegative: true }, + ]], + ["p", [ + { scaleKey: "padding", ns: 'padding', class: ['padding'], type: 'length' }, + ]], + ["px", [ + { scaleKey: "padding", ns: 'paddingX', class: ['padding-left', 'padding-right'], type: 'length' }, + ]], + ["py", [ + { scaleKey: "padding", ns: 'paddingY', class: ['padding-top', 'padding-bottom'], type: 'length' }, + ]], + ["pt", [ + { scaleKey: "padding", ns: 'paddingTop', class: ['padding-top'], type: 'length' }, + ]], + ["pr", [ + { scaleKey: "padding", ns: 'paddingRight', class: ['padding-right'], type: 'length' }, + ]], + ["pb", [ + { scaleKey: "padding", ns: 'paddingBottom', class: ['padding-bottom'], type: 'length' }, + ]], + ["pl", [ + { scaleKey: "padding", ns: 'paddingLeft', class: ['padding-left'], type: 'length' }, + ]], + ["text", [ + { scaleKey: "fontSize", ns: 'fontSize', class: ['font-size'], type: 'length' }, + { scaleKey: "textColor", ns: 'textColor', class: ['color'], type: 'color' }, + ]], + ["indent", [ + { scaleKey: "textIndent", ns: 'textIndent', class: ['text-indent'], type: 'length', supportNegative: true }, + ]], + ["underline-offset", [ + { scaleKey: "textUnderlineOffset", ns: 'textUnderlineOffset', class: ['text-underline-offset'], type: 'number' }, + ]], + ["decoration", [ + { scaleKey: "textDecorationThickness", ns: 'textDecorationThickness', class: ['text-decoration-thickness'], type: 'number' }, + { scaleKey: "textDecorationColor", ns: 'textDecorationColor', class: ['text-decoration-color'], type: 'color' }, + ]], + ["font", [ + { scaleKey: "fontWeight", ns: 'fontWeight', class: ['font-weight'], type: 'number' }, + { scaleKey: "fontFamily", ns: 'fontFamily', class: ['font-family'], type: 'family-name' }, + ]], + ["leading", [ + { scaleKey: "lineHeight", ns: 'lineHeight', class: ['line-height'], type: 'number' }, + ]], + ["tracking", [ + { scaleKey: "letterSpacing", ns: 'letterSpacing', class: ['letter-spacing'], type: 'length', supportNegative: true }, + ]], + ["bg", [ + { scaleKey: "backgroundImage", ns: 'backgroundImage', class: ['background-image'], type: 'image' }, + { scaleKey: "backgroundPosition", ns: 'backgroundPosition', class: ['background-position'], type: 'position' }, + { scaleKey: "backgroundSize", ns: 'backgroundSize', class: ['background-size'], type: 'bg-size' }, + { scaleKey: "backgroundColor", ns: 'backgroundColor', class: ['background-color'], type: 'color' }, + ]], + ["from", [ + { scaleKey: "gradientColorStops", ns: 'gradientStopsFrom', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'color' }, + { scaleKey: "gradientColorStopPositions", ns: 'gradientStopsFromPosition', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage' }, + ]], + ["to", [ + { scaleKey: "gradientColorStops", ns: 'gradientStopsTo', class: ['--tw-gradient-to', '--tw-gradient-to-position'], type: 'color' }, + { scaleKey: "gradientColorStopPositions", ns: 'gradientStopsToPosition', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage' }, + ]], + ["via", [ + { scaleKey: "gradientColorStops", ns: 'gradientStopsVia', class: ['--tw-gradient-from'], type: 'color' }, + { scaleKey: "gradientColorStopPositions", ns: 'gradientStopsViaPositions', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'percentage' }, + ]], + ["fill", [ + { scaleKey: "fill", ns: 'fillColor', class: ['fill'], type: 'color' }, + ]], + ["stroke", [ + { scaleKey: "strokeWidth", ns: 'strokeWidth', class: ['stroke-width'], type: 'line-width' }, + { scaleKey: "stroke", ns: 'strokeColor', class: ['border-color'], type: 'color' }, + ]], + ["border", [ + { scaleKey: "borderWidth", ns: 'borderWidth', class: ['border-width'], type: 'line-width' }, + { scaleKey: "borderColor", ns: 'borderColor', class: ['border-color'], type: 'color' }, + ]], + ["ring", [ + { scaleKey: "ringWidth", ns: 'ringWidth', class: ['box-shadow'], type: 'line-width' }, + { scaleKey: "ringColor", ns: 'ringColor', class: ['--tw-ring-color'], type: 'color' }, + ]], + ["ring-offset", [ + { scaleKey: "ringOffsetWidth", ns: 'ringOffsetWidth', class: ['--tw-ring-offset-width'], type: 'line-width' }, + { scaleKey: "ringOffsetColor", ns: 'ringOffsetColor', class: ['--tw-ring-offset-color'], type: 'color' }, + ]], + ["rounded", [ + { scaleKey: "borderRadius", ns: 'borderRadius', class: ['border-radius'], type: 'length' }, + ]], + ["rounded-tl", [ + { scaleKey: "borderRadius", ns: 'borderTopLeftRadius', class: ['border-top-left-radius'], type: 'length' }, + ]], + ["rounded-tr", [ + { scaleKey: "borderRadius", ns: 'borderTopRightRadius', class: ['border-top-right-radius'], type: 'length' }, + ]], + ["rounded-br", [ + { scaleKey: "borderRadius", ns: 'borderBottomRightRadius', class: ['border-bottom-right-radius'], type: 'length' }, + ]], + ["rounded-bl", [ + { scaleKey: "borderRadius", ns: 'borderBottomLeftRadius', class: ['border-bottom-left-radius'], type: 'length' }, + ]], + ["flex", [ + { scaleKey: "flex", ns: 'flex', class: ['flex'], type: 'number' }, + ]], + ["basis", [ + { scaleKey: "flexBasis", ns: 'flexBasis', class: ['flex-basis'], type: 'length' }, + ]], + ["grid-cols", [ + { scaleKey: "gridTemplateColumns", ns: 'gridTemplateColumns', class: ['grid-template-columns'], type: 'number' }, + ]], + ["grid-rows", [ + { scaleKey: "gridTemplateRows", ns: 'gridTemplateRows', class: ['grid-template-rows'], type: 'number' }, + ]], + ["gap", [ + { scaleKey: "gap", ns: 'gap', class: ['gap'], type: 'length' }, + ]], + ["gap-x", [ + { scaleKey: "gap", ns: 'gapX', class: ['gap'], type: 'length' }, + ]], + ["gap-y", [ + { scaleKey: "gap", ns: 'gapY', class: ['gap'], type: 'length' }, + ]], + ["divide-x", [ + { scaleKey: "divideWidth", ns: 'divideX', class: ['border-left-width', 'border-right-width'], type: 'length' }, + ]], + ["divide-y", [ + { scaleKey: "divideWidth", ns: 'divideY', class: ['border-top-width', 'border-bottom-width'], type: 'length' }, + ]], + ["divide", [ + { scaleKey: "divideColor", ns: 'divideColor', class: ['--tw-divide-color'], type: 'color' }, + ]], + ["divide-opacity", [ + { scaleKey: "divideOpacity", ns: 'divideOpacity', class: ['--tw-divide-opacity'], type: 'number' }, + ]], + ["col", [ + { scaleKey: "gridColumn", ns: 'gridColumn', class: ['grid-column'], type: 'number' }, + ]], + ["row", [ + { scaleKey: "gridRow", ns: 'gridRow', class: ['grid-row'], type: 'number' }, + ]], + ["space-x", [ + { scaleKey: "spacing", ns: 'spaceX', class: ['margin-left'], type: 'number', supportNegative: true }, + ]], + ["space-y", [ + { scaleKey: "spacing", ns: 'spaceY', class: ['margin-top'], type: 'number', supportNegative: true }, + ]], + ["opacity", [ + { scaleKey: "opacity", ns: 'opacity', class: ['opacity'], type: 'number' }, + ]], + ["shadow", [ + { scaleKey: "boxShadow", ns: 'boxShadow', class: ['box-shadow'], type: 'length' }, + { scaleKey: "boxShadowColor", ns: 'boxShadowColor', class: ['box-shadow'], type: 'color' }, + ]], + ["transition", [ + { scaleKey: "transitionProperty", ns: 'transitionProperty', class: ['transition-property'], type: 'named' }, + ]], + ["duration", [ + { scaleKey: "transitionDuration", ns: 'transitionDuration', class: ['transition-duration'], type: 'number' }, + ]], + ["delay", [ + { scaleKey: "transitionDelay", ns: 'transitionDelay', class: ['transition-delay'], type: 'number' }, + ]], + ["scale", [ + { scaleKey: "scale", ns: 'scale', class: ['scale'], type: 'number', supportNegative: true }, + ]], + ["rotate", [ + { scaleKey: "rotate", ns: 'rotate', class: ['rotate'], type: 'angle', supportNegative: true }, + ]], + ["translate", [ + { scaleKey: "translate", ns: 'translate', class: ['transform'], type: 'length', supportNegative: true }, + ]], + ["translate-y", [ + { scaleKey: "translate", ns: 'translateY', class: ['transform'], type: 'length', supportNegative: true }, + ]], + ["translate-x", [ + { scaleKey: "translate", ns: 'translateX', class: ['transform'], type: 'length', supportNegative: true }, + ]], + ["skew", [ + { scaleKey: "skew", ns: 'skew', class: ['skew'], type: 'angle', supportNegative: true }, + ]], + ["z", [ + { scaleKey: "zIndex", ns: 'zIndex', class: ['z-index'], type: 'number', supportNegative: true }, + ]], + ["inset", [ + { scaleKey: "inset", ns: 'inset', class: ['inset'], type: 'length', supportNegative: true }, + ]], + ["inset-x", [ + { scaleKey: "inset", ns: 'insetX', class: ['left', 'right'], type: 'length', supportNegative: true }, + ]], + ["inset-y", [ + { scaleKey: "inset", ns: 'insetY', class: ['top', 'bottom'], type: 'length', supportNegative: true }, + ]], + ["top", [ + { scaleKey: "inset", ns: 'positionTop', class: ['top'], type: 'length', supportNegative: true }, + ]], + ["right", [ + { scaleKey: "inset", ns: 'positionRight', class: ['right'], type: 'length', supportNegative: true }, + ]], + ["bottom", [ + { scaleKey: "inset", ns: 'positionBottom', class: ['bottom'], type: 'length', supportNegative: true }, + ]], + ["left", [ + { scaleKey: "inset", ns: 'positionLeft', class: ['left'], type: 'length', supportNegative: true }, + ]], + ["start", [ + { scaleKey: "inset", ns: 'insetInlineStart', class: ['inset-inline-start'], type: 'length', supportNegative: true }, + ]], + ["end", [ + { scaleKey: "inset", ns: 'insetInlineEnd', class: ['inset-inline-end'], type: 'length', supportNegative: true }, + ]], + ["order", [ + { scaleKey: "order", ns: 'order', class: ['order'], type: 'length', supportNegative: true }, + ]], + ["blur", [ + { scaleKey: "blur", ns: 'blur', class: ['filter'], type: 'number' }, + ]], + ["brightness", [ + { scaleKey: "brightness", ns: 'brightness', class: ['filter'], type: 'number' }, + ]], + ["contrast", [ + { scaleKey: "contrast", ns: 'contrast', class: ['filter'], type: 'number' }, + ]], + ["drop-shadow", [ + { scaleKey: "dropShadow", ns: 'dropShadow', class: ['filter'], type: 'number' }, + ]], + ["hue-rotate", [ + { scaleKey: "hueRotate", ns: 'hueRotate', class: ['filter'], type: 'number' }, + ]], + ["saturate", [ + { scaleKey: "saturate", ns: 'saturate', class: ['filter'], type: 'number' }, + ]], + ["backdrop-blur", [ + { scaleKey: "blur", ns: 'blur', class: ['filter'], type: 'number' }, + ]], + ["backdrop-brightness", [ + { scaleKey: "backdropBrightness", ns: 'backdropBrightness', class: ['backdrop-filter'], type: 'number' }, + ]], + ["backdrop-contrast", [ + { scaleKey: "backdropContrast", ns: 'backdropContrast', class: ['backdrop-filter'], type: 'number' }, + ]], + ["backdrop-drop-shadow", [ + { scaleKey: "backdropDropShadow", ns: 'backdropDropShadow', class: ['backdrop-filter'], type: 'number' }, + ]], + ["backdrop-hue-rotate", [ + { scaleKey: "backdropHueRotate", ns: 'backdropHueRotate', class: ['backdrop-filter'], type: 'number' }, + ]], + ["backdrop-saturate", [ + { scaleKey: "backdropSaturate", ns: 'backdropSaturate', class: ['backdrop-filter'], type: 'number' }, + ]], +]); + +export const namedPlugins = new Map([ + // Border Styles + ["border-solid", { class: ['border-style'], value: 'solid', ns: 'borderStyle' }], + ["border-dashed", { class: ['border-style'], value: 'dashed', ns: 'borderStyle' }], + ["border-dotted", { class: ['border-style'], value: 'dotted', ns: 'borderStyle' }], + ["border-double", { class: ['border-style'], value: 'double', ns: 'borderStyle' }], + ["border-none", { class: ['border-style'], value: 'none', ns: 'borderStyle' }], + ["ring-inset", { class: ['--tw-ring-inset'], value: 'inset', ns: 'ring' }], + + ["divide-solid", { class: ['divide-style'], value: 'solid', ns: 'divideStyle' }], + ["divide-dashed", { class: ['divide-style'], value: 'dashed', ns: 'divideStyle' }], + ["divide-dotted", { class: ['divide-style'], value: 'dotted', ns: 'divideStyle' }], + ["divide-double", { class: ['divide-style'], value: 'double', ns: 'divideStyle' }], + ["divide-none", { class: ['divide-style'], value: 'none', ns: 'divideStyle' }], + + // Display + ["block", { class: ['display'], value: 'block', ns: 'display' }], + ["inline-block", { class: ['display'], value: 'inline-block', ns: 'display' }], + ["inline", { class: ['display'], value: 'inline', ns: 'display' }], + ["flex", { class: ['display'], value: 'flex', ns: 'display' }], + ["grid", { class: ['display'], value: 'grid', ns: 'display' }], + ["hidden", { class: ['display'], value: 'none', ns: 'display' }], + ["table", { class: ['display'], value: 'table', ns: 'display' }], + ["table-row", { class: ['display'], value: 'table-row', ns: 'display' }], + ["table-cell", { class: ['display'], value: 'table-cell', ns: 'display' }], + ["contents", { class: ['display'], value: 'contents', ns: 'display' }], + ["list-item", { class: ['display'], value: 'list-item', ns: 'display' }], + ["flow-root", { class: ['display'], value: 'flow-root', ns: 'display' }], + + // Visibility + ["visible", { class: ['visibility'], value: 'visible', ns: 'visibility' }], + ["invisible", { class: ['visibility'], value: 'hidden', ns: 'visibility' }], + + // Hyphens + ["hyphens-none", { class: ['hyphens'], value: 'none', ns: 'hyphens' }], + ["hyphens-manual", { class: ['hyphens'], value: 'manual', ns: 'hyphens' }], + ["hyphens-auto", { class: ['hyphens'], value: 'auto', ns: 'hyphens' }], + + // Word Break + ["break-normal", { class: ['word-break', 'overflow-wrap'], value: 'normal', ns: 'wordBreak' }], + ["break-words", { class: ['overflow-wrap'], value: 'break-word', ns: 'wordBreak' }], + ["break-all", { class: ['word-break'], value: 'break-all', ns: 'wordBreak' }], + ["break-keep", { class: ['word-break'], value: 'keep-all', ns: 'wordBreak' }], + + // Whitespace + ["whitespace-normal", { class: ['white-space'], value: 'normal', ns: 'whitespace' }], + ["whitespace-nowrap", { class: ['white-space'], value: 'nowrap', ns: 'whitespace' }], + ["whitespace-pre", { class: ['white-space'], value: 'pre', ns: 'whitespace' }], + ["whitespace-pre-line", { class: ['white-space'], value: 'pre-line', ns: 'whitespace' }], + ["whitespace-pre-wrap", { class: ['white-space'], value: 'pre-wrap', ns: 'whitespace' }], + ["whitespace-break-spaces", { class: ['white-space'], value: 'break-spaces', ns: 'whitespace' }], + + // Position + ["static", { class: ['position'], value: 'static', ns: 'position' }], + ["fixed", { class: ['position'], value: 'fixed', ns: 'position' }], + ["absolute", { class: ['position'], value: 'absolute', ns: 'position' }], + ["relative", { class: ['position'], value: 'relative', ns: 'position' }], + ["sticky", { class: ['position'], value: 'sticky', ns: 'position' }], + + // Overflow + ["overflow-auto", { class: ['overflow'], value: 'auto', ns: 'overflow' }], + ["overflow-hidden", { class: ['overflow'], value: 'hidden', ns: 'overflow' }], + ["overflow-visible", { class: ['overflow'], value: 'visible', ns: 'overflow' }], + ["overflow-scroll", { class: ['overflow'], value: 'scroll', ns: 'overflow' }], + ["overflow-x-auto", { class: ['overflow-x'], value: 'auto', ns: 'overflowX' }], + ["overflow-x-hidden", { class: ['overflow-x'], value: 'hidden', ns: 'overflowX' }], + ["overflow-x-visible", { class: ['overflow-x'], value: 'visible', ns: 'overflowX' }], + ["overflow-x-scroll", { class: ['overflow-x'], value: 'scroll', ns: 'overflowX' }], + ["overflow-y-auto", { class: ['overflow-y'], value: 'auto', ns: 'overflowY' }], + ["overflow-y-hidden", { class: ['overflow-y'], value: 'hidden', ns: 'overflowY' }], + ["overflow-y-visible", { class: ['overflow-y'], value: 'visible', ns: 'overflowY' }], + ["overflow-y-scroll", { class: ['overflow-y'], value: 'scroll', ns: 'overflowY' }], + + // Text Align + ["text-left", { class: ['text-align'], value: 'left', ns: 'textAlign' }], + ["text-center", { class: ['text-align'], value: 'center', ns: 'textAlign' }], + ["text-right", { class: ['text-align'], value: 'right', ns: 'textAlign' }], + ["text-justify", { class: ['text-align'], value: 'justify', ns: 'textAlign' }], + ["text-start", { class: ['text-align'], value: 'start', ns: 'textAlign' }], + ["text-end", { class: ['text-align'], value: 'end', ns: 'textAlign' }], + + // Text Decoration + ["underline", { class: ['text-decoration-line'], value: 'underline', ns: 'textDecoration' }], + ["line-through", { class: ['text-decoration-line'], value: 'line-through', ns: 'textDecoration' }], + ["overline", { class: ['text-decoration-line'], value: 'overline', ns: 'textDecoration' }], + ["no-underline", { class: ['text-decoration-line'], value: 'none', ns: 'textDecoration' }], + + // Text Wrap + ["text-wrap", { class: ['text-wrap'], value: 'wrap', ns: 'textWrap' }], + ["text-nowrap", { class: ['text-wrap'], value: 'nowrap', ns: 'textWrap' }], + ["text-balance", { class: ['text-wrap'], value: 'balance', ns: 'textWrap' }], + ["text-pretty", { class: ['text-wrap'], value: 'pretty', ns: 'textWrap' }], + + // Text Overflow + ["truncate", { class: ['overflow', 'text-overflow', 'white-space'], value: '', ns: 'TextOverflow' }], + ["text-ellipsis", { class: ['text-overflow'], value: 'ellipsis', ns: 'textWrap' }], + ["text-clip", { class: ['text-overflow'], value: 'clip', ns: 'textWrap' }], + + // Text Transform + ["uppercase", { class: ['text-transform'], value: 'uppercase', ns: 'textTransform' }], + ["lowercase", { class: ['text-transform'], value: 'lowercase', ns: 'textTransform' }], + ["capitalize", { class: ['text-transform'], value: 'capitalize', ns: 'textTransform' }], + ["normal-case", { class: ['text-transform'], value: 'none', ns: 'textTransform' }], + + // Font Style + ["italic", { class: ['font-style'], value: 'italic', ns: 'fontStyle' }], + ["not-italic", { class: ['font-style'], value: 'normal', ns: 'fontStyle' }], + + // Background Attachment + ["bg-fixed", { class: ['background-attachment'], value: 'fixed', ns: 'backgroundAttachment' }], + ["bg-local", { class: ['background-attachment'], value: 'local', ns: 'backgroundAttachment' }], + ["bg-scroll", { class: ['background-attachment'], value: 'scroll', ns: 'backgroundAttachment' }], + + // Background Repeat + ["bg-repeat", { class: ['background-repeat'], value: 'repeat', ns: 'backgroundRepeat' }], + ["bg-no-repeat", { class: ['background-repeat'], value: 'no-repeat', ns: 'backgroundRepeat' }], + ["bg-repeat-x", { class: ['background-repeat'], value: 'repeat-x', ns: 'backgroundRepeat' }], + ["bg-repeat-y", { class: ['background-repeat'], value: 'repeat-y', ns: 'backgroundRepeat' }], + ["bg-repeat-round", { class: ['background-repeat'], value: 'round', ns: 'backgroundRepeat' }], + ["bg-repeat-space", { class: ['background-repeat'], value: 'space', ns: 'backgroundRepeat' }], + + // Background Origin + ["bg-origin-border", { class: ['background-origin'], value: 'border', ns: 'backgroundOrigin' }], + ["bg-origin-padding", { class: ['background-origin'], value: 'padding', ns: 'backgroundOrigin' }], + ["bg-origin-content", { class: ['background-origin'], value: 'content', ns: 'backgroundOrigin' }], + + // Flex Direction + ["flex-row", { class: ['flex-direction'], value: 'row', ns: 'flexDirection' }], + ["flex-row-reverse", { class: ['flex-direction'], value: 'row-reverse', ns: 'flexDirection' }], + ["flex-col", { class: ['flex-direction'], value: 'column', ns: 'flexDirection' }], + ["flex-col-reverse", { class: ['flex-direction'], value: 'column-reverse', ns: 'flexDirection' }], + + // Flex Wrap + ["flex-wrap", { class: ['flex-wrap'], value: 'wrap', ns: 'flexWrap' }], + ["flex-wrap-reverse", { class: ['flex-wrap'], value: 'wrap-reverse', ns: 'flexWrap' }], + ["flex-nowrap", { class: ['flex-wrap'], value: 'nowrap', ns: 'flexWrap' }], + + // Align Items + ["items-start", { class: ['align-items'], value: 'flex-start', ns: 'alignItems' }], + ["items-end", { class: ['align-items'], value: 'flex-end', ns: 'alignItems' }], + ["items-center", { class: ['align-items'], value: 'center', ns: 'alignItems' }], + ["items-baseline", { class: ['align-items'], value: 'baseline', ns: 'alignItems' }], + ["items-stretch", { class: ['align-items'], value: 'stretch', ns: 'alignItems' }], + + // Justify Content + ["justify-start", { class: ['justify-content'], value: 'flex-start', ns: 'justifyContent' }], + ["justify-end", { class: ['justify-content'], value: 'flex-end', ns: 'justifyContent' }], + ["justify-center", { class: ['justify-content'], value: 'center', ns: 'justifyContent' }], + ["justify-between", { class: ['justify-content'], value: 'space-between', ns: 'justifyContent' }], + ["justify-around", { class: ['justify-content'], value: 'space-around', ns: 'justifyContent' }], + ["justify-evenly", { class: ['justify-content'], value: 'space-evenly', ns: 'justifyContent' }], + + // Flex Grow & Shrink + ["grow", { class: ['flex-grow'], value: '1', ns: 'flexGrow' }], + ["grow-0", { class: ['flex-grow'], value: '0', ns: 'flexGrow' }], + ["flex-grow", { class: ['flex-grow'], value: '1', ns: 'flexGrow' }], + ["flex-grow-0", { class: ['flex-grow'], value: '0', ns: 'flexGrow' }], + ["shrink", { class: ['flex-shrink'], value: '1', ns: 'flexShrink' }], + ["shrink-0", { class: ['flex-shrink'], value: '0', ns: 'flexShrink' }], + ["flex-shrink", { class: ['flex-shrink'], value: '1', ns: 'flexShrink' }], + ["flex-shrink-0", { class: ['flex-shrink'], value: '0', ns: 'flexShrink' }], + + // Flex Basis + ['basis-auto', { class: ['flex-basis'], value: 'auto', ns: 'flexBasis' }], + ['basis-full', { class: ['flex-basis'], value: '100%', ns: 'flexBasis' }], + + // Filters + ["grayscale", { class: ['filter'], value: 'grayscale(100%)', ns: 'grayScale' }], + ["grayscale-0", { class: ['filter'], value: 'grayscale(0)', ns: 'grayScale' }], + ["invert", { class: ['filter'], value: 'invert(100%)', ns: 'invert' }], + ["invert-0", { class: ['filter'], value: 'invert(0)', ns: 'invert' }], + ["sepia", { class: ['filter'], value: 'sepia(100%)', ns: 'sepia' }], + ["sepia-0", { class: ['filter'], value: 'sepia(0)', ns: 'sepia' }], + ["backdrop-grayscale", { class: ['backdrop-filter'], value: 'backdrop-grayscale(100%)', ns: 'backdropGrayScale' }], + ["backdrop-grayscale-0", { class: ['backdrop-filter'], value: 'backdrop-grayscale(0)', ns: 'backdropGrayScale' }], + ["backdrop-invert", { class: ['backdrop-filter'], value: 'backdrop-invert(100%)', ns: 'backdrop-invert' }], + ["backdrop-invert-0", { class: ['backdrop-filter'], value: 'backdrop-invert(0)', ns: 'backdrop-invert' }], + ["backdrop-sepia", { class: ['backdrop-filter'], value: 'backdrop-sepia(100%)', ns: 'backdrop-sepia' }], + ["backdrop-sepia-0", { class: ['backdrop-filter'], value: 'backdrop-sepia(0)', ns: 'backdrop-sepia' }], + + ["ease-linear", { class: ['transition-timing-function'], value: 'linear', ns: 'transitionTiming' }], + ["ease-in", { class: ['transition-timing-function'], value: 'cubic-bezier(0.4, 0, 1, 1)', ns: 'transitionTiming' }], + ["ease-out", { class: ['transition-timing-function'], value: 'cubic-bezier(0, 0, 0.2, 1)', ns: 'transitionTiming' }], + ["ease-in-out", { class: ['transition-timing-function'], value: 'cubic-bezier(0.4, 0, 0.2, 1)', ns: 'transitionTiming' }], +]) + +export const variants = new Map([ + ['first', 'pseudo'], + ['last', 'pseudo'], + ['only', 'pseudo'], + ['odd', 'pseudo'], + ['even', 'pseudo'], + ['first-of-type', 'pseudo'], + ['last-of-type', 'pseudo'], + ['only-of-type', 'pseudo'], + + // State + ['visited', 'state'], + ['target', 'state'], + ['open', 'state'], + + // Forms + ['default', 'form'], + ['checked', 'form'], + ['indeterminate', 'form'], + ['placeholder-shown', 'form'], + ['autofill', 'form'], + ['optional', 'form'], + ['required', 'form'], + ['valid', 'form'], + ['invalid', 'form'], + ['in-range', 'form'], + ['out-of-range', 'form'], + ['read-only', 'form'], + + // Content + ['empty', 'content'], + + // Interactive + ['focus-within', 'interaction'], + ['hover', 'interaction'], + ['group-hover', 'interaction'], + ['focus', 'interaction'], + ['focus-visible', 'interaction'], + ['active', 'interaction'], + ['enabled', 'interaction'], + ['disabled', 'interaction'], + + ['dark', 'system'], +]) + +export const getPluginsByNs = (ns, pluginMap) => { + const filteredMap = new Map() + for (const [key, value] of Object.entries(pluginMap)) { + if (Array.isArray(value)) { + for (const plugin of value) { + if (plugin.ns === ns) { + filteredMap.set(key, plugin) + } + } + } else if (value.ns === ns) { + filteredMap.set(key, value) + } + } + + return filteredMap +} diff --git a/src/tailwind/utils/build-modifier.js b/src/tailwind/utils/build-modifier.js new file mode 100644 index 0000000..4b8d2f8 --- /dev/null +++ b/src/tailwind/utils/build-modifier.js @@ -0,0 +1,30 @@ +import { StringBuilder } from './string-builder'; + +export const buildModifier = (modifier, opacityScale = {}) => { + if (!modifier) return '' + + if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') { + modifier = modifier.slice(1, -1) + } + + if (modifier[modifier.length - 1] === '%') { + return StringBuilder.makeArbitrary(modifier) + } + + for (const [key, value] of Object.entries(opacityScale)) { + if (key === modifier || value === modifier) { + return key + } + } + + if ((Number(modifier) === 0 || Number(modifier) >= 1) && Number(modifier) <= 100) { + return StringBuilder.makeArbitrary(`${modifier}%`) + } + + if (Number(modifier) >= 0 && Number(modifier) <= 1) { + // we have number between 0-1 but it's not in the scale just make it arbitrary. + return StringBuilder.makeArbitrary(modifier) + } + + return '' +} diff --git a/src/utils/calculate-hex-from-string.ts b/src/tailwind/utils/calculate-hex-from-string.js similarity index 70% rename from src/utils/calculate-hex-from-string.ts rename to src/tailwind/utils/calculate-hex-from-string.js index 966ab96..95579d3 100644 --- a/src/utils/calculate-hex-from-string.ts +++ b/src/tailwind/utils/calculate-hex-from-string.js @@ -1,6 +1,6 @@ import { colord } from 'colord'; -export const calculateHexFromString = (input: string): { hex: string, alpha: string | undefined } | undefined => { +export const calculateHexFromString = input => { const color = colord(input) const alpha = color.alpha() diff --git a/src/tailwind/utils/decode-arbitrary-value.js b/src/tailwind/utils/decode-arbitrary-value.js new file mode 100644 index 0000000..d100c33 --- /dev/null +++ b/src/tailwind/utils/decode-arbitrary-value.js @@ -0,0 +1,30 @@ +import { addWhitespaceAroundMathOperators } from './math-operators' + +export function decodeArbitraryValue(input) { + if (input.startsWith('url(')) { + return input + } + + input = convertUnderscoresToWhitespace(input) + input = addWhitespaceAroundMathOperators(input) + + return input +} + +function convertUnderscoresToWhitespace(input) { + let output = '' + for (let i = 0; i < input.length; i++) { + const char = input[i] + + if (char === '\\' && input[i + 1] === '_') { + output += '_' + i += 1 + } else if (char === '_') { + output += ' ' + } else { + output += char + } + } + + return output +} diff --git a/src/tailwind/utils/find-root.js b/src/tailwind/utils/find-root.js new file mode 100644 index 0000000..5f7c3ac --- /dev/null +++ b/src/tailwind/utils/find-root.js @@ -0,0 +1,23 @@ +export function findRoot(input, lookup) { + if (lookup.has(input)) return [input, null] + + let idx = input.lastIndexOf('-') + if (idx === -1) { + if (input[0] === '@' && lookup.has('@')) { + return ['@', input.slice(1)] + } + + return [null, null] + } + + do { + const maybeRoot = input.slice(0, idx) + if (lookup.has(maybeRoot)) { + return [maybeRoot, input.slice(idx + 1)] + } + + idx = input.lastIndexOf('-', idx - 1) + } while (idx > 0) + + return [null, null] +} diff --git a/src/tailwind/utils/find-tailwind-color-from-hex.js b/src/tailwind/utils/find-tailwind-color-from-hex.js new file mode 100644 index 0000000..b89734d --- /dev/null +++ b/src/tailwind/utils/find-tailwind-color-from-hex.js @@ -0,0 +1,19 @@ +import { colord } from 'colord'; + +export const findTailwindColorFromHex = (colorInput, colors = {}) => { + if (!colorInput) return false + + for (const [key, twColors] of Object.entries(colors)) { + if ((twColors === '#fff' || twColors === '#000') && colord(colorInput).isEqual(twColors)) { + return key + } else { + for (const [shade, hex] of Object.entries(twColors)) { + if (hex === colorInput) { + return `${key}-${shade}` + } + } + } + } + + return false +} diff --git a/src/tailwind/utils/infer-data-type.js b/src/tailwind/utils/infer-data-type.js new file mode 100644 index 0000000..49f4d9f --- /dev/null +++ b/src/tailwind/utils/infer-data-type.js @@ -0,0 +1,235 @@ +import { isColor } from './is-color' +import { hasMathFn } from './math-operators' +import { segment } from './segment' + +const checks = { + color: isColor, + length: isLength, + percentage: isPercentage, + number: isNumber, + url: isUrl, + position: isBackgroundPosition, + 'bg-size': isBackgroundSize, + 'line-width': isLineWidth, + image: isImage, + 'family-name': isFamilyName, + 'generic-name': isGenericName, + 'absolute-size': isAbsoluteSize, + 'relative-size': isRelativeSize, + angle: isAngle, + vector: isVector, + named: () => true, +} + +export function inferDataType(value, types) { + if (value.startsWith('var(')) return null + + for (const type of types) { + if (checks[type]?.(value)) { + return type + } + } + + return null +} + +/* -------------------------------------------------------------------------- */ + +const IS_URL = /^url\(.*\)$/ + +function isUrl(value) { + return IS_URL.test(value) +} + +/* -------------------------------------------------------------------------- */ + +function isLineWidth(value) { + return value === 'thin' || value === 'medium' || value === 'thick' +} + +/* -------------------------------------------------------------------------- */ + +const IS_IMAGE_FN = /^(?:element|image|cross-fade|image-set)\(/ +const IS_GRADIENT_FN = /^(repeating-)?(conic|linear|radial)-gradient\(/ + +function isImage(value) { + let count = 0 + + for (const part of segment(value, ',')) { + if (part.startsWith('var(')) continue + + if (isUrl(part)) { + count += 1 + continue + } + + if (IS_GRADIENT_FN.test(part)) { + count += 1 + continue + } + + if (IS_IMAGE_FN.test(part)) { + count += 1 + continue + } + + return false + } + + return count > 0 +} + +/* -------------------------------------------------------------------------- */ + +function isGenericName(value) { + return ( + value === 'serif' || value === 'sans-serif' || value === 'monospace' || value === 'cursive' || value === 'fantasy' || value === 'system-ui' || value === 'ui-serif' || value === 'ui-sans-serif' || value === 'ui-monospace' || value === 'ui-rounded' || value === 'math' || value === 'emoji' || value === 'fangsong' + ) +} + +function isFamilyName(value) { + let count = 0 + + for (const part of segment(value, ',')) { + const char = part.charCodeAt(0) + if (char >= 48 && char <= 57) return false + + if (part.startsWith('var(')) continue + + count += 1 + } + + return count > 0 +} + +function isAbsoluteSize(value) { + return ( + value === 'xx-small' || value === 'x-small' || value === 'small' || value === 'medium' || value === 'large' || value === 'x-large' || value === 'xx-large' || value === 'xxx-large' + ) +} + +function isRelativeSize(value) { + return value === 'larger' || value === 'smaller' +} + +/* -------------------------------------------------------------------------- */ + +const HAS_NUMBER = /[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?/ + +/* -------------------------------------------------------------------------- */ + +const IS_NUMBER = new RegExp(`^${HAS_NUMBER.source}$`) + +function isNumber(value) { + return IS_NUMBER.test(value) || hasMathFn(value) +} + +/* -------------------------------------------------------------------------- */ + +const IS_PERCENTAGE = new RegExp(`^${HAS_NUMBER.source}%$`) + +function isPercentage(value) { + return IS_PERCENTAGE.test(value) || hasMathFn(value) +} + +const LENGTH_UNITS = [ + 'cm', + 'mm', + 'Q', + 'in', + 'pc', + 'pt', + 'px', + 'em', + 'ex', + 'ch', + 'rem', + 'lh', + 'rlh', + 'vw', + 'vh', + 'vmin', + 'vmax', + 'vb', + 'vi', + 'svw', + 'svh', + 'lvw', + 'lvh', + 'dvw', + 'dvh', + 'cqw', + 'cqh', + 'cqi', + 'cqb', + 'cqmin', + 'cqmax', +] + +const IS_LENGTH = new RegExp(`^${HAS_NUMBER.source}(${LENGTH_UNITS.join('|')})$`) + +function isLength(value) { + return IS_LENGTH.test(value) || hasMathFn(value) +} + +function isBackgroundPosition(value) { + let count = 0 + + for (const part of segment(value, ' ')) { + if ( + part === 'center' || part === 'top' || part === 'right' || part === 'bottom' || part === 'left' + ) { + count += 1 + continue + } + + if (part.startsWith('var(')) continue + + if (isLength(part) || isPercentage(part)) { + count += 1 + continue + } + + return false + } + + return count > 0 +} + +function isBackgroundSize(value) { + let count = 0 + + for (const size of segment(value, ',')) { + if (size === 'cover' || size === 'contain') { + count += 1 + continue + } + + const values = segment(size, ' ') + + // Sizes must have exactly one or two values + if (values.length !== 1 && values.length !== 2) { + return false + } + + if (values.every(value => value === 'auto' || isLength(value) || isPercentage(value))) { + count += 1 + } + } + + return count > 0 +} + +const ANGLE_UNITS = ['deg', 'rad', 'grad', 'turn'] + +const IS_ANGLE = new RegExp(`^${HAS_NUMBER.source}(${ANGLE_UNITS.join('|')})$`) + +function isAngle(value) { + return IS_ANGLE.test(value) +} + +const IS_VECTOR = new RegExp(`^${HAS_NUMBER.source} +${HAS_NUMBER.source} +${HAS_NUMBER.source}$`) + +function isVector(value) { + return IS_VECTOR.test(value) +} diff --git a/src/utils/is-color.ts b/src/tailwind/utils/is-color.js similarity index 94% rename from src/utils/is-color.ts rename to src/tailwind/utils/is-color.js index 76184b7..5cc8e8a 100644 --- a/src/utils/is-color.ts +++ b/src/tailwind/utils/is-color.js @@ -1,10 +1,6 @@ -import { segment } from "./segment"; -import get from "lodash/get"; - const HASH = 0x23 const NAMED_COLORS = new Set([ - // CSS Level 1 colors 'black', 'silver', 'gray', @@ -22,7 +18,6 @@ const NAMED_COLORS = new Set([ 'teal', 'aqua', - // CSS Level 2/3 colors 'aliceblue', 'antiquewhite', 'aqua', @@ -172,11 +167,9 @@ const NAMED_COLORS = new Set([ 'yellow', 'yellowgreen', - // Keywords 'transparent', 'currentcolor', - // System colors 'canvas', 'canvastext', 'linktext', @@ -200,7 +193,7 @@ const NAMED_COLORS = new Set([ const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix)\(/i -export function isColor(value: string): boolean { +export function isColor(value) { if (!value) return false return ( diff --git a/src/tailwind/utils/math-operators.js b/src/tailwind/utils/math-operators.js new file mode 100644 index 0000000..ac77680 --- /dev/null +++ b/src/tailwind/utils/math-operators.js @@ -0,0 +1,104 @@ +const mathFunctions = [ + 'calc', + 'min', + 'max', + 'clamp', + 'mod', + 'rem', + 'sin', + 'cos', + 'tan', + 'asin', + 'acos', + 'atan', + 'atan2', + 'pow', + 'sqrt', + 'hypot', + 'log', + 'exp', + 'round', +] + +export function hasMathFn(input) { + return input.indexOf('(') !== -1 && mathFunctions.some(fn => input.includes(`${fn}(`)) +} + +export function addWhitespaceAroundMathOperators(input) { + if (input.indexOf('(') === -1) { + return input + } + + if (!mathFunctions.some(fn => input.includes(fn))) { + return input + } + + let result = '' + const formattable = [] + + for (let i = 0; i < input.length; i++) { + const char = input[i] + + if (char === '(') { + result += char + + let start = i + + for (let j = i - 1; j >= 0; j--) { + const inner = input.charCodeAt(j) + + if (inner >= 48 && inner <= 57) { + start = j + } else if (inner >= 97 && inner <= 122) { + start = j + } else { + break + } + } + + const fn = input.slice(start, i) + + if (mathFunctions.includes(fn)) { + formattable.unshift(true) + continue + } else if (formattable[0] && fn === '') { + formattable.unshift(true) + continue + } + + formattable.unshift(false) + continue + } else if (char === ')') { + result += char + formattable.shift() + } else if (char === ',' && formattable[0]) { + result += `, ` + continue + } else if (char === ' ' && formattable[0] && result[result.length - 1] === ' ') { + continue + } else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { + const trimmed = result.trimEnd() + const prev = trimmed[trimmed.length - 1] + + if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { + result += char + continue + } else if (prev === '(' || prev === ',') { + result += char + continue + } else if (input[i - 1] === ' ') { + result += `${char} ` + } else { + result += ` ${char} ` + } + } else if (formattable[0] && input.startsWith('to-zero', i)) { + const start = i + i += 7 + result += input.slice(start, i + 1) + } else { + result += char + } + } + + return result +} diff --git a/src/tailwind/utils/parse-variant.js b/src/tailwind/utils/parse-variant.js new file mode 100644 index 0000000..96fa3e9 --- /dev/null +++ b/src/tailwind/utils/parse-variant.js @@ -0,0 +1,28 @@ +import { variants } from '../plugins'; + +import { decodeArbitraryValue } from './decode-arbitrary-value'; + +export function parseVariant(variant) { + if (variant[0] === '[' && variant[variant.length - 1] === ']') { + let arbitraryValue = variant.slice(1, -1) + + if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') { + arbitraryValue = `var(${arbitraryValue})` + } else { + arbitraryValue = decodeArbitraryValue(arbitraryValue) + } + + return { + kind: 'arbitrary', + type: 'misc', + value: arbitraryValue, + } + } + + const matchedVariantType = variants.get(variant) + return { + kind: 'named', + type: matchedVariantType || 'misc', + value: variant, + } +} diff --git a/src/tailwind/utils/segment.js b/src/tailwind/utils/segment.js new file mode 100644 index 0000000..b5e90f3 --- /dev/null +++ b/src/tailwind/utils/segment.js @@ -0,0 +1,75 @@ +const BACKSLASH = 0x5c +const OPEN_CURLY = 0x7b +const CLOSE_CURLY = 0x7d +const OPEN_PAREN = 0x28 +const CLOSE_PAREN = 0x29 +const OPEN_BRACKET = 0x5b +const CLOSE_BRACKET = 0x5d +const DOUBLE_QUOTE = 0x22 +const SINGLE_QUOTE = 0x27 + +const closingBracketStack = new Uint8Array(256) + +export function segment(input, separator) { + let stackPos = 0 + const parts = [] + let lastPos = 0 + const len = input.length + + const separatorCode = separator.charCodeAt(0) + + for (let idx = 0; idx < len; idx++) { + const char = input.charCodeAt(idx) + + if (stackPos === 0 && char === separatorCode) { + parts.push(input.slice(lastPos, idx)) + lastPos = idx + 1 + continue + } + + switch (char) { + case BACKSLASH: + idx += 1 + break + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + // Ensure we don't go out of bounds. + while (++idx < len) { + const nextChar = input.charCodeAt(idx) + + if (nextChar === BACKSLASH) { + idx += 1 + continue + } + + if (nextChar === char) { + break + } + } + break + case OPEN_PAREN: + closingBracketStack[stackPos] = CLOSE_PAREN + stackPos++ + break + case OPEN_BRACKET: + closingBracketStack[stackPos] = CLOSE_BRACKET + stackPos++ + break + case OPEN_CURLY: + closingBracketStack[stackPos] = CLOSE_CURLY + stackPos++ + break + case CLOSE_BRACKET: + case CLOSE_CURLY: + case CLOSE_PAREN: + if (stackPos > 0 && char === closingBracketStack[stackPos - 1]) { + stackPos-- + } + break + } + } + + parts.push(input.slice(lastPos)) + + return parts +} diff --git a/src/utils/string-builder.js b/src/tailwind/utils/string-builder.js similarity index 100% rename from src/utils/string-builder.js rename to src/tailwind/utils/string-builder.js diff --git a/src/theme.ts b/src/theme.ts deleted file mode 100644 index a135147..0000000 --- a/src/theme.ts +++ /dev/null @@ -1,6 +0,0 @@ -import memoize from 'lodash/memoize' - -export const getTailwindTheme = memoize((config: any | undefined = {}): any => { - const parsedConfig = config || {} - return parsedConfig.theme || {} -}) diff --git a/src/utils/build-modifier.js b/src/utils/build-modifier.js deleted file mode 100644 index 33c8f6b..0000000 --- a/src/utils/build-modifier.js +++ /dev/null @@ -1,38 +0,0 @@ -import { StringBuilder } from './string-builder'; - -/* - * possible modifiers - * 80% 0.80 80 32 - * ^^ this is not valid - * value must either between 0-1 or between 0-100 - * with multiples of 5 or has % at the end. - */ - -export const buildModifier = (modifier, opacityScale = {}) => { - if (!modifier) return '' - - if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') { - modifier = modifier.slice(1, -1) - } - - if (modifier[modifier.length - 1] === '%') { - return StringBuilder.makeArbitrary(modifier) - } - - for (const [key, value] of Object.entries(opacityScale)) { - if (key === modifier || value === modifier) { - return key - } - } - - if ((Number(modifier) === 0 || Number(modifier) >= 1) && Number(modifier) <= 100) { - return StringBuilder.makeArbitrary(`${modifier}%`) - } - - if (Number(modifier) >= 0 && Number(modifier) <= 1) { - // we have number between 0-1 but it's not in the scale just make it arbitrary. - return StringBuilder.makeArbitrary(modifier) - } - - return '' -} diff --git a/src/utils/decodeArbitraryValue.ts b/src/utils/decodeArbitraryValue.ts deleted file mode 100644 index 17ab2b5..0000000 --- a/src/utils/decodeArbitraryValue.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { addWhitespaceAroundMathOperators } from './math-operators' - -export function decodeArbitraryValue(input: string): string { - // We do not want to normalize anything inside of a url() because if we - // replace `_` with ` `, then it will very likely break the url. - if (input.startsWith('url(')) { - return input - } - - input = convertUnderscoresToWhitespace(input) - input = addWhitespaceAroundMathOperators(input) - - return input -} - -/** - * Convert `_` to ` `, except for escaped underscores `\_` they should be - * converted to `_` instead. - */ -function convertUnderscoresToWhitespace(input: string) { - let output = '' - for (let i = 0; i < input.length; i++) { - let char = input[i] - - // Escaped underscore - if (char === '\\' && input[i + 1] === '_') { - output += '_' - i += 1 - } - - // Unescaped underscore - else if (char === '_') { - output += ' ' - } - - // All other characters - else { - output += char - } - } - - return output -} \ No newline at end of file diff --git a/src/utils/find-tailwind-color-from-hex.js b/src/utils/find-tailwind-color-from-hex.js deleted file mode 100644 index b485013..0000000 --- a/src/utils/find-tailwind-color-from-hex.js +++ /dev/null @@ -1,19 +0,0 @@ -import { colord } from 'colord'; - -export const findTailwindColorFromHex = (colorInput, colors = {}) => { - if (!colorInput) return false - - for (const [key, twColors] of Object.entries(colors)) { - if ((twColors === '#fff' || twColors === '#000') && colord(colorInput).isEqual(twColors)) { - return key - } else { - for (const [shade, hex] of Object.entries(twColors)) { - if (hex === colorInput) { - return `${key}-${shade}` - } - } - } - } - - return false -} diff --git a/src/utils/infer-data-type.ts b/src/utils/infer-data-type.ts deleted file mode 100644 index c818df5..0000000 --- a/src/utils/infer-data-type.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { isColor } from './is-color' -import { hasMathFn } from './math-operators' -import { segment } from './segment' - -export type DataType = - | 'color' - | 'length' - | 'percentage' - | 'number' - | 'url' - | 'position' - | 'bg-size' - | 'line-width' - | 'image' - | 'family-name' - | 'generic-name' - | 'absolute-size' - | 'relative-size' - | 'angle' - | 'vector' - | 'named' - -const checks: Record boolean> = { - color: isColor, - length: isLength, - percentage: isPercentage, - number: isNumber, - url: isUrl, - position: isBackgroundPosition, - 'bg-size': isBackgroundSize, - 'line-width': isLineWidth, - image: isImage, - 'family-name': isFamilyName, - 'generic-name': isGenericName, - 'absolute-size': isAbsoluteSize, - 'relative-size': isRelativeSize, - angle: isAngle, - vector: isVector, - named: () => true -} - -/** - * Determine the type of `value` using syntax rules from CSS specs. - */ -export function inferDataType(value: string, types: DataType[]): DataType | null { - if (value.startsWith('var(')) return null - - for (let type of types) { - if (checks[type]?.(value)) { - return type - } - } - - return null -} - -/* -------------------------------------------------------------------------- */ - -const IS_URL = /^url\(.*\)$/ - -function isUrl(value: string): boolean { - return IS_URL.test(value) -} - -/* -------------------------------------------------------------------------- */ - -function isLineWidth(value: string): boolean { - return value === 'thin' || value === 'medium' || value === 'thick' -} - -/* -------------------------------------------------------------------------- */ - -const IS_IMAGE_FN = /^(?:element|image|cross-fade|image-set)\(/ -const IS_GRADIENT_FN = /^(repeating-)?(conic|linear|radial)-gradient\(/ - -function isImage(value: string) { - let count = 0 - - for (let part of segment(value, ',')) { - if (part.startsWith('var(')) continue - - if (isUrl(part)) { - count += 1 - continue - } - - if (IS_GRADIENT_FN.test(part)) { - count += 1 - continue - } - - if (IS_IMAGE_FN.test(part)) { - count += 1 - continue - } - - return false - } - - return count > 0 -} - -/* -------------------------------------------------------------------------- */ - -function isGenericName(value: string): boolean { - return ( - value === 'serif' || - value === 'sans-serif' || - value === 'monospace' || - value === 'cursive' || - value === 'fantasy' || - value === 'system-ui' || - value === 'ui-serif' || - value === 'ui-sans-serif' || - value === 'ui-monospace' || - value === 'ui-rounded' || - value === 'math' || - value === 'emoji' || - value === 'fangsong' - ) -} - -function isFamilyName(value: string): boolean { - let count = 0 - - for (let part of segment(value, ',')) { - // If it starts with a digit, then it's not a family name - let char = part.charCodeAt(0) - if (char >= 48 && char <= 57) return false - - if (part.startsWith('var(')) continue - - count += 1 - } - - return count > 0 -} - -function isAbsoluteSize(value: string): boolean { - return ( - value === 'xx-small' || - value === 'x-small' || - value === 'small' || - value === 'medium' || - value === 'large' || - value === 'x-large' || - value === 'xx-large' || - value === 'xxx-large' - ) -} - -function isRelativeSize(value: string): boolean { - return value === 'larger' || value === 'smaller' -} - -/* -------------------------------------------------------------------------- */ - -const HAS_NUMBER = /[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?/ - -/* -------------------------------------------------------------------------- */ - -const IS_NUMBER = new RegExp(`^${HAS_NUMBER.source}$`) - -function isNumber(value: string): boolean { - return IS_NUMBER.test(value) || hasMathFn(value) -} - -/* -------------------------------------------------------------------------- */ - -const IS_PERCENTAGE = new RegExp(`^${HAS_NUMBER.source}%$`) - -function isPercentage(value: string): boolean { - return IS_PERCENTAGE.test(value) || hasMathFn(value) -} - -/* -------------------------------------------------------------------------- */ - -/** - * Please refer to MDN when updating this list: - * @see https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries#container_query_length_units - */ -const LENGTH_UNITS = [ - 'cm', - 'mm', - 'Q', - 'in', - 'pc', - 'pt', - 'px', - 'em', - 'ex', - 'ch', - 'rem', - 'lh', - 'rlh', - 'vw', - 'vh', - 'vmin', - 'vmax', - 'vb', - 'vi', - 'svw', - 'svh', - 'lvw', - 'lvh', - 'dvw', - 'dvh', - 'cqw', - 'cqh', - 'cqi', - 'cqb', - 'cqmin', - 'cqmax', -] - -const IS_LENGTH = new RegExp(`^${HAS_NUMBER.source}(${LENGTH_UNITS.join('|')})$`) - -function isLength(value: string): boolean { - return IS_LENGTH.test(value) || hasMathFn(value) -} - -/* -------------------------------------------------------------------------- */ - -function isBackgroundPosition(value: string): boolean { - let count = 0 - - for (let part of segment(value, ' ')) { - if ( - part === 'center' || - part === 'top' || - part === 'right' || - part === 'bottom' || - part === 'left' - ) { - count += 1 - continue - } - - if (part.startsWith('var(')) continue - - if (isLength(part) || isPercentage(part)) { - count += 1 - continue - } - - return false - } - - return count > 0 -} - -/* -------------------------------------------------------------------------- */ - -/** - * Determine if `value` is valid for `background-size` - * - * background-size = - * # - * - * = - * [ | auto ]{1,2} | - * cover | - * contain - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/background-size#formal_syntax - */ -function isBackgroundSize(value: string) { - let count = 0 - - for (let size of segment(value, ',')) { - if (size === 'cover' || size === 'contain') { - count += 1 - continue - } - - let values = segment(size, ' ') - - // Sizes must have exactly one or two values - if (values.length !== 1 && values.length !== 2) { - return false - } - - if (values.every((value) => value === 'auto' || isLength(value) || isPercentage(value))) { - count += 1 - } - } - - return count > 0 -} - -/* -------------------------------------------------------------------------- */ - -const ANGLE_UNITS = ['deg', 'rad', 'grad', 'turn'] - -const IS_ANGLE = new RegExp(`^${HAS_NUMBER.source}(${ANGLE_UNITS.join('|')})$`) - -/** - * Determine if `value` is valid angle - * - * = - * = deg | rad | grad | turn - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/angle - */ -function isAngle(value: string) { - return IS_ANGLE.test(value) -} - -/* -------------------------------------------------------------------------- */ - -const IS_VECTOR = new RegExp(`^${HAS_NUMBER.source} +${HAS_NUMBER.source} +${HAS_NUMBER.source}$`) - -/** - * Determine if `value` is valid for the vector component of `rotate` - * - * = - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/rotate#vector_plus_angle_value - */ -function isVector(value: string) { - return IS_VECTOR.test(value) -} \ No newline at end of file diff --git a/src/utils/math-operators.ts b/src/utils/math-operators.ts deleted file mode 100644 index 1105556..0000000 --- a/src/utils/math-operators.ts +++ /dev/null @@ -1,154 +0,0 @@ -const mathFunctions = [ - 'calc', - 'min', - 'max', - 'clamp', - 'mod', - 'rem', - 'sin', - 'cos', - 'tan', - 'asin', - 'acos', - 'atan', - 'atan2', - 'pow', - 'sqrt', - 'hypot', - 'log', - 'exp', - 'round', -] - -export function hasMathFn(input: string) { - return input.indexOf('(') !== -1 && mathFunctions.some((fn) => input.includes(`${fn}(`)) -} - -export function addWhitespaceAroundMathOperators(input: string) { - // There's definitely no functions in the input, so bail early - if (input.indexOf('(') === -1) { - return input - } - - // Bail early if there are no math functions in the input - if (!mathFunctions.some((fn) => input.includes(fn))) { - return input - } - - let result = '' - let formattable: boolean[] = [] - - for (let i = 0; i < input.length; i++) { - let char = input[i] - - // Determine if we're inside a math function - if (char === '(') { - result += char - - // Scan backwards to determine the function name. This assumes math - // functions are named with lowercase alphanumeric characters. - let start = i - - for (let j = i - 1; j >= 0; j--) { - let inner = input.charCodeAt(j) - - if (inner >= 48 && inner <= 57) { - start = j // 0-9 - } else if (inner >= 97 && inner <= 122) { - start = j // a-z - } else { - break - } - } - - let fn = input.slice(start, i) - - // This is a known math function so start formatting - if (mathFunctions.includes(fn)) { - formattable.unshift(true) - continue - } - - // We've encountered nested parens inside a math function, record that and - // keep formatting until we've closed all parens. - else if (formattable[0] && fn === '') { - formattable.unshift(true) - continue - } - - // This is not a known math function so don't format it - formattable.unshift(false) - continue - } - - // We've exited the function so format according to the parent function's - // type. - else if (char === ')') { - result += char - formattable.shift() - } - - // Add spaces after commas in math functions - else if (char === ',' && formattable[0]) { - result += `, ` - continue - } - - // Skip over consecutive whitespace - else if (char === ' ' && formattable[0] && result[result.length - 1] === ' ') { - continue - } - - // Add whitespace around operators inside math functions - else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) { - let trimmed = result.trimEnd() - let prev = trimmed[trimmed.length - 1] - - // If we're preceded by an operator don't add spaces - if (prev === '+' || prev === '*' || prev === '/' || prev === '-') { - result += char - continue - } - - // If we're at the beginning of an argument don't add spaces - else if (prev === '(' || prev === ',') { - result += char - continue - } - - // Add spaces only after the operator if we already have spaces before it - else if (input[i - 1] === ' ') { - result += `${char} ` - } - - // Add spaces around the operator - else { - result += ` ${char} ` - } - } - - // Skip over `to-zero` when in a math function. - // - // This is specifically to handle this value in the round(…) function: - // - // ``` - // round(to-zero, 1px) - // ^^^^^^^ - // ``` - // - // This is because the first argument is optionally a keyword and `to-zero` - // contains a hyphen and we want to avoid adding spaces inside it. - else if (formattable[0] && input.startsWith('to-zero', i)) { - let start = i - i += 7 - result += input.slice(start, i + 1) - } - - // Handle all other characters - else { - result += char - } - } - - return result -} \ No newline at end of file diff --git a/src/utils/segment.ts b/src/utils/segment.ts deleted file mode 100644 index e31176a..0000000 --- a/src/utils/segment.ts +++ /dev/null @@ -1,102 +0,0 @@ -const BACKSLASH = 0x5c -const OPEN_CURLY = 0x7b -const CLOSE_CURLY = 0x7d -const OPEN_PAREN = 0x28 -const CLOSE_PAREN = 0x29 -const OPEN_BRACKET = 0x5b -const CLOSE_BRACKET = 0x5d -const DOUBLE_QUOTE = 0x22 -const SINGLE_QUOTE = 0x27 - -// This is a shared buffer that is used to keep track of the current nesting level -// of parens, brackets, and braces. It is used to determine if a character is at -// the top-level of a string. This is a performance optimization to avoid memory -// allocations on every call to `segment`. -const closingBracketStack = new Uint8Array(256) - -/** - * This splits a string on a top-level character. - * - * Regex doesn't support recursion (at least not the JS-flavored version), - * so we have to use a tiny state machine to keep track of paren placement. - * - * Expected behavior using commas: - * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) - * ┬ ┬ ┬ ┬ - * x x x ╰──────── Split because top-level - * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens - */ -export function segment(input: string, separator: string) { - // SAFETY: We can use an index into a shared buffer because this function is - // synchronous, non-recursive, and runs in a single-threaded environment. - let stackPos = 0 - let parts: string[] = [] - let lastPos = 0 - let len = input.length - - let separatorCode = separator.charCodeAt(0) - - for (let idx = 0; idx < len; idx++) { - let char = input.charCodeAt(idx) - - if (stackPos === 0 && char === separatorCode) { - parts.push(input.slice(lastPos, idx)) - lastPos = idx + 1 - continue - } - - switch (char) { - case BACKSLASH: - // The next character is escaped, so we skip it. - idx += 1 - break - // Strings should be handled as-is until the end of the string. No need to - // worry about balancing parens, brackets, or curlies inside a string. - case SINGLE_QUOTE: - case DOUBLE_QUOTE: - // Ensure we don't go out of bounds. - while (++idx < len) { - let nextChar = input.charCodeAt(idx) - - // The next character is escaped, so we skip it. - if (nextChar === BACKSLASH) { - idx += 1 - continue - } - - if (nextChar === char) { - break - } - } - break - case OPEN_PAREN: - closingBracketStack[stackPos] = CLOSE_PAREN - stackPos++ - break - case OPEN_BRACKET: - closingBracketStack[stackPos] = CLOSE_BRACKET - stackPos++ - break - case OPEN_CURLY: - closingBracketStack[stackPos] = CLOSE_CURLY - stackPos++ - break - case CLOSE_BRACKET: - case CLOSE_CURLY: - case CLOSE_PAREN: - if (stackPos > 0 && char === closingBracketStack[stackPos - 1]) { - // SAFETY: The buffer does not need to be mutated because the stack is - // only ever read from or written to its current position. Its current - // position is only ever incremented after writing to it. Meaning that - // the buffer can be dirty for the next use and still be correct since - // reading/writing always starts at position `0`. - stackPos-- - } - break - } - } - - parts.push(input.slice(lastPos)) - - return parts -} \ No newline at end of file diff --git a/src/utils/value.ts b/src/utils/value.ts deleted file mode 100644 index 1abefe9..0000000 --- a/src/utils/value.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type {DataType} from "./infer-data-type"; -import type {FunctionalPlugin} from "../plugins"; -import {UnmatchedValueException} from "../exceptions/unmatched-value-exception"; -import get from "lodash/get"; - -export type Value = { - kind: DataType, - value: string, - raw: string, - class: string[] -} - -export const getValue = (value: string, plugin: FunctionalPlugin, scale: any): Value => { - if (plugin.type === "color") { - let matchedColor = get(scale, value.split('-').join('.')) - if (!matchedColor) { - throw new UnmatchedValueException(plugin.ns, value) - } - if (typeof matchedColor === 'object' && matchedColor.DEFAULT) { - { - matchedColor = matchedColor.DEFAULT - } - } - return { - value: matchedColor, - kind: plugin.type, - class: plugin.class, - raw: value, - } - } - - const matchedValue = scale[value] - if (!matchedValue) { - throw new UnmatchedValueException(plugin.ns, value) - } - - return { - value: matchedValue, - class: plugin.class, - raw: value, - kind: plugin.type - } -} \ No newline at end of file From 181d8b7c8a1b0f7a0be590f8d9f65688cefa71c6 Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Tue, 11 Nov 2025 14:06:18 -0300 Subject: [PATCH 4/7] fix build --- index.html | 4 +- package.json | 12 +- src/index.js | 2 + test/classname.test.ts | 27 ---- test/parse.test.ts | 343 ----------------------------------------- tsconfig.json | 26 ---- vite.config.js | 6 +- 7 files changed, 8 insertions(+), 412 deletions(-) create mode 100644 src/index.js delete mode 100644 test/classname.test.ts delete mode 100644 test/parse.test.ts delete mode 100644 tsconfig.json diff --git a/index.html b/index.html index d475388..7a8fef7 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,10 @@ - Vite + TS + Vite
- + diff --git a/package.json b/package.json index 40221ab..fcfe14c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,13 @@ { - "name": "tailwind-parser", + "name": "tailwindcss-class-parser", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview", - "test": "vitest" + "preview": "vite preview" }, - "types": "./dist/tailwindcss-class-parser.es.d.ts", "main": "./dist/tailwindcss-class-parser.umd.cjs", "module": "./dist/tailwindcss-class-parser.js", "exports": { @@ -22,11 +20,7 @@ "tailwindcss": "*" }, "devDependencies": { - "@types/node": "^22.7.7", - "typescript": "^5.6.3", - "vite": "^5.4.9", - "vite-plugin-dts": "^4.2.4", - "vitest": "^2.1.3" + "vite": "^5.4.9" }, "publishConfig": { "access": "public" diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6dce6d1 --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +export { parse, classname } from './tailwind/index.js' + diff --git a/test/classname.test.ts b/test/classname.test.ts deleted file mode 100644 index c675598..0000000 --- a/test/classname.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {expect, test} from 'vitest' -import {classname} from "../src"; - -const table = [ - [{property: "display", value: "flex"}, "flex"], - [{property: "display", value: "flex", important: true}, "!flex"], - [{property: "backgroundColor", value: "#ff0000"}, "bg-[#ff0000]"], - [{property: "backgroundColor", value: "red-500"}, "bg-red-500"], - [{property: "backgroundColor", value: "#ef4444"}, "bg-red-500"], - [{property: "backgroundColor", value: "#ef4444", modifier: 30}, "bg-red-500/30"], - [{property: "backgroundColor", value: "#ef4444", modifier: 32}, "bg-red-500/[32%]"], - [{property: "backgroundColor", value: "#ef44444f"}, "bg-red-500/[0.31]"], - [{property: "backgroundColor", value: "rgba(239, 68, 68, 0.3)"}, "bg-red-500/30"], - [{property: "backgroundColor", value: "hsla(0, 84%, 60.1%, 0.3)"}, "bg-red-500/30"], - [{property: "fontSize", value: "1rem"}, "text-base"], - [{property: "fontSize", value: "12px"}, "text-[12px]"], - [{property: "margin", value: "12px"}, "m-[12px]"], - [{property: "marginRight", value: "1rem"}, "mr-4"], - [{property: "marginRight", value: "-1rem"}, "-mr-4"], - [{property: "marginRight", value: "1rem", negative: true}, "-mr-4"], - [{property: "width", value: "50%"}, "w-1/2"], -] - -//@ts-ignore -test.each(table)('should parse declaration into tailwindcss classes: "%s" -> "%s"', (input: any, expected: any) => { - expect(classname(input)).toEqual(expected) -}) diff --git a/test/parse.test.ts b/test/parse.test.ts deleted file mode 100644 index 1816791..0000000 --- a/test/parse.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -import {expect, it} from 'vitest' -import {parse} from "../src"; - -it('should parse a functional class', () => { - expect(parse("bg-red-500")).toEqual( - { - "root": "bg", - "kind": "functional", - "property": "backgroundColor", - "value": "#ef4444", - "valueDef": { - "value": "#ef4444", - "kind": "color", - "class": [ - "background-color" - ], - "raw": "red-500" - }, - "variants": [], - "modifier": null, - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) - -it("should parse a named/static class", () => { - expect(parse("flex")).toEqual( - { - "root": "flex", - "kind": "named", - "property": "display", - "value": "flex", - "valueDef": { - "class": [ - "display" - ], - "raw": "flex", - "kind": "named", - "value": "flex" - }, - "variants": [], - "modifier": null, - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) -it("should parse a functional class with arbitrary modifier", () => { - expect(parse("bg-gray-50/[50%]")).toEqual( - { - "root": "bg", - "kind": "functional", - "property": "backgroundColor", - "value": "#f9fafb", - "valueDef": { - "value": "#f9fafb", - "kind": "color", - "class": [ - "background-color" - ], - "raw": "gray-50" - }, - "variants": [], - "modifier": "[50%]", - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) -it("should parse a functional class with modifier", () => { - expect(parse("bg-gray-50/20")).toEqual( - { - "root": "bg", - "kind": "functional", - "property": "backgroundColor", - "value": "#f9fafb", - "valueDef": { - "value": "#f9fafb", - "kind": "color", - "class": [ - "background-color" - ], - "raw": "gray-50" - }, - "variants": [], - "modifier": "20", - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) -it("should parse a functional class with variants", () => { - expect(parse("lg:hover:text-red-500")).toEqual( - { - "root": "text", - "kind": "functional", - "property": "textColor", - "value": "#ef4444", - "valueDef": { - "value": "#ef4444", - "kind": "color", - "class": [ - "color" - ], - "raw": "red-500" - }, - "variants": [ - { - "kind": "named", - "type": "interaction", - "value": "hover" - }, - { - "kind": "named", - "type": "media", - "value": "lg" - } - ], - "modifier": null, - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) -it("w-1/2", () => { - expect(parse("w-1/2")).toEqual( - { - "root": "w", - "kind": "functional", - "property": "width", - "value": "50%", - "valueDef": { - "value": "50%", - "class": [ - "width" - ], - "raw": "1/2", - "kind": "length" - }, - "variants": [], - "modifier": null, - "important": false, - "negative": false, - "arbitrary": false - } - ) -}) -it("should parse rounded corner classes", () => { - expect(parse("rounded-tl-lg")).toEqual({ - arbitrary: false, - important: false, - kind: "functional", - modifier: null, - negative: false, - property: "borderTopLeftRadius", - root: "rounded-tl", - value: "0.5rem", - valueDef: { - class: ["border-top-left-radius"], - kind: "length", - raw: "lg", - value: "0.5rem", - }, - variants: [], - }); - expect(parse("rounded-tr-lg")).toEqual({ - arbitrary: false, - important: false, - kind: "functional", - modifier: null, - negative: false, - property: "borderTopRightRadius", - root: "rounded-tr", - value: "0.5rem", - valueDef: { - class: ["border-top-right-radius"], - kind: "length", - raw: "lg", - value: "0.5rem", - }, - variants: [], - }); - expect(parse("rounded-bl-lg")).toEqual({ - arbitrary: false, - important: false, - kind: "functional", - modifier: null, - negative: false, - property: "borderBottomLeftRadius", - root: "rounded-bl", - value: "0.5rem", - valueDef: { - class: ["border-bottom-left-radius"], - kind: "length", - raw: "lg", - value: "0.5rem", - }, - variants: [], - }); - expect(parse("rounded-br-lg")).toEqual({ - arbitrary: false, - important: false, - kind: "functional", - modifier: null, - negative: false, - property: "borderBottomRightRadius", - root: "rounded-br", - value: "0.5rem", - valueDef: { - class: ["border-bottom-right-radius"], - kind: "length", - raw: "lg", - value: "0.5rem", - }, - variants: [], - }); -}); -it('should parse flex-basis classes', () => { - expect(parse('basis-full')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "named", - "modifier": null, - "negative": false, - "property": "flexBasis", - "root": "basis-full", - "value": "100%", - "valueDef": { - "class": [ - "flex-basis", - ], - "kind": "named", - "raw": "basis-full", - "value": "100%", - }, - "variants": [], - }) - expect(parse('basis-auto')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "named", - "modifier": null, - "negative": false, - "property": "flexBasis", - "root": "basis-auto", - "value": "auto", - "valueDef": { - "class": [ - "flex-basis", - ], - "kind": "named", - "raw": "basis-auto", - "value": "auto", - }, - "variants": [], - }) - expect(parse('basis-7')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "property": "flexBasis", - "root": "basis", - "value": "1.75rem", - "valueDef": { - "class": [ - "flex-basis", - ], - "kind": "length", - "raw": "7", - "value": "1.75rem", - }, - "variants": [], - }) -}) -it('should parse inset classes', () => { - expect(parse('inset-2')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "property": "inset", - "root": "inset", - "value": "0.5rem", - "valueDef": { - "class": [ - "inset", - ], - "kind": "length", - "raw": "2", - "value": "0.5rem", - }, - "variants": [], - }) - expect(parse('inset-y-2')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "property": "insetY", - "root": "inset-y", - "value": "0.5rem", - "valueDef": { - "class": [ - "top", - "bottom", - ], - "kind": "length", - "raw": "2", - "value": "0.5rem", - }, - "variants": [], - }) - expect(parse('inset-x-2')).toEqual({ - "arbitrary": false, - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "property": "insetX", - "root": "inset-x", - "value": "0.5rem", - "valueDef": { - "class": [ - "left", - "right", - ], - "kind": "length", - "raw": "2", - "value": "0.5rem", - }, - "variants": [], - }) -}) diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 494777e..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "allowJs": true, - - "moduleResolution": "Bundler", - "allowImportingTsExtensions": false, - "verbatimModuleSyntax": true, - "isolatedModules": true, - "preserveConstEnums": true, - "declaration": true, - "declarationDir": "./dist/types", - "esModuleInterop": true, - - "skipLibCheck": true, - "strict": true, - "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true, - "outDir": "dist" - }, - "files": ["src/index.ts"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 8888581..3f9ff9d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,11 @@ // vite.config.js import {resolve} from 'path' import {defineConfig} from 'vite' -import dts from 'vite-plugin-dts' export default defineConfig({ - plugins: [ - dts({ rollupTypes: true }) - ], build: { lib: { - entry: resolve(__dirname, './src/index.ts'), + entry: resolve(__dirname, './src/index.js'), name: 'TailwindcssClassParser', fileName: 'tailwindcss-class-parser', }, From 9ebde5d9a70665500ff5592ed92ef44d98621704 Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Mon, 17 Nov 2025 14:39:35 -0300 Subject: [PATCH 5/7] fix bg preference --- src/tailwind/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tailwind/plugins.js b/src/tailwind/plugins.js index da9fba2..a88fd5e 100644 --- a/src/tailwind/plugins.js +++ b/src/tailwind/plugins.js @@ -87,10 +87,10 @@ export const functionalPlugins = new Map([ { scaleKey: "letterSpacing", ns: 'letterSpacing', class: ['letter-spacing'], type: 'length', supportNegative: true }, ]], ["bg", [ + { scaleKey: "backgroundColor", ns: 'backgroundColor', class: ['background-color'], type: 'color' }, { scaleKey: "backgroundImage", ns: 'backgroundImage', class: ['background-image'], type: 'image' }, { scaleKey: "backgroundPosition", ns: 'backgroundPosition', class: ['background-position'], type: 'position' }, { scaleKey: "backgroundSize", ns: 'backgroundSize', class: ['background-size'], type: 'bg-size' }, - { scaleKey: "backgroundColor", ns: 'backgroundColor', class: ['background-color'], type: 'color' }, ]], ["from", [ { scaleKey: "gradientColorStops", ns: 'gradientStopsFrom', class: ['--tw-gradient-from', '--tw-gradient-from-position'], type: 'color' }, From d45f3c367be51c7da752bc50350632adaa394b3b Mon Sep 17 00:00:00 2001 From: Inan Brunelli Date: Tue, 25 Nov 2025 13:28:18 -0300 Subject: [PATCH 6/7] fix exporting --- package.json | 17 ++++------------- src/{tailwind => }/classname.js | 0 .../exceptions/plugin-not-found-exception.js | 0 .../exceptions/unmatched-value-exception.js | 0 src/index.js | 4 ++-- src/{tailwind => }/parse.js | 0 src/{tailwind => }/plugins.js | 0 src/tailwind/index.js | 2 -- src/{tailwind => }/utils/build-modifier.js | 0 .../utils/calculate-hex-from-string.js | 0 .../utils/decode-arbitrary-value.js | 0 src/{tailwind => }/utils/find-root.js | 0 .../utils/find-tailwind-color-from-hex.js | 0 src/{tailwind => }/utils/infer-data-type.js | 0 src/{tailwind => }/utils/is-color.js | 0 src/{tailwind => }/utils/math-operators.js | 0 src/{tailwind => }/utils/parse-variant.js | 0 src/{tailwind => }/utils/segment.js | 0 src/{tailwind => }/utils/string-builder.js | 0 19 files changed, 6 insertions(+), 17 deletions(-) rename src/{tailwind => }/classname.js (100%) rename src/{tailwind => }/exceptions/plugin-not-found-exception.js (100%) rename src/{tailwind => }/exceptions/unmatched-value-exception.js (100%) rename src/{tailwind => }/parse.js (100%) rename src/{tailwind => }/plugins.js (100%) delete mode 100644 src/tailwind/index.js rename src/{tailwind => }/utils/build-modifier.js (100%) rename src/{tailwind => }/utils/calculate-hex-from-string.js (100%) rename src/{tailwind => }/utils/decode-arbitrary-value.js (100%) rename src/{tailwind => }/utils/find-root.js (100%) rename src/{tailwind => }/utils/find-tailwind-color-from-hex.js (100%) rename src/{tailwind => }/utils/infer-data-type.js (100%) rename src/{tailwind => }/utils/is-color.js (100%) rename src/{tailwind => }/utils/math-operators.js (100%) rename src/{tailwind => }/utils/parse-variant.js (100%) rename src/{tailwind => }/utils/segment.js (100%) rename src/{tailwind => }/utils/string-builder.js (100%) diff --git a/package.json b/package.json index fcfe14c..2a04753 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,13 @@ { "name": "tailwindcss-class-parser", "version": "0.1.0", - "private": true, "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "main": "./dist/tailwindcss-class-parser.umd.cjs", - "module": "./dist/tailwindcss-class-parser.js", + "main": "./src/index.js", + "module": "./src/index.js", "exports": { ".": { - "import": "./dist/tailwindcss-class-parser.js", - "require": "./dist/tailwindcss-class-parser.umd.cjs" + "import": "./src/index.js", + "require": "./src/index.js" } }, "peerDependencies": { @@ -22,9 +16,6 @@ "devDependencies": { "vite": "^5.4.9" }, - "publishConfig": { - "access": "public" - }, "dependencies": { "colord": "^2.9.3" } diff --git a/src/tailwind/classname.js b/src/classname.js similarity index 100% rename from src/tailwind/classname.js rename to src/classname.js diff --git a/src/tailwind/exceptions/plugin-not-found-exception.js b/src/exceptions/plugin-not-found-exception.js similarity index 100% rename from src/tailwind/exceptions/plugin-not-found-exception.js rename to src/exceptions/plugin-not-found-exception.js diff --git a/src/tailwind/exceptions/unmatched-value-exception.js b/src/exceptions/unmatched-value-exception.js similarity index 100% rename from src/tailwind/exceptions/unmatched-value-exception.js rename to src/exceptions/unmatched-value-exception.js diff --git a/src/index.js b/src/index.js index 6dce6d1..a817994 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,2 @@ -export { parse, classname } from './tailwind/index.js' - +export { parse } from './parse.js' +export { classname } from './classname.js' diff --git a/src/tailwind/parse.js b/src/parse.js similarity index 100% rename from src/tailwind/parse.js rename to src/parse.js diff --git a/src/tailwind/plugins.js b/src/plugins.js similarity index 100% rename from src/tailwind/plugins.js rename to src/plugins.js diff --git a/src/tailwind/index.js b/src/tailwind/index.js deleted file mode 100644 index 03cb421..0000000 --- a/src/tailwind/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { classname } from './classname' -export { parse } from './parse' diff --git a/src/tailwind/utils/build-modifier.js b/src/utils/build-modifier.js similarity index 100% rename from src/tailwind/utils/build-modifier.js rename to src/utils/build-modifier.js diff --git a/src/tailwind/utils/calculate-hex-from-string.js b/src/utils/calculate-hex-from-string.js similarity index 100% rename from src/tailwind/utils/calculate-hex-from-string.js rename to src/utils/calculate-hex-from-string.js diff --git a/src/tailwind/utils/decode-arbitrary-value.js b/src/utils/decode-arbitrary-value.js similarity index 100% rename from src/tailwind/utils/decode-arbitrary-value.js rename to src/utils/decode-arbitrary-value.js diff --git a/src/tailwind/utils/find-root.js b/src/utils/find-root.js similarity index 100% rename from src/tailwind/utils/find-root.js rename to src/utils/find-root.js diff --git a/src/tailwind/utils/find-tailwind-color-from-hex.js b/src/utils/find-tailwind-color-from-hex.js similarity index 100% rename from src/tailwind/utils/find-tailwind-color-from-hex.js rename to src/utils/find-tailwind-color-from-hex.js diff --git a/src/tailwind/utils/infer-data-type.js b/src/utils/infer-data-type.js similarity index 100% rename from src/tailwind/utils/infer-data-type.js rename to src/utils/infer-data-type.js diff --git a/src/tailwind/utils/is-color.js b/src/utils/is-color.js similarity index 100% rename from src/tailwind/utils/is-color.js rename to src/utils/is-color.js diff --git a/src/tailwind/utils/math-operators.js b/src/utils/math-operators.js similarity index 100% rename from src/tailwind/utils/math-operators.js rename to src/utils/math-operators.js diff --git a/src/tailwind/utils/parse-variant.js b/src/utils/parse-variant.js similarity index 100% rename from src/tailwind/utils/parse-variant.js rename to src/utils/parse-variant.js diff --git a/src/tailwind/utils/segment.js b/src/utils/segment.js similarity index 100% rename from src/tailwind/utils/segment.js rename to src/utils/segment.js diff --git a/src/tailwind/utils/string-builder.js b/src/utils/string-builder.js similarity index 100% rename from src/tailwind/utils/string-builder.js rename to src/utils/string-builder.js From 8dd1ecfb203e8f4f5281e6ed75fce13c4ee093d0 Mon Sep 17 00:00:00 2001 From: Francisco Sobrinho Date: Tue, 25 Nov 2025 15:09:54 -0300 Subject: [PATCH 7/7] Update package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2a04753..7ff3c10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss-class-parser", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "main": "./src/index.js", "module": "./src/index.js", @@ -19,4 +19,4 @@ "dependencies": { "colord": "^2.9.3" } -} \ No newline at end of file +}