From 4d4384cb9a6d2c78775f68d2cc4422b8b2e2b1fe Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 13:49:43 +0000 Subject: [PATCH 1/2] fix: preserve casing of custom-ident identifiers (--foo) across all printers Several call sites lowercased CSS identifiers unconditionally (function names, attribute selector names, pseudo names, type/universal selector names and namespaces, at-rule names), while only the declaration-property printer checked for a `--`-prefixed custom-ident first. This meant a dashed-ident custom function call like `--myFunc()` got silently lowercased to `--myfunc()`, corrupting it. Added a shared print_identifier() helper (lowercase unless the name starts with `--`) and applied it at every one of these call sites, so custom-idents keep their case everywhere, not just in declaration properties. --- src/lib/index.ts | 24 ++++++++++++++---------- test/selectors.test.ts | 6 ++++++ test/values.test.ts | 8 ++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 67434db..ff9b022 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -78,6 +78,12 @@ export function unquote(str: string): string { return str.replaceAll(UNQUOTE_RE, EMPTY_STRING) } +/** Lowercases a CSS identifier, except a custom-ident starting with `--`, + * which must keep its case as written. */ +function print_identifier(name: string): string { + return name.startsWith('--') ? name : name.toLowerCase() +} + function print_string(str: string | number | null, quote?: '"' | "'"): string { str = str?.toString() || '' let inner = unquote(str) @@ -124,7 +130,7 @@ function print_list(nodes: CSSNode[], optional_space = SPACE): string { let parts = [] for (let node of nodes) { if (is_function(node)) { - let fn = node.name.toLowerCase() + let fn = print_identifier(node.name) parts.push(fn, OPEN_PARENTHESES, print_list(node.children, optional_space), CLOSE_PARENTHESES) } else if (is_dimension(node)) { parts.push(node.value, node.unit?.toLowerCase()) @@ -183,9 +189,7 @@ export function format_declaration( value += SPACE } - if (!property.startsWith('--')) { - property = property.toLowerCase() - } + property = print_identifier(property) return property + COLON + optional_space + value + important } @@ -233,7 +237,7 @@ function print_combinator(node: Combinator, optional_space: string, is_first: bo /** Prints an attribute selector, e.g. `[href^="https://" i]`. */ function print_attribute_selector(node: AttributeSelector): string { - let parts = [OPEN_BRACKET, node.name.toLowerCase()] + let parts = [OPEN_BRACKET, print_identifier(node.name)] if (node.attr_operator) { parts.push(node.attr_operator) @@ -256,7 +260,7 @@ function print_pseudo_selector( optional_space = SPACE, ): string { let parts = [COLON] - let name = node.name.toLowerCase() + let name = print_identifier(node.name) // Legacy pseudo-elements or actual pseudo-elements use double colon if (name === 'before' || name === 'after' || is_pseudo_element_selector(node)) { @@ -293,12 +297,12 @@ function print_selector_component( } if (is_type_selector(node)) { - let prefix = node.namespace === null ? '' : node.namespace.toLowerCase() + '|' - return prefix + node.name.toLowerCase() + let prefix = node.namespace === null ? '' : print_identifier(node.namespace) + '|' + return prefix + print_identifier(node.name) } if (is_universal_selector(node)) { - let prefix = node.namespace === null ? '' : node.namespace.toLowerCase() + '|' + let prefix = node.namespace === null ? '' : print_identifier(node.namespace) + '|' return prefix + '*' } @@ -588,7 +592,7 @@ export function format( } function print_atrule(node: Atrule): string { - let name = '@' + node.name!.toLowerCase() + let name = '@' + print_identifier(node.name!) if (node.prelude) { name += SPACE + format_atrule_prelude(node.prelude.text, { minify }) } diff --git a/test/selectors.test.ts b/test/selectors.test.ts index 79c0834..62d03aa 100644 --- a/test/selectors.test.ts +++ b/test/selectors.test.ts @@ -196,6 +196,12 @@ test('forces attribute selectors to have quoted values', () => { expect(actual).toEqual(expected) }) +test('lowercases attribute selector names', () => { + let actual = format(`[HREF] {}`) + let expected = `[href] {}` + expect(actual).toEqual(expected) +}) + test('adds a space before attribute selector flags', () => { let actual = format(` [title="foo" i], diff --git a/test/values.test.ts b/test/values.test.ts index 749f037..10ebc7c 100644 --- a/test/values.test.ts +++ b/test/values.test.ts @@ -178,6 +178,14 @@ test('lowercases CSS functions', () => { expect(actual).toEqual(expected) }) +test('preserves casing of a custom function name', () => { + let actual = format(`a { width: --myFunc(1px); }`) + let expected = `a { + width: --myFunc(1px); +}` + expect(actual).toEqual(expected) +}) + test('relative colors', () => { let actual = format(`a { color: rgb( from red 0 0 255); From de2b3e3c93723b6e8309dce8241673eb1057d778 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 17:43:35 +0000 Subject: [PATCH 2/2] test: attribute selector value keeps its casing/quoting when name is lowercased Addresses review feedback on #232. --- test/selectors.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/selectors.test.ts b/test/selectors.test.ts index 62d03aa..97e38de 100644 --- a/test/selectors.test.ts +++ b/test/selectors.test.ts @@ -202,6 +202,12 @@ test('lowercases attribute selector names', () => { expect(actual).toEqual(expected) }) +test('lowercases attribute selector name but keeps the value as-is', () => { + let actual = format(`[HREF=Some-Value] {}`) + let expected = `[href="Some-Value"] {}` + expect(actual).toEqual(expected) +}) + test('adds a space before attribute selector flags', () => { let actual = format(` [title="foo" i],