Skip to content

Commit

Permalink
Ensure escaped theme variables are handled correctly (#16064)
Browse files Browse the repository at this point in the history
This PR ensures that escaped theme variables are properly handled. We do
this by moving the `escape`/`unescape` responsibility back into the main
tailwindcss entrypoint that reads and writes from the CSS and making
sure that _all internal state of the `Theme` class are unescaped
classes.

However, due to us accidentally shipping the part where a dot in the
theme variable would translate to an underscore in CSS already, this
logic is going to stay as-is for now.

Here's an example test that visualizes the new changes:

```ts
expect(
  await compileCss(
    css`
      @theme {
        --spacing-*: initial;
        --spacing-1\.5: 2.5rem;
        --spacing-foo\/bar: 3rem;
      }
      @tailwind utilities;
    `,
    ['m-1.5', 'm-foo/bar'],
  ),
).toMatchInlineSnapshot(`
  ":root, :host {
    --spacing-1\.5: 2.5rem;
    --spacing-foo\\/bar: 3rem;
  }

  .m-1\\.5 {
    margin: var(--spacing-1\.5);
  }

  .m-foo\\/bar {
    margin: var(--spacing-foo\\/bar);
  }"
`)
```

## Test plan

- Added a unit test
- Ensure this works end-to-end using the Vite playground:
   
<img width="1016" alt="Screenshot 2025-01-30 at 14 51 05"
src="https://github.com/user-attachments/assets/463c6fd5-793f-4ecc-86d2-5ad40bbb3e74"
/>
  • Loading branch information
philipp-spiess authored Jan 31, 2025
1 parent 3aa0e49 commit 60e6195
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Only generate positive `grid-cols-*` and `grid-rows-*` utilities ([#16020](https://github.com/tailwindlabs/tailwindcss/pull/16020))
- Ensure escaped theme variables are handled correctly ([#16064](https://github.com/tailwindlabs/tailwindcss/pull/16064))
- Ensure we process Tailwind CSS features when only using `@reference` or `@variant` ([#16057](https://github.com/tailwindlabs/tailwindcss/pull/16057))
- Refactor gradient implementation to work around [prettier/prettier#17058](https://github.com/prettier/prettier/issues/17058) ([#16072](https://github.com/tailwindlabs/tailwindcss/pull/16072))
- Vite: Ensure hot-reloading works with SolidStart setups ([#16052](https://github.com/tailwindlabs/tailwindcss/pull/16052))
Expand Down
3 changes: 1 addition & 2 deletions packages/tailwindcss/src/compat/apply-config-to-theme.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { DesignSystem } from '../design-system'
import { ThemeOptions } from '../theme'
import { escape } from '../utils/escape'
import type { ResolvedConfig } from './config/types'

function resolveThemeValue(value: unknown, subValue: string | null = null): string | null {
Expand Down Expand Up @@ -55,7 +54,7 @@ export function applyConfigToTheme(
if (!name) continue

designSystem.theme.add(
`--${escape(name)}`,
`--${name}`,
'' + value,
ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT,
)
Expand Down
43 changes: 43 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,49 @@ describe('compiling CSS', () => {
`)
})

test('unescapes theme variables and handles dots as underscore', async () => {
expect(
await compileCss(
css`
@theme {
--spacing-*: initial;
--spacing-1\.5: 1.5px;
--spacing-2_5: 2.5px;
--spacing-3\.5: 3.5px;
--spacing-3_5: 3.5px;
--spacing-foo\/bar: 3rem;
}
@tailwind utilities;
`,
['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'],
),
).toMatchInlineSnapshot(`
":root, :host {
--spacing-1\\.5: 1.5px;
--spacing-2_5: 2.5px;
--spacing-3\\.5: 3.5px;
--spacing-3_5: 3.5px;
--spacing-foo\\/bar: 3rem;
}
.m-1\\.5 {
margin: var(--spacing-1\\.5);
}
.m-2\\.5, .m-2_5 {
margin: var(--spacing-2_5);
}
.m-3\\.5 {
margin: var(--spacing-3\\.5);
}
.m-foo\\/bar {
margin: var(--spacing-foo\\/bar);
}"
`)
})

test('adds vendor prefixes', async () => {
expect(
await compileCss(
Expand Down
5 changes: 3 additions & 2 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as CSS from './css-parser'
import { buildDesignSystem, type DesignSystem } from './design-system'
import { Theme, ThemeOptions } from './theme'
import { createCssUtility } from './utilities'
import { escape, unescape } from './utils/escape'
import { segment } from './utils/segment'
import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants'
export type Config = UserConfig
Expand Down Expand Up @@ -467,7 +468,7 @@ async function parseCss(

if (child.kind === 'comment') return
if (child.kind === 'declaration' && child.property.startsWith('--')) {
theme.add(child.property, child.value ?? '', themeOptions)
theme.add(unescape(child.property), child.value ?? '', themeOptions)
return
}

Expand Down Expand Up @@ -526,7 +527,7 @@ async function parseCss(

for (let [key, value] of theme.entries()) {
if (value.options & ThemeOptions.REFERENCE) continue
nodes.push(decl(key, value.value))
nodes.push(decl(escape(key), value.value))
}

let keyframesRules = theme.getKeyframes()
Expand Down
23 changes: 14 additions & 9 deletions packages/tailwindcss/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ export class Theme {
) {}

add(key: string, value: string, options = ThemeOptions.NONE): void {
if (key.endsWith('\\*')) {
key = key.slice(0, -2) + '*'
}

if (key.endsWith('-*')) {
if (value !== 'initial') {
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
Expand Down Expand Up @@ -149,11 +145,20 @@ export class Theme {
#resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
for (let namespace of themeKeys) {
let themeKey =
candidateValue !== null
? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey)
: namespace
candidateValue !== null ? (`${namespace}-${candidateValue}` as ThemeKey) : namespace

if (!this.values.has(themeKey)) {
// If the exact theme key is not found, we might be trying to resolve a key containing a dot
// that was registered with an underscore instead:
if (candidateValue !== null && candidateValue.includes('.')) {
themeKey = `${namespace}-${candidateValue.replaceAll('.', '_')}` as ThemeKey

if (!this.values.has(themeKey)) continue
} else {
continue
}
}

if (!this.values.has(themeKey)) continue
if (isIgnoredThemeKey(themeKey, namespace)) continue

return themeKey
Expand All @@ -167,7 +172,7 @@ export class Theme {
return null
}

return `var(${this.#prefixKey(themeKey)})`
return `var(${escape(this.#prefixKey(themeKey))})`
}

resolve(candidateValue: string | null, themeKeys: ThemeKey[]): string | null {
Expand Down

0 comments on commit 60e6195

Please sign in to comment.