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
This commit is contained in:
Robin Malfait 2024-07-10 10:01:23 +02:00 • committed by GitHub
parent 06e96e0767
commit 8e97a48357
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 121 additions and 37 deletions

View file

@ -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

View file

@ -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', () => [])

View file

@ -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,
}
}