diff --git a/__fixtures__/separator/separator.tsx b/__fixtures__/separator/separator.tsx new file mode 100644 index 0000000..58ee451 --- /dev/null +++ b/__fixtures__/separator/separator.tsx @@ -0,0 +1,4 @@ +// @ts-nocheck +import tw from './macro' + +tw`[&[data-foo][data-bar]:not([data-baz])]__underline` diff --git a/__fixtures__/separator/tailwind.config.js b/__fixtures__/separator/tailwind.config.js new file mode 100644 index 0000000..541456d --- /dev/null +++ b/__fixtures__/separator/tailwind.config.js @@ -0,0 +1,3 @@ +module.exports = { + separator: '__', +} diff --git a/__snapshots__/plugin.test.js.snap b/__snapshots__/plugin.test.js.snap index 72dcfe7..3abf8eb 100644 --- a/__snapshots__/plugin.test.js.snap +++ b/__snapshots__/plugin.test.js.snap @@ -64854,6 +64854,25 @@ tw\`snap-proximity\` }) +`; + +exports[`twin.macro separator.tsx: separator.tsx 1`] = ` + +// @ts-nocheck +import tw from './macro' + +tw\`[&[data-foo][data-bar]:not([data-baz])]__underline\` + + ↓ ↓ ↓ ↓ ↓ ↓ + +// @ts-nocheck +;({ + '&[data-foo][data-bar]:not([data-baz])': { + textDecorationLine: 'underline', + }, +}) + + `; exports[`twin.macro sepia.tsx: sepia.tsx 1`] = ` diff --git a/src/core/getStyles.ts b/src/core/getStyles.ts index eaa15f9..79eb510 100644 --- a/src/core/getStyles.ts +++ b/src/core/getStyles.ts @@ -13,6 +13,7 @@ import type { AssertContext, TailwindMatch, TailwindContext, + TailwindConfig, } from './types' const IMPORTANT_OUTSIDE_BRACKETS = @@ -63,7 +64,10 @@ function multilineReplaceWith( function validateClasses( classes: string, - { assert }: { assert: CoreContext['assert'] } + { + assert, + tailwindConfig, + }: { tailwindConfig: TailwindConfig; assert: CoreContext['assert'] } ): boolean { assert( (classes.match(/\[/g) ?? []).length === (classes.match(/]/g) ?? []).length, @@ -78,7 +82,7 @@ function validateClasses( for (const className of splitAtTopLevelOnly(classes, ' ')) { assert( - !className.endsWith(':'), + !className.endsWith(tailwindConfig.separator ?? ':'), ({ color }: AssertContext) => `${color( `✕ The variant ${String( @@ -93,12 +97,15 @@ function validateClasses( return true } -const tasks: Array<(classes: string) => string> = [ +const tasks: Array< + (classes: string, tailwindConfig: TailwindConfig) => string +> = [ (classes): string => classes.replace(CLASS_DIVIDER_PIPE, ' '), (classes): string => classes.replace(COMMENTS_MULTI_LINE, multilineReplaceWith), (classes): string => classes.replace(COMMENTS_SINGLE_LINE, ''), - expandVariantGroups, // Expand grouped variants to individual classes + (classes, tailwindConfig): string => + expandVariantGroups(classes, { tailwindConfig }), // Expand grouped variants to individual classes ] function bigSign(bigIntValue: bigint): number { @@ -163,11 +170,14 @@ function getStyles( )}\n\nRead more at https://twinredirect.page.link/template-literals` ) - const result = validateClasses(classes, { assert }) + const result = validateClasses(classes, { + tailwindConfig: params.tailwindConfig, + assert, + }) if (!result) return { styles: undefined, matched: [], unmatched: [] } for (const task of tasks) { - classes = task(classes) + classes = task(classes, params.tailwindConfig) } params.debug('classes after format', classes) @@ -184,6 +194,7 @@ function getStyles( const convertedClassNameContext = { ...commonContext, + tailwindConfig: params.tailwindConfig, isShortCssOnly: params.isShortCssOnly, disableShortCss: params.twinConfig.disableShortCss, } diff --git a/src/core/lib/convertClassName.ts b/src/core/lib/convertClassName.ts index 24755ff..ed3c0ca 100644 --- a/src/core/lib/convertClassName.ts +++ b/src/core/lib/convertClassName.ts @@ -11,17 +11,20 @@ const ALL_COMMAS = /,/g type ConvertShortCssToArbitraryPropertyParameters = { disableShortCss: CoreContext['twinConfig']['disableShortCss'] -} & Pick +} & Pick function convertShortCssToArbitraryProperty( className: string, { + tailwindConfig, assert, disableShortCss, isShortCssOnly, }: ConvertShortCssToArbitraryPropertyParameters ): string { - const splitArray = [...splitAtTopLevelOnly(className, ':')] + const splitArray = [ + ...splitAtTopLevelOnly(className, tailwindConfig.separator ?? ':'), + ] const lastValue = splitArray.slice(-1)[0] @@ -38,10 +41,10 @@ function convertShortCssToArbitraryProperty( const template = `${preSelector}[${[ property, value === '' ? "''" : value, - ].join(':')}]` + ].join(tailwindConfig.separator ?? ':')}]` splitArray.splice(-1, 1, template) - const arbitraryProperty = splitArray.join(':') + const arbitraryProperty = splitArray.join(tailwindConfig.separator ?? ':') const isShortCssDisabled = disableShortCss && !isShortCssOnly assert(!isShortCssDisabled, ({ color }) => @@ -64,12 +67,16 @@ function convertShortCssToArbitraryProperty( type ConvertClassNameParameters = { disableShortCss: CoreContext['twinConfig']['disableShortCss'] -} & Pick +} & Pick< + CoreContext, + 'tailwindConfig' | 'theme' | 'assert' | 'debug' | 'isShortCssOnly' +> // Convert a twin class to a tailwindcss friendly class function convertClassName( className: string, { + tailwindConfig, theme, isShortCssOnly, disableShortCss, @@ -84,10 +91,15 @@ function convertClassName( if (className.endsWith('!')) { debug('trailing bang found', className) - const splitArray = [...splitAtTopLevelOnly(className.slice(0, -1), ':')] + const splitArray = [ + ...splitAtTopLevelOnly( + className.slice(0, -1), + tailwindConfig.separator ?? ':' + ), + ] // Place a ! before the class splitArray.splice(-1, 1, `!${splitArray[splitArray.length - 1]}`) - className = splitArray.join(':') + className = splitArray.join(tailwindConfig.separator ?? ':') } // Convert short css to an arbitrary property, eg: `[display:block]` @@ -95,6 +107,7 @@ function convertClassName( if (isShortCss(className)) { debug('short css found', className) className = convertShortCssToArbitraryProperty(className, { + tailwindConfig, assert, disableShortCss, isShortCssOnly, diff --git a/src/suggestions/index.ts b/src/suggestions/index.ts index e5da2c9..762f140 100644 --- a/src/suggestions/index.ts +++ b/src/suggestions/index.ts @@ -58,7 +58,9 @@ function getVariantSuggestions( function getClassError(rawClass: string, context: ClassErrorContext): string { const input = rawClass.replace(ALL_SPACE_IDS, ' ') - const classPieces = [...splitAtTopLevelOnly(input, ':')] + const classPieces = [ + ...splitAtTopLevelOnly(input, context.tailwindConfig.separator ?? ':'), + ] for (const validator of validators) { const error = validator(classPieces, context) diff --git a/src/suggestions/lib/getClassSuggestions.ts b/src/suggestions/lib/getClassSuggestions.ts index b50a41d..6db6807 100644 --- a/src/suggestions/lib/getClassSuggestions.ts +++ b/src/suggestions/lib/getClassSuggestions.ts @@ -1,5 +1,7 @@ import stringSimilarity from 'string-similarity' -import type { ClassErrorContext } from 'suggestions/types' +import type { TailwindConfig, ClassErrorContext } from 'suggestions/types' + +const RATING_MINIMUM = 0.2 type RateCandidate = [number, string, string] @@ -7,16 +9,16 @@ function rateCandidate( classData: [string, string], className: string, matchee: string, - threshold = 0.2 + params: { tailwindConfig: TailwindConfig } ): RateCandidate | undefined { const [classEnd, value] = classData const candidate = `${[className, classEnd === 'DEFAULT' ? '' : classEnd] .filter(Boolean) - .join('-')}` + .join(params.tailwindConfig.separator)}` const rating = Number(stringSimilarity.compareTwoStrings(matchee, candidate)) - if (rating < threshold) return + if (rating < RATING_MINIMUM) return const classValue = `${String( (typeof value === 'string' && (value.length === 0 ? `''` : value)) ?? @@ -28,7 +30,8 @@ function rateCandidate( function extractCandidates( candidates: ClassErrorContext['candidates'], - matchee: string + matchee: string, + tailwindConfig: TailwindConfig ): RateCandidate[] { const results = [] as RateCandidate[] @@ -38,13 +41,17 @@ function extractCandidates( if (options?.values) { // Dynamic classes like mt-xxx, bg-xxx for (const value of Object.entries(options?.values)) { - const rated = rateCandidate(value, className, matchee) + const rated = rateCandidate(value, className, matchee, { + tailwindConfig, + }) // eslint-disable-next-line max-depth if (rated) results.push(rated) } } else { // Non-dynamic classes like fixed, block - const rated = rateCandidate(['', className], className, matchee) + const rated = rateCandidate(['', className], className, matchee, { + tailwindConfig, + }) if (rated) results.push(rated) } } @@ -59,7 +66,11 @@ export function getClassSuggestions( ): string { const { color } = context - const candidates = extractCandidates(context.candidates, matchee) + const candidates = extractCandidates( + context.candidates, + matchee, + context.tailwindConfig + ) const errorText = `${context.color( `✕ ${context.color(matchee, 'errorLight')} was not found`, diff --git a/src/suggestions/lib/validateVariants.ts b/src/suggestions/lib/validateVariants.ts index c070320..c91a5a8 100644 --- a/src/suggestions/lib/validateVariants.ts +++ b/src/suggestions/lib/validateVariants.ts @@ -25,10 +25,7 @@ export function validateVariants( .filter(Boolean) as Array<[string, number]> const errorText = `${context.color( - `✕ Variant ${context.color( - `${variantMatch}:`, - 'errorLight' - )} was not found`, + `✕ Variant ${context.color(`${variantMatch}`, 'errorLight')} was not found`, 'error' )}` @@ -37,7 +34,10 @@ export function validateVariants( const suggestions = results .sort(([, a]: [string, number], [, b]: [string, number]) => b - a) .slice(0, 4) - .map(([i]: [string, number]): string => `${i}:`) + .map( + ([i]: [string, number]): string => + `${i}${context.tailwindConfig.separator ?? ':'}` + ) const showMore = results.length > 2 && results[0][1] - results[1][1] < 0.1