From e9c71406365b7736bd70b3f1b8799efb31497fca Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 30 Jan 2025 14:53:01 +0100 Subject: [PATCH 1/2] Ensure escaped theme variables are handled correctly --- CHANGELOG.md | 1 + .../src/compat/apply-config-to-theme.ts | 3 +- .../tailwindcss/src/compat/plugin-api.test.ts | 4 +-- packages/tailwindcss/src/index.test.ts | 29 +++++++++++++++++++ packages/tailwindcss/src/index.ts | 5 ++-- packages/tailwindcss/src/theme.ts | 8 ++--- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3481c52a7ba..21307ef07392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) ## [4.0.1] - 2025-01-29 diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index 3f18711c7f16..c57e4308fcb7 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -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 { @@ -55,7 +54,7 @@ export function applyConfigToTheme( if (!name) continue designSystem.theme.add( - `--${escape(name)}`, + `--${name}`, '' + value, ThemeOptions.INLINE | ThemeOptions.REFERENCE | ThemeOptions.DEFAULT, ) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 539c04a3c66c..3c36387c8f83 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1425,7 +1425,7 @@ describe('theme', async () => { :root, :host { --width-1: 0.25rem; --width-1\\/2: 60%; - --width-1\\.5: 0.375rem; + --width-1_5: 0.375rem; --width-2_5: 0.625rem; } " @@ -1482,7 +1482,7 @@ describe('theme', async () => { :root, :host { --width-1: 0.25rem; --width-1\\/2: 60%; - --width-1\\.5: 0.375rem; + --width-1_5: 0.375rem; --width-2_5: 0.625rem; } " diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 8fba63cfccef..ae0b5f86981d 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -152,6 +152,35 @@ describe('compiling CSS', () => { `) }) + test('unescapes theme variables and handles dots as underscore', async () => { + 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('adds vendor prefixes', async () => { expect( await compileCss( diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index df6e09f90649..1a55ed96e8cf 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -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 @@ -461,7 +462,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 } @@ -520,7 +521,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() diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index a2d3c205fa4a..d7cc46d95f71 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -41,9 +41,7 @@ export class Theme { ) {} add(key: string, value: string, options = ThemeOptions.NONE): void { - if (key.endsWith('\\*')) { - key = key.slice(0, -2) + '*' - } + key = key.replaceAll('.', '_') if (key.endsWith('-*')) { if (value !== 'initial') { @@ -150,7 +148,7 @@ export class Theme { for (let namespace of themeKeys) { let themeKey = candidateValue !== null - ? (escape(`${namespace}-${candidateValue.replaceAll('.', '_')}`) as ThemeKey) + ? (`${namespace}-${candidateValue.replaceAll('.', '_')}` as ThemeKey) : namespace if (!this.values.has(themeKey)) continue @@ -167,7 +165,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 { From c9afcbe2b52dd7a13a80e02511d7fc2ae01c746e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 30 Jan 2025 15:22:24 +0100 Subject: [PATCH 2/2] Allow both escaped dots and underscores to be registered in theme --- .../tailwindcss/src/compat/plugin-api.test.ts | 4 ++-- packages/tailwindcss/src/index.test.ts | 22 +++++++++++++++---- packages/tailwindcss/src/theme.ts | 19 +++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 3c36387c8f83..539c04a3c66c 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1425,7 +1425,7 @@ describe('theme', async () => { :root, :host { --width-1: 0.25rem; --width-1\\/2: 60%; - --width-1_5: 0.375rem; + --width-1\\.5: 0.375rem; --width-2_5: 0.625rem; } " @@ -1482,7 +1482,7 @@ describe('theme', async () => { :root, :host { --width-1: 0.25rem; --width-1\\/2: 60%; - --width-1_5: 0.375rem; + --width-1\\.5: 0.375rem; --width-2_5: 0.625rem; } " diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index ae0b5f86981d..9c1e4db84d6e 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -158,21 +158,35 @@ describe('compiling CSS', () => { css` @theme { --spacing-*: initial; - --spacing-1\.5: 2.5rem; + --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-foo/bar'], + ['m-1.5', 'm-2.5', 'm-2_5', 'm-3.5', 'm-foo/bar'], ), ).toMatchInlineSnapshot(` ":root, :host { - --spacing-1_5: 2.5rem; + --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); + 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 { diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts index d7cc46d95f71..0b0ca96c8d11 100644 --- a/packages/tailwindcss/src/theme.ts +++ b/packages/tailwindcss/src/theme.ts @@ -41,8 +41,6 @@ export class Theme { ) {} add(key: string, value: string, options = ThemeOptions.NONE): void { - key = key.replaceAll('.', '_') - if (key.endsWith('-*')) { if (value !== 'initial') { throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``) @@ -147,11 +145,20 @@ export class Theme { #resolveKey(candidateValue: string | null, themeKeys: ThemeKey[]): string | null { for (let namespace of themeKeys) { let themeKey = - candidateValue !== null - ? (`${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