Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep underscores in dashed-idents #13538

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Make sure `contain-*` utility variables resolve to a valid value ([#13521](https://github.com/tailwindlabs/tailwindcss/pull/13521))
- Support unbalanced parentheses and braces in quotes in arbitrary values and variants ([#13608](https://github.com/tailwindlabs/tailwindcss/pull/13608))
- Keep underscores in dashed-idents ([#13538](https://github.com/tailwindlabs/tailwindcss/pull/13538))

### Changed

Expand Down
35 changes: 31 additions & 4 deletions packages/tailwindcss/src/utils/decode-arbitrary-value.test.ts
Expand Up @@ -14,13 +14,40 @@ describe('decoding arbitrary values', () => {
expect(decodeArbitraryValue('foo\\_bar')).toBe('foo_bar')
})

it('should not replace underscores in url()', () => {
it('should not replace underscores in url()', () => {
expect(decodeArbitraryValue('url(./my_file.jpg)')).toBe('url(./my_file.jpg)')
})

it('should leave var(…) as is', () => {
expect(decodeArbitraryValue('var(--foo)')).toBe('var(--foo)')
expect(decodeArbitraryValue('var(--headings-h1-size)')).toBe('var(--headings-h1-size)')
it('should not replace underscores in var(…)', () => {
expect(decodeArbitraryValue('var(--foo_bar)')).toBe('var(--foo_bar)')
})

it('should replace underscores in the fallback value of var(…)', () => {
expect(decodeArbitraryValue('var(--foo_bar, "my_content")')).toBe(
'var(--foo_bar, "my content")',
)
})

it('should not replace underscores in nested var(…)', () => {
expect(decodeArbitraryValue('var(--foo_bar, var(--bar_baz))')).toBe(
'var(--foo_bar, var(--bar_baz))',
)
})

it('should replace underscores in the fallback value of nested var(…)', () => {
expect(decodeArbitraryValue('var(--foo_bar, var(--bar_baz, "my_content"))')).toBe(
'var(--foo_bar, var(--bar_baz, "my content"))',
)
})

it('should not replace underscores in dashed idents', () => {
expect(decodeArbitraryValue('--foo_bar')).toBe('--foo_bar')
})

it('should replace underscores in strings that look like dashed idents', () => {
expect(decodeArbitraryValue('content-["some--thing_here"]')).toBe(
'content-["some--thing here"]',
)
})
})

Expand Down
132 changes: 121 additions & 11 deletions packages/tailwindcss/src/utils/decode-arbitrary-value.ts
@@ -1,5 +1,17 @@
import { addWhitespaceAroundMathOperators } from './math-operators'

const BACKSLASH = 0x5c
const UNDERSCORE = 0x5f
const DASH = 0x2d
const DOUBLE_QUOTE = 0x22
const SINGLE_QUOTE = 0x27
const LOWER_A = 0x61
const LOWER_Z = 0x7a
const UPPER_A = 0x41
const UPPER_Z = 0x5a
const ZERO = 0x30
const NINE = 0x39

export function decodeArbitraryValue(input: string): string {
// We do not want to normalize anything inside of a url() because if we
// replace `_` with ` `, then it will very likely break the url.
Expand All @@ -14,28 +26,126 @@ export function decodeArbitraryValue(input: string): string {
}

/**
* Convert `_` to ` `, except for escaped underscores `\_` they should be
* converted to `_` instead.
* Convert underscores `_` to whitespace ` `
*
* Except for:
*
* - Escaped underscores `\_`, these are converted to underscores `_`
* - Dashed idents `--foo_bar`, these are left as-is
*
* Inside strings, dashed idents are considered to be normal strings without any
* special meaning, so the `_` in "dashed idents" are converted to whitespace.
*/
function convertUnderscoresToWhitespace(input: string) {
let output = ''
for (let i = 0; i < input.length; i++) {
let char = input[i]
let len = input.length

for (let idx = 0; idx < len; idx++) {
let char = input.charCodeAt(idx)

// Escaped values
if (input.charCodeAt(idx) === BACKSLASH) {
// An escaped underscore (e.g.: `\_`) is converted to a non-escaped
// underscore, but without converting the `_` to a space.
if (input.charCodeAt(idx + 1) === UNDERSCORE) {
output += '_'
idx += 1
}

// Escaped underscore
if (char === '\\' && input[i + 1] === '_') {
output += '_'
i += 1
// Consume the backslash and the next character as-is
else {
output += input.slice(idx, idx + 2)
idx += 1
}
}

// Unescaped underscore
else if (char === '_') {
// Underscores are converted to whitespace
else if (char === UNDERSCORE) {
output += ' '
}

// Start of a dashed ident, consume the ident as-is
else if (char === DASH && input.charCodeAt(idx + 1) === DASH) {
let start = idx

// Skip the first two dashes, we already know they are there
idx += 2

char = input.charCodeAt(idx)
while (
(char >= LOWER_A && char <= LOWER_Z) ||
(char >= UPPER_A && char <= UPPER_Z) ||
(char >= ZERO && char <= NINE) ||
char === DASH ||
char === UNDERSCORE ||
char === BACKSLASH
) {
// Escaped value, consume the next character as-is
if (char === BACKSLASH) {
// In theory, we can also escape a unicode code point where 1 to 6 hex
// digits are allowed after the \. However, each hex digit is also a
// valid ident character, so we can just consume the next character
// as-is and go to the next character.
idx += 1
}

// Next character
char = input.charCodeAt(++idx)
}

output += input.slice(start, idx)

// The last character was not a valid ident character, so we need to back
// up one character.
idx -= 1
}

// Start of a string
else if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE) {
let quote = input[idx++]

// Keep the quote
output += quote

// Consume to the end of the string, but replace any non-escaped
// underscores with spaces.
while (idx < len && input.charCodeAt(idx) !== char) {
// Escaped values
if (input.charCodeAt(idx) === BACKSLASH) {
// An escaped underscore (e.g.: `\_`) is converted to a non-escaped
// underscore, but without converting the `_` to a space.
if (input.charCodeAt(idx + 1) === UNDERSCORE) {
output += '_'
idx += 1
}

// Consume the backslash and the next character as-is
else {
output += input.slice(idx, idx + 2)
idx += 1
}
}

// Unescaped underscore
else if (input.charCodeAt(idx) === UNDERSCORE) {
output += ' '
}

// All other characters
else {
output += input[idx]
}

idx += 1
}

// Keep the end quote
output += quote
}

// All other characters
else {
output += char
output += input[idx]
}
}

Expand Down