From 57be02a983bf98a78c95b319fc6a047e2554ddf1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 20 Nov 2024 15:47:26 +0100 Subject: [PATCH] Improve `in-*` variant migrations (#15054) While testing the codemods on some projects, I noticed some issues with the migration to the new `in-*` variant. One such example is that we checked for `&` at the end, instead of ` &` (the whitespace is significant). This meant that `[figure>&]:my-0` was converted to `in-[figure>]:my-0` which is wrong. In this case, we want to keep it as `[figure>&]:my-0`. Additionally this PR brings back the migration from `group-[]:flex` to `in-[.group]:flex`. If you are using a prefix, then `group-[]:tw-flex` is migrated to `tw:in-[.tw\:group]:flex`. Last but not least, this does some internal refactors to group migrations logically together. --- CHANGELOG.md | 8 +- .../modernize-arbitrary-values.test.ts | 45 ++ .../codemods/modernize-arbitrary-values.ts | 597 +++++++++--------- 3 files changed, 359 insertions(+), 291 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 037f948f476a..247fe492c465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Added + +- _Upgrade (experimental)_: Convert `group-[]:flex` to `in-[.group]:flex` ([#15054](https://github.com/tailwindlabs/tailwindcss/pull/15054)) + +### Fixed + +- _Upgrade (experimental)_: Ensure migrating to the `in-*` requires a descendant selector ([#15054](https://github.com/tailwindlabs/tailwindcss/pull/15054)) ## [4.0.0-alpha.35] - 2024-11-20 diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts index 51d26a107eab..442971ed0d30 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.test.ts @@ -1,6 +1,7 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import { expect, test } from 'vitest' import { modernizeArbitraryValues } from './modernize-arbitrary-values' +import { prefix } from './prefix' test.each([ // Arbitrary variants @@ -22,6 +23,20 @@ test.each([ ['[p_&]:flex', 'in-[p]:flex'], ['[.foo_&]:flex', 'in-[.foo]:flex'], ['[[data-visible]_&]:flex', 'in-data-visible:flex'], + // Multiple selectors, should stay as-is + ['[[data-foo][data-bar]_&]:flex', '[[data-foo][data-bar]_&]:flex'], + // Using `>` instead of ` ` should not be transformed: + ['[figure>&]:my-0', '[figure>&]:my-0'], + // Some extreme examples of what happens in the wild: + ['group-[]:flex', 'in-[.group]:flex'], + ['group-[]/name:flex', 'in-[.group\\/name]:flex'], + + // These shouldn't happen in the real world (because compound variants are + // new). But this could happen once we allow codemods to run in v4+ projects. + ['has-group-[]:flex', 'has-in-[.group]:flex'], + ['has-group-[]/name:flex', 'has-in-[.group\\/name]:flex'], + ['not-group-[]:flex', 'not-in-[.group]:flex'], + ['not-group-[]/name:flex', 'not-in-[.group\\/name]:flex'], // nth-child ['[&:nth-child(2)]:flex', 'nth-2:flex'], @@ -77,3 +92,33 @@ test.each([ expect(modernizeArbitraryValues(designSystem, {}, candidate)).toEqual(result) }) + +test.each([ + // Should not prefix classes in arbitrary values + ['[.foo_&]:tw-flex', 'tw:in-[.foo]:flex'], + + // Should migrate `.group` classes + ['group-[]:tw-flex', 'tw:in-[.tw\\:group]:flex'], + ['group-[]/name:tw-flex', 'tw:in-[.tw\\:group\\/name]:flex'], + + // However, `.group` inside of an arbitrary variant should not be prefixed: + ['[.group_&]:tw-flex', 'tw:in-[.group]:flex'], + + // These shouldn't happen in the real world (because compound variants are + // new). But this could happen once we allow codemods to run in v4+ projects. + ['has-group-[]:tw-flex', 'tw:has-in-[.tw\\:group]:flex'], + ['has-group-[]/name:tw-flex', 'tw:has-in-[.tw\\:group\\/name]:flex'], + ['not-group-[]:tw-flex', 'tw:not-in-[.tw\\:group]:flex'], + ['not-group-[]/name:tw-flex', 'tw:not-in-[.tw\\:group\\/name]:flex'], +])('%s => %s', async (candidate, result) => { + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss" prefix(tw);', { + base: __dirname, + }) + + expect( + [prefix, modernizeArbitraryValues].reduce( + (acc, step) => step(designSystem, { prefix: 'tw-' }, acc), + candidate, + ), + ).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts b/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts index 3efa3f5ddd38..215b4533f79b 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/modernize-arbitrary-values.ts @@ -5,6 +5,14 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system' import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type' import { printCandidate } from '../candidates' +function memcpy(target: T, source: U): U { + // Clear out the target object, otherwise inspecting the final object will + // look very confusing. + for (let key in target) delete target[key] + + return Object.assign(target, source) +} + export function modernizeArbitraryValues( designSystem: DesignSystem, _userConfig: Config, @@ -28,336 +36,345 @@ export function modernizeArbitraryValues( } } - // Expecting an arbitrary variant - if (variant.kind !== 'arbitrary') continue - - // Expecting a non-relative arbitrary variant - if (variant.relative) continue - - let ast = SelectorParser().astSync(variant.selector) - - // Expecting a single selector node - if (ast.nodes.length !== 1) continue - - let prefixedVariant: Variant | null = null - - // `[&>*]` can be replaced with `*` + // Promote `group-[]:flex` to `in-[.group]:flex` + // ^^ Yes, this is empty + // Promote `group-[]/name:flex` to `in-[.group\/name]:flex` if ( - // Only top-level, so `has-[&>*]` is not supported - parent === null && - // [&_>_*]:flex - // ^ ^ ^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === '>' && - ast.nodes[0].nodes[2].type === 'universal' + variant.kind === 'compound' && + variant.root === 'group' && + variant.variant.kind === 'arbitrary' && + variant.variant.selector === '&:is()' ) { - changed = true - Object.assign(variant, designSystem.parseVariant('*')) - continue - } + // `group-[]` + if (variant.modifier === null) { + changed = true + memcpy( + variant, + designSystem.parseVariant( + designSystem.theme.prefix + ? `in-[.${designSystem.theme.prefix}\\:group]` + : 'in-[.group]', + ), + ) + } - // `[&_*]` can be replaced with `**` - if ( - // Only top-level, so `has-[&_*]` is not supported - parent === null && - // [&_*]:flex - // ^ ^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'universal' - ) { - changed = true - Object.assign(variant, designSystem.parseVariant('**')) + // `group-[]/name` + else if (variant.modifier.kind === 'named') { + changed = true + memcpy( + variant, + designSystem.parseVariant( + designSystem.theme.prefix + ? `in-[.${designSystem.theme.prefix}\\:group\\/${variant.modifier.value}]` + : `in-[.group\\/${variant.modifier.value}]`, + ), + ) + } continue } - // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` - if ( - // Only top-level, so `has-[&>[data-visible]]` is not supported - parent === null && - // [&_>_[data-visible]]:flex - // ^ ^ ^^^^^^^^^^^^^^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === '>' && - ast.nodes[0].nodes[2].type === 'attribute' - ) { - ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] - prefixedVariant = designSystem.parseVariant('*') - } - - // Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**:data-visible` - if ( - // Only top-level, so `has-[&_[data-visible]]` is not supported - parent === null && - // [&_[data-visible]]:flex - // ^ ^^^^^^^^^^^^^^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'nesting' && - ast.nodes[0].nodes[0].value === '&' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'attribute' - ) { - ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] - prefixedVariant = designSystem.parseVariant('**') - } - - // Handling a child/parent combinator. E.g.: `[[data-visible]_&]` => `in-data-visible` - if ( - // Only top-level, so `has-[&_[data-visible]]` is not supported - parent === null && - // [[data-visible]___&]:flex - // ^^^^^^^^^^^^^^ ^ ^ - ast.nodes[0].length === 3 && - ast.nodes[0].nodes[0].type === 'attribute' && - ast.nodes[0].nodes[1].type === 'combinator' && - ast.nodes[0].nodes[1].value === ' ' && - ast.nodes[0].nodes[2].type === 'nesting' && - ast.nodes[0].nodes[2].value === '&' - ) { - ast.nodes[0].nodes = [ast.nodes[0].nodes[0]] - changed = true - // When handling a compound like `in-[[data-visible]]`, we will first - // handle `[[data-visible]]`, then the parent `in-*` part. This means - // that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`. - // - // Later this gets converted to `in-data-visible`. - Object.assign(variant, designSystem.parseVariant(`in-[${ast.toString()}]`)) - continue - } + // Expecting an arbitrary variant + if (variant.kind === 'arbitrary') { + // Expecting a non-relative arbitrary variant + if (variant.relative) continue - // `in-*` variant - if ( - // Only top-level, so `has-[p_&]` is not supported - parent === null && - // `[data-*]` and `[aria-*]` are handled separately - !( - ast.nodes[0].nodes[0].type === 'attribute' && - (ast.nodes[0].nodes[0].attribute.startsWith('data-') || - ast.nodes[0].nodes[0].attribute.startsWith('aria-')) - ) && - // [.foo___&]:flex - // ^^^^ ^ ^ - ast.nodes[0].nodes.at(-1)?.type === 'nesting' - ) { - let selector = ast.nodes[0] - let nodes = selector.nodes + let ast = SelectorParser().astSync(variant.selector) - nodes.pop() // Remove the last node `&` + // Expecting a single selector node + if (ast.nodes.length !== 1) continue + + // `[&>*]` can be replaced with `*` + if ( + // Only top-level, so `has-[&>*]` is not supported + parent === null && + // [&_>_*]:flex + // ^ ^ ^ + ast.nodes[0].length === 3 && + ast.nodes[0].nodes[0].type === 'nesting' && + ast.nodes[0].nodes[0].value === '&' && + ast.nodes[0].nodes[1].type === 'combinator' && + ast.nodes[0].nodes[1].value === '>' && + ast.nodes[0].nodes[2].type === 'universal' + ) { + changed = true + memcpy(variant, designSystem.parseVariant('*')) + continue + } - // Remove trailing whitespace - let last = nodes.at(-1) - while (last?.type === 'combinator' && last.value === ' ') { - nodes.pop() - last = nodes.at(-1) + // `[&_*]` can be replaced with `**` + if ( + // Only top-level, so `has-[&_*]` is not supported + parent === null && + // [&_*]:flex + // ^ ^ + ast.nodes[0].length === 3 && + ast.nodes[0].nodes[0].type === 'nesting' && + ast.nodes[0].nodes[0].value === '&' && + ast.nodes[0].nodes[1].type === 'combinator' && + ast.nodes[0].nodes[1].value === ' ' && + ast.nodes[0].nodes[2].type === 'universal' + ) { + changed = true + memcpy(variant, designSystem.parseVariant('**')) + continue } - changed = true - Object.assign(variant, designSystem.parseVariant(`in-[${selector.toString().trim()}]`)) - continue - } + // `in-*` variant. If the selector ends with ` &`, we can convert it to an + // `in-*` variant. + // + // E.g.: `[[data-visible]_&]` => `in-data-visible` + if ( + // Only top-level, so `in-[&_[data-visible]]` is not supported + parent === null && + // [[data-visible]___&]:flex + // ^^^^^^^^^^^^^^ ^ ^ + ast.nodes[0].nodes.length === 3 && + ast.nodes[0].nodes[1].type === 'combinator' && + ast.nodes[0].nodes[1].value === ' ' && + ast.nodes[0].nodes[2].type === 'nesting' + ) { + ast.nodes[0].nodes.pop() // Remove the nesting node + ast.nodes[0].nodes.pop() // Remove the combinator - // Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]` - let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting') + changed = true + // When handling a compound like `in-[[data-visible]]`, we will first + // handle `[[data-visible]]`, then the parent `in-*` part. This means + // that we can convert `[[data-visible]_&]` to `in-[[data-visible]]`. + // + // Later this gets converted to `in-data-visible`. + memcpy(variant, designSystem.parseVariant(`in-[${ast.toString()}]`)) + continue + } - // Expecting a single selector (normal selector or attribute selector) - if (selectorNodes.length !== 1) continue + let prefixedVariant: Variant | null = null + + // Handling a child combinator. E.g.: `[&>[data-visible]]` => `*:data-visible` + if ( + // Only top-level, so `has-[&>[data-visible]]` is not supported + parent === null && + // [&_>_[data-visible]]:flex + // ^ ^ ^^^^^^^^^^^^^^ + ast.nodes[0].length === 3 && + ast.nodes[0].nodes[0].type === 'nesting' && + ast.nodes[0].nodes[0].value === '&' && + ast.nodes[0].nodes[1].type === 'combinator' && + ast.nodes[0].nodes[1].value === '>' && + ast.nodes[0].nodes[2].type === 'attribute' + ) { + ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] + prefixedVariant = designSystem.parseVariant('*') + } - let target = selectorNodes[0] - if (target.type === 'pseudo' && target.value === ':is') { - // Expecting a single selector node - if (target.nodes.length !== 1) continue + // Handling a grand child combinator. E.g.: `[&_[data-visible]]` => `**:data-visible` + if ( + // Only top-level, so `has-[&_[data-visible]]` is not supported + parent === null && + // [&_[data-visible]]:flex + // ^ ^^^^^^^^^^^^^^ + ast.nodes[0].length === 3 && + ast.nodes[0].nodes[0].type === 'nesting' && + ast.nodes[0].nodes[0].value === '&' && + ast.nodes[0].nodes[1].type === 'combinator' && + ast.nodes[0].nodes[1].value === ' ' && + ast.nodes[0].nodes[2].type === 'attribute' + ) { + ast.nodes[0].nodes = [ast.nodes[0].nodes[2]] + prefixedVariant = designSystem.parseVariant('**') + } - // Expecting a single attribute selector - if (target.nodes[0].nodes.length !== 1) continue + // Filter out `&`. E.g.: `&[data-foo]` => `[data-foo]` + let selectorNodes = ast.nodes[0].filter((node) => node.type !== 'nesting') - // Unwrap the selector from inside `&:is(…)` - target = target.nodes[0].nodes[0] - } + // Expecting a single selector (normal selector or attribute selector) + if (selectorNodes.length !== 1) continue - // Expecting a pseudo selector - if (target.type === 'pseudo') { - let targetNode = target - let compoundNot = false - if (target.value === ':not') { - compoundNot = true + let target = selectorNodes[0] + if (target.type === 'pseudo' && target.value === ':is') { + // Expecting a single selector node if (target.nodes.length !== 1) continue - if (target.nodes[0].type !== 'selector') continue + + // Expecting a single attribute selector if (target.nodes[0].nodes.length !== 1) continue - if (target.nodes[0].nodes[0].type !== 'pseudo') continue - targetNode = target.nodes[0].nodes[0] + // Unwrap the selector from inside `&:is(…)` + target = target.nodes[0].nodes[0] } - let newVariant = ((value) => { - // - if (value === ':first-letter') return 'first-letter' - else if (value === ':first-line') return 'first-line' - // - else if (value === ':file-selector-button') return 'file' - else if (value === ':placeholder') return 'placeholder' - else if (value === ':backdrop') return 'backdrop' - // Positional - else if (value === ':first-child') return 'first' - else if (value === ':last-child') return 'last' - else if (value === ':only-child') return 'only' - else if (value === ':first-of-type') return 'first-of-type' - else if (value === ':last-of-type') return 'last-of-type' - else if (value === ':only-of-type') return 'only-of-type' - // State - else if (value === ':visited') return 'visited' - else if (value === ':target') return 'target' - // Forms - else if (value === ':default') return 'default' - else if (value === ':checked') return 'checked' - else if (value === ':indeterminate') return 'indeterminate' - else if (value === ':placeholder-shown') return 'placeholder-shown' - else if (value === ':autofill') return 'autofill' - else if (value === ':optional') return 'optional' - else if (value === ':required') return 'required' - else if (value === ':valid') return 'valid' - else if (value === ':invalid') return 'invalid' - else if (value === ':in-range') return 'in-range' - else if (value === ':out-of-range') return 'out-of-range' - else if (value === ':read-only') return 'read-only' - // Content - else if (value === ':empty') return 'empty' - // Interactive - else if (value === ':focus-within') return 'focus-within' - else if (value === ':focus') return 'focus' - else if (value === ':focus-visible') return 'focus-visible' - else if (value === ':active') return 'active' - else if (value === ':enabled') return 'enabled' - else if (value === ':disabled') return 'disabled' - // - if ( - value === ':nth-child' && - targetNode.nodes.length === 1 && - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - targetNode.nodes[0].nodes[0].value === 'odd' - ) { - if (compoundNot) { - compoundNot = false - return 'even' - } - return 'odd' + // Expecting a pseudo selector + if (target.type === 'pseudo') { + let targetNode = target + let compoundNot = false + if (target.value === ':not') { + compoundNot = true + if (target.nodes.length !== 1) continue + if (target.nodes[0].type !== 'selector') continue + if (target.nodes[0].nodes.length !== 1) continue + if (target.nodes[0].nodes[0].type !== 'pseudo') continue + + targetNode = target.nodes[0].nodes[0] } - if ( - value === ':nth-child' && - targetNode.nodes.length === 1 && - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - targetNode.nodes[0].nodes[0].value === 'even' - ) { - if (compoundNot) { - compoundNot = false + + let newVariant = ((value) => { + // + if (value === ':first-letter') return 'first-letter' + else if (value === ':first-line') return 'first-line' + // + else if (value === ':file-selector-button') return 'file' + else if (value === ':placeholder') return 'placeholder' + else if (value === ':backdrop') return 'backdrop' + // Positional + else if (value === ':first-child') return 'first' + else if (value === ':last-child') return 'last' + else if (value === ':only-child') return 'only' + else if (value === ':first-of-type') return 'first-of-type' + else if (value === ':last-of-type') return 'last-of-type' + else if (value === ':only-of-type') return 'only-of-type' + // State + else if (value === ':visited') return 'visited' + else if (value === ':target') return 'target' + // Forms + else if (value === ':default') return 'default' + else if (value === ':checked') return 'checked' + else if (value === ':indeterminate') return 'indeterminate' + else if (value === ':placeholder-shown') return 'placeholder-shown' + else if (value === ':autofill') return 'autofill' + else if (value === ':optional') return 'optional' + else if (value === ':required') return 'required' + else if (value === ':valid') return 'valid' + else if (value === ':invalid') return 'invalid' + else if (value === ':in-range') return 'in-range' + else if (value === ':out-of-range') return 'out-of-range' + else if (value === ':read-only') return 'read-only' + // Content + else if (value === ':empty') return 'empty' + // Interactive + else if (value === ':focus-within') return 'focus-within' + else if (value === ':focus') return 'focus' + else if (value === ':focus-visible') return 'focus-visible' + else if (value === ':active') return 'active' + else if (value === ':enabled') return 'enabled' + else if (value === ':disabled') return 'disabled' + // + if ( + value === ':nth-child' && + targetNode.nodes.length === 1 && + targetNode.nodes[0].nodes.length === 1 && + targetNode.nodes[0].nodes[0].type === 'tag' && + targetNode.nodes[0].nodes[0].value === 'odd' + ) { + if (compoundNot) { + compoundNot = false + return 'even' + } return 'odd' } - return 'even' - } - - for (let [selector, variantName] of [ - [':nth-child', 'nth'], - [':nth-last-child', 'nth-last'], - [':nth-of-type', 'nth-of-type'], - [':nth-last-of-type', 'nth-of-last-type'], - ]) { - if (value === selector && targetNode.nodes.length === 1) { - if ( - targetNode.nodes[0].nodes.length === 1 && - targetNode.nodes[0].nodes[0].type === 'tag' && - isPositiveInteger(targetNode.nodes[0].nodes[0].value) - ) { - return `${variantName}-${targetNode.nodes[0].nodes[0].value}` + if ( + value === ':nth-child' && + targetNode.nodes.length === 1 && + targetNode.nodes[0].nodes.length === 1 && + targetNode.nodes[0].nodes[0].type === 'tag' && + targetNode.nodes[0].nodes[0].value === 'even' + ) { + if (compoundNot) { + compoundNot = false + return 'odd' } - - return `${variantName}-[${targetNode.nodes[0].toString()}]` + return 'even' } - } - return null - })(targetNode.value) + for (let [selector, variantName] of [ + [':nth-child', 'nth'], + [':nth-last-child', 'nth-last'], + [':nth-of-type', 'nth-of-type'], + [':nth-last-of-type', 'nth-of-last-type'], + ]) { + if (value === selector && targetNode.nodes.length === 1) { + if ( + targetNode.nodes[0].nodes.length === 1 && + targetNode.nodes[0].nodes[0].type === 'tag' && + isPositiveInteger(targetNode.nodes[0].nodes[0].value) + ) { + return `${variantName}-${targetNode.nodes[0].nodes[0].value}` + } + + return `${variantName}-[${targetNode.nodes[0].toString()}]` + } + } - if (newVariant === null) continue + return null + })(targetNode.value) - // Add `not-` prefix - if (compoundNot) newVariant = `not-${newVariant}` + if (newVariant === null) continue - let parsed = designSystem.parseVariant(newVariant) - if (parsed === null) continue + // Add `not-` prefix + if (compoundNot) newVariant = `not-${newVariant}` - // Update original variant - changed = true - Object.assign(variant, parsed) - } + let parsed = designSystem.parseVariant(newVariant) + if (parsed === null) continue - // Expecting an attribute selector - else if (target.type === 'attribute') { - // Attribute selectors - let attributeKey = target.attribute - let attributeValue = target.value - ? target.quoted - ? `${target.quoteMark}${target.value}${target.quoteMark}` - : target.value - : null - - // Insensitive attribute selectors. E.g.: `[data-foo="value" i]` - // ^ - if (target.insensitive && attributeValue) { - attributeValue += ' i' + // Update original variant + changed = true + memcpy(variant, parsed) } - let operator = target.operator ?? '=' + // Expecting an attribute selector + else if (target.type === 'attribute') { + // Attribute selectors + let attributeKey = target.attribute + let attributeValue = target.value + ? target.quoted + ? `${target.quoteMark}${target.value}${target.quoteMark}` + : target.value + : null + + // Insensitive attribute selectors. E.g.: `[data-foo="value" i]` + // ^ + if (target.insensitive && attributeValue) { + attributeValue += ' i' + } - // Migrate `data-*` - if (attributeKey.startsWith('data-')) { - changed = true - attributeKey = attributeKey.slice(5) // Remove `data-` - Object.assign(variant, { - kind: 'functional', - root: 'data', - modifier: null, - value: - attributeValue === null - ? { kind: 'named', value: attributeKey } - : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, - } satisfies Variant) - } + let operator = target.operator ?? '=' + + // Migrate `data-*` + if (attributeKey.startsWith('data-')) { + changed = true + attributeKey = attributeKey.slice(5) // Remove `data-` + memcpy(variant, { + kind: 'functional', + root: 'data', + modifier: null, + value: + attributeValue === null + ? { kind: 'named', value: attributeKey } + : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, + } satisfies Variant) + } - // Migrate `aria-*` - else if (attributeKey.startsWith('aria-')) { - changed = true - attributeKey = attributeKey.slice(5) // Remove `aria-` - Object.assign(variant, { - kind: 'functional', - root: 'aria', - modifier: null, - value: - attributeValue === null - ? { kind: 'arbitrary', value: attributeKey } // aria-[foo] - : operator === '=' && target.value === 'true' && !target.insensitive - ? { kind: 'named', value: attributeKey } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true] - : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, // aria-[foo~="true"], aria-[foo|="true"], … - } satisfies Variant) + // Migrate `aria-*` + else if (attributeKey.startsWith('aria-')) { + changed = true + attributeKey = attributeKey.slice(5) // Remove `aria-` + memcpy(variant, { + kind: 'functional', + root: 'aria', + modifier: null, + value: + attributeValue === null + ? { kind: 'arbitrary', value: attributeKey } // aria-[foo] + : operator === '=' && target.value === 'true' && !target.insensitive + ? { kind: 'named', value: attributeKey } // aria-[foo="true"] or aria-[foo='true'] or aria-[foo=true] + : { kind: 'arbitrary', value: `${attributeKey}${operator}${attributeValue}` }, // aria-[foo~="true"], aria-[foo|="true"], … + } satisfies Variant) + } } - } - if (prefixedVariant) { - let idx = clone.variants.indexOf(variant) - if (idx === -1) continue + if (prefixedVariant) { + let idx = clone.variants.indexOf(variant) + if (idx === -1) continue - // Ensure we inject the prefixed variant - clone.variants.splice(idx, 1, variant, prefixedVariant) + // Ensure we inject the prefixed variant + clone.variants.splice(idx, 1, variant, prefixedVariant) + } } }