Skip to content

Commit a4776d0

Browse files
committed
add @utility name validation
- We check for a valid root which should optionally start with `-`, and be followed by `[a-z]`. After that, only `[a-zA-Z0-9_-]` is valid for the root. - For static utilities, the remaining part (the value) can include: - `.`: but not consecutive ones, e.g. `foo..bar` is invalid - `%`: but only at the end and preceded by a digit, e.g. `foo-x%` and `foo-%-bar` are invalid - `/`: as the modifier, but there can only be one, and must be followed by another character, e.g.: `foo/bar/baz` and `foo/` are invalid - For functional utilities, we need a valid root and a valid `-*` suffix. The remaining "value" part should be empty.
1 parent b30cb2e commit a4776d0

File tree

1 file changed

+143
-5
lines changed

1 file changed

+143
-5
lines changed

packages/tailwindcss/src/utilities.ts

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ import { segment } from './utils/segment'
2828
import * as ValueParser from './value-parser'
2929
import { walk, WalkAction } from './walk'
3030

31-
const IS_VALID_STATIC_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*$/
32-
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/
33-
3431
const DEFAULT_SPACING_SUGGESTIONS = [
3532
'0',
3633
'0.5',
@@ -5835,7 +5832,7 @@ export function createCssUtility(node: AtRule) {
58355832
let name = node.params
58365833

58375834
// Functional utilities. E.g.: `tab-size-*`
5838-
if (IS_VALID_FUNCTIONAL_UTILITY_NAME.test(name)) {
5835+
if (isValidFunctionalUtilityName(name)) {
58395836
// API:
58405837
//
58415838
// - `--value('literal')` resolves a literal named value
@@ -6184,7 +6181,7 @@ export function createCssUtility(node: AtRule) {
61846181
}
61856182
}
61866183

6187-
if (IS_VALID_STATIC_UTILITY_NAME.test(name)) {
6184+
if (isValidStaticUtilityName(name)) {
61886185
return (designSystem: DesignSystem) => {
61896186
designSystem.utilities.static(name, () => node.nodes.map(cloneAstNode))
61906187
}
@@ -6428,3 +6425,144 @@ function alphaReplacedDropShadowProperties(
64286425
return [decl(property, prefix + replacedValue)]
64296426
}
64306427
}
6428+
6429+
const UTILITY_ROOT = /^-?[a-z][a-zA-Z0-9_-]*/
6430+
6431+
const PERCENT = 37
6432+
const SLASH = 47
6433+
const DOT = 46
6434+
const LOWER_A = 97
6435+
const LOWER_Z = 122
6436+
const UPPER_A = 65
6437+
const UPPER_Z = 90
6438+
const ZERO = 48
6439+
const NINE = 57
6440+
const UNDERSCORE = 95
6441+
const DASH = 45
6442+
6443+
export function isValidStaticUtilityName(name: string): boolean {
6444+
let match = UTILITY_ROOT.exec(name)
6445+
if (match === null) return false // Invalid root
6446+
6447+
let root = match[0]
6448+
let value = name.slice(root.length)
6449+
6450+
// Root should not end in `-` if there is no value
6451+
//
6452+
// `tab-size-`
6453+
// --------- Root
6454+
if (value.length === 0 && root.endsWith('-')) {
6455+
return false
6456+
}
6457+
6458+
// No remaining value is valid
6459+
//
6460+
// `tab-size`
6461+
// -------- Root
6462+
if (value.length === 0) {
6463+
return true
6464+
}
6465+
6466+
// Any valid (static) utility should be valid including:
6467+
// - Bare values with `.`: `p-1.5`
6468+
// - Bare values with `%`: `w-50%`
6469+
// - With an embedded modifier: `text-xs/8`
6470+
6471+
let seenSlash = false
6472+
for (let i = 0; i < value.length; i++) {
6473+
let charCode = value.charCodeAt(i)
6474+
switch (charCode) {
6475+
case PERCENT: {
6476+
// A percentage is only valid at the end of the value
6477+
if (i !== value.length - 1) return false
6478+
6479+
// A percent is only valid when preceded by a digit. E.g.: `w-%` is invalid
6480+
let previousChar = value[i - 1] || root[root.length - 1] || ''
6481+
let previousCharCode = previousChar.charCodeAt(0)
6482+
if (previousCharCode < ZERO || previousCharCode > NINE) return false
6483+
break
6484+
}
6485+
6486+
case SLASH: {
6487+
// A slash must be followed by at least 1 character. E.g.: `foo/` is invalid
6488+
if (i === value.length - 1) return false
6489+
6490+
// A slash can only appear once. E.g.: `foo/bar/baz` is invalid
6491+
if (seenSlash) return false
6492+
seenSlash = true
6493+
break
6494+
}
6495+
6496+
case DOT: {
6497+
// Dots are only allowed between digits. E.g.: `p-1.a` is invalid
6498+
let previousChar = value[i - 1] || root[root.length - 1] || ''
6499+
let previousCharCode = previousChar.charCodeAt(0)
6500+
if (previousCharCode < ZERO || previousCharCode > NINE) return false
6501+
6502+
let nextChar = value[i + 1] || ''
6503+
let nextCharCode = nextChar.charCodeAt(0)
6504+
if (nextCharCode < ZERO || nextCharCode > NINE) return false
6505+
break
6506+
}
6507+
6508+
// Allowed special characters
6509+
case UNDERSCORE:
6510+
case DASH: {
6511+
continue
6512+
}
6513+
6514+
default: {
6515+
if (
6516+
(charCode >= LOWER_A && charCode <= LOWER_Z) || // Allow a-z
6517+
(charCode >= UPPER_A && charCode <= UPPER_Z) || // Allow A-Z
6518+
(charCode >= ZERO && charCode <= NINE) // Allow 0-9
6519+
) {
6520+
continue
6521+
}
6522+
6523+
// Everything else is invalid
6524+
return false
6525+
}
6526+
}
6527+
}
6528+
6529+
return true
6530+
}
6531+
6532+
export function isValidFunctionalUtilityName(name: string): boolean {
6533+
if (!name.endsWith('-*')) return false // Missing '-*' suffix
6534+
name = name.slice(0, -2)
6535+
6536+
let match = UTILITY_ROOT.exec(name)
6537+
if (match === null) return false // Invalid root
6538+
6539+
let root = match[0]
6540+
let value = name.slice(root.length)
6541+
6542+
// Root should not end in `-` if there is no value
6543+
//
6544+
// `tab-size--*`
6545+
// --------- Root
6546+
// -- Suffix
6547+
//
6548+
// Because with default values, this could match `tab-size-` which is invalid.
6549+
if (value.length === 0 && root.endsWith('-')) {
6550+
return false
6551+
}
6552+
6553+
// No remaining value is valid
6554+
//
6555+
// `tab-size-*`
6556+
// -------- Root
6557+
// -- Suffix
6558+
if (value.length === 0) {
6559+
return true
6560+
}
6561+
6562+
// But if there is a value remaining, it's invalid.
6563+
//
6564+
// E.g.: `tab-size-[…]-*`
6565+
//
6566+
// If we allow more characters, we can extend the validation here
6567+
return false
6568+
}

0 commit comments

Comments
 (0)