From 8e97a483579efced46159d136c58fe5938a531b2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 10 Jul 2024 10:01:23 +0200 Subject: [PATCH] Discard invalid classes such as `bg-red-[#000]` (#13970) * add test that verifies that parsing `bg-red-[#0088cc]` should not work * improve parsing of arbitrary values, root is known When using arbitrary values such as `bg-[#0088cc]` then we know that everything before the `-[` part is the `root` of the utility. This also means that this should exist in the design system as-is. If we use `findRoot` instead, it means that `bg-red-[#0088cc]` would also just parse fine and we don't want. * small refactor: define `important` and `negative` directly It's not necessary to track state in a state object. Let's use the variables directly. * small refactor: use return on same line for consistency * add test that verifies that parsing `bg-` should not work * move `value === ''` check up The value doesn't change anymore, which means we can discard early and don't need to start creating candidate objects. * adjust comment with example * update changelog --- CHANGELOG.md | 4 +- packages/tailwindcss/src/candidate.test.ts | 45 +++++++++ packages/tailwindcss/src/candidate.ts | 109 ++++++++++++++------- 3 files changed, 121 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cad8da11..a86400e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Discard invalid classes such as `bg-red-[#000]` ([#13970](https://github.com/tailwindlabs/tailwindcss/pull/13970)) ## [4.0.0-alpha.17] - 2024-07-04 diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 80c3e5d53..e39932526 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -436,6 +436,15 @@ it('should parse a utility with a modifier and a variant', () => { `) }) +it('should not parse a partial utility', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + utilities.functional('bg', () => []) + + expect(run('flex-', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('bg-', { utilities })).toMatchInlineSnapshot(`null`) +}) + it('should parse a utility with an arbitrary value', () => { let utilities = new Utilities() utilities.functional('bg', () => []) @@ -641,6 +650,42 @@ it('should parse a utility with an explicit variable as the arbitrary value that `) }) +it('should not parse invalid arbitrary values', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + for (let candidate of [ + 'bg-red-[#0088cc]', + 'bg-red[#0088cc]', + + 'bg-red-[color:var(--value)]', + 'bg-red[color:var(--value)]', + + 'bg-red-[#0088cc]/50', + 'bg-red[#0088cc]/50', + + 'bg-red-[#0088cc]/[50%]', + 'bg-red[#0088cc]/[50%]', + + 'bg-red-[#0088cc]!', + 'bg-red[#0088cc]!', + + 'bg-red-[--value]', + 'bg-red[--value]', + + 'bg-red-[--value]!', + 'bg-red[--value]!', + + 'bg-red-[var(--value)]', + 'bg-red[var(--value)]', + + 'bg-red-[var(--value)]!', + 'bg-red[var(--value)]!', + ]) { + expect(run(candidate, { utilities })).toEqual(null) + } +}) + it('should parse a utility with an implicit variable as the modifier', () => { let utilities = new Utilities() utilities.functional('bg', () => []) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 71a4aa880..40612458d 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -233,27 +233,36 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi parsedCandidateVariants.push(parsedVariant) } - let state = { - important: false, - negative: false, - } + let important = false + let negative = false // Candidates that end with an exclamation mark are the important version with // higher specificity of the non-important candidate, e.g. `mx-4!`. if (base[base.length - 1] === '!') { - state.important = true + important = true base = base.slice(0, -1) } // Legacy syntax with leading `!`, e.g. `!mx-4`. else if (base[0] === '!') { - state.important = true + important = true base = base.slice(1) } + // Figure out the new base and the modifier segment if present. + // + // E.g.: + // + // ``` + // bg-red-500/50 + // ^^^^^^^^^^ -> Base without modifier + // ^^ -> Modifier segment + // ``` + let [baseWithoutModifier, modifierSegment = null] = segment(base, '/') + // Arbitrary properties - if (base[0] === '[') { - let [baseWithoutModifier, modifierSegment = null] = segment(base, '/') + if (baseWithoutModifier[0] === '[') { + // Arbitrary properties should end with a `]`. if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return null // The property part of the arbitrary property can only start with a-z @@ -287,20 +296,57 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi value, modifier: modifierSegment === null ? null : parseModifier(modifierSegment), variants: parsedCandidateVariants, - important: state.important, + important, } } // Candidates that start with a dash are the negative versions of another // candidate, e.g. `-mx-4`. - if (base[0] === '-') { - state.negative = true - base = base.slice(1) + if (baseWithoutModifier[0] === '-') { + negative = true + baseWithoutModifier = baseWithoutModifier.slice(1) } - let [root, value] = findRoot(base, designSystem.utilities) + // The root of the utility, e.g.: `bg-red-500` + // ^^ + let root: string | null = null - let modifierSegment: string | null = null + // The value of the utility, e.g.: `bg-red-500` + // ^^^^^^^ + let value: string | null = null + + // If the base of the utility ends with a `]`, then we know it's an arbitrary + // value. This also means that everything before the `[…]` part should be the + // root of the utility. + // + // E.g.: + // + // ``` + // bg-[#0088cc] + // ^^ -> Root + // ^^^^^^^^^ -> Arbitrary value + // + // bg-red-[#0088cc] + // ^^^^^^ -> Root + // ^^^^^^^^^ -> Arbitrary value + // ``` + if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') { + let idx = baseWithoutModifier.indexOf('-[') + if (idx === -1) return null + + root = baseWithoutModifier.slice(0, idx) + + // The root of the utility should exist as-is in the utilities map. If not, + // it's an invalid utility and we can skip continue parsing. + if (!designSystem.utilities.has(root)) return null + + value = baseWithoutModifier.slice(idx + 1) + } + + // Not an arbitrary value + else { + ;[root, value] = findRoot(baseWithoutModifier, designSystem.utilities) + } // If the root is null, but it contains a `/`, then it could be that we are // dealing with a functional utility that contains a modifier but doesn't @@ -319,6 +365,11 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // If there's no root, the candidate isn't a valid class and can be discarded. if (root === null) return null + // If the leftover value is an empty string, it means that the value is an + // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we + // can skip any further parsing. + if (value === '') return null + let kind = designSystem.utilities.kind(root) if (kind === 'static') { @@ -328,8 +379,8 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi kind: 'static', root, variants: parsedCandidateVariants, - negative: state.negative, - important: state.important, + negative, + important, } } @@ -339,25 +390,18 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi modifier: modifierSegment === null ? null : parseModifier(modifierSegment), value: null, variants: parsedCandidateVariants, - negative: state.negative, - important: state.important, + negative, + important, } if (value === null) return candidate { - // Extract a modifier if present, e.g. `text-xl/9` or `bg-red-500/[14%]` - let [valueWithoutModifier, modifierSegment = null] = segment(value, '/') - - if (modifierSegment !== null) { - candidate.modifier = parseModifier(modifierSegment) - } - - let startArbitraryIdx = valueWithoutModifier.indexOf('[') + let startArbitraryIdx = value.indexOf('[') let valueIsArbitrary = startArbitraryIdx !== -1 if (valueIsArbitrary) { - let arbitraryValue = valueWithoutModifier.slice(startArbitraryIdx + 1, -1) + let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])` let typehint = '' @@ -408,18 +452,11 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi let fraction = modifierSegment === null || candidate.modifier?.kind === 'arbitrary' ? null - : value.slice(valueWithoutModifier.lastIndexOf('-') + 1) - - // If the leftover value is an empty string, it means that the value is an - // invalid named value. This makes the candidate invalid and we can - // skip any further parsing. - if (valueWithoutModifier === '') { - return null - } + : `${value.slice(value.lastIndexOf('-') + 1)}/${modifierSegment}` candidate.value = { kind: 'named', - value: valueWithoutModifier, + value, fraction, } }