From 26566b65006a67e1b6a99d8b64f979e17c3fd0c6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 16 Oct 2025 07:15:15 -0400 Subject: [PATCH 1/2] Update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some of these would’ve caught a bug if written correctly 🤦‍♂️ --- tests/format.test.ts | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/format.test.ts b/tests/format.test.ts index b5927a1..6321296 100644 --- a/tests/format.test.ts +++ b/tests/format.test.ts @@ -193,21 +193,21 @@ describe('regex matching', () => { }) test('works with Vue dynamic bindings', async ({ expect }) => { - let result = await format('
', { + let result = await format('
', { parser: 'vue', tailwindAttributes: ['/data-.*/'], }) - expect(result).toEqual('
') + expect(result).toEqual('
') }) test('works with Angular property bindings', async ({ expect }) => { - let result = await format('
', { + let result = await format('
', { parser: 'angular', tailwindAttributes: ['/data.*/i'], }) - expect(result).toEqual('
') + expect(result).toEqual('
') }) test('invalid regex patterns do nothing', async ({ expect }) => { @@ -218,25 +218,43 @@ describe('regex matching', () => { expect(result).toEqual('
') }) + test('dynamic attributes are not matched as static attributes', async ({ expect }) => { + let result = await format(`
`, { + parser: 'vue', + tailwindAttributes: ['/.*-class/'], + }) + + expect(result).toEqual(`
`) + }) + + test('dynamic attributes are not matched as static attributes (2)', async ({ expect }) => { + let result = await format(`
`, { + parser: 'vue', + tailwindAttributes: ['/:custom-class/'], + }) + + expect(result).toEqual(`
`) + }) + // These tests pass but that is a side-effect of the implementation // If these change in the future to no longer pass that is a good thing describe('dynamic attribute matching quirks', () => { test('Vue', async ({ expect }) => { - let result = await format('
', { + let result = await format('
', { parser: 'vue', tailwindAttributes: ['/:data-.*/'], }) - expect(result).toEqual('
') + expect(result).toEqual('
') }) test('Angular', async ({ expect }) => { - let result = await format('
', { + let result = await format('
', { parser: 'angular', tailwindAttributes: ['/\\[data.*\\]/i'], }) - expect(result).toEqual('
') + expect(result).toEqual('
') }) }) }) From f0b60cc7f3cd470bcaafadbb220f80cecce27a3f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 16 Oct 2025 07:55:59 -0400 Subject: [PATCH 2/2] Directly encode dynamic attribute rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dynamic attribute names have definite rules in Vue and Angular. Testing those rules directly and matching the actual “name” portion against the regex is a better option. --- src/options.ts | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/options.ts b/src/options.ts index 9cc2b50..1a60a4a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -72,7 +72,6 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults let dynamicAttrs = new Set(defaults.dynamicAttrs) let functions = new Set(defaults.functions) let staticAttrsRegex: RegExp[] = [...defaults.staticAttrsRegex] - let dynamicAttrsRegex: RegExp[] = [...defaults.dynamicAttrsRegex] let functionsRegex: RegExp[] = [...defaults.functionsRegex] // Create a list of "static" attributes @@ -104,15 +103,6 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults } } - for (let regex of staticAttrsRegex) { - if (parser === 'vue') { - dynamicAttrsRegex.push(new RegExp(`:${regex.source}`, regex.flags)) - dynamicAttrsRegex.push(new RegExp(`v-bind:${regex.source}`, regex.flags)) - } else if (parser === 'angular') { - dynamicAttrsRegex.push(new RegExp(`\\[${regex.source}\\]`, regex.flags)) - } - } - // Generate a list of supported functions for (let fn of options.tailwindFunctions ?? []) { let regex = parseRegex(fn) @@ -125,12 +115,47 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults } return { - hasStaticAttr: (name: string) => hasMatch(name, staticAttrs, staticAttrsRegex), - hasDynamicAttr: (name: string) => hasMatch(name, dynamicAttrs, dynamicAttrsRegex), + hasStaticAttr: (name: string) => { + // If the name looks like a dynamic attribute we're not a static attr + // Only applies to Vue and Angular + let newName = nameFromDynamicAttr(name, parser) + if (newName) return false + + return hasMatch(name, staticAttrs, staticAttrsRegex) + }, + + hasDynamicAttr: (name: string) => { + // This is definitely a dynamic attribute + if (hasMatch(name, dynamicAttrs, [])) return true + + // If the name looks like a dynamic attribute compare the actual name + // Only applies to Vue and Angular + let newName = nameFromDynamicAttr(name, parser) + if (!newName) return false + + return hasMatch(newName, staticAttrs, staticAttrsRegex) + }, + hasFunction: (name: string) => hasMatch(name, functions, functionsRegex), } } +function nameFromDynamicAttr(name: string, parser: string) { + if (parser === 'vue') { + if (name.startsWith(':')) return name.slice(1) + if (name.startsWith('v-bind:')) return name.slice(7) + if (name.startsWith('v-')) return name + return null + } + + if (parser === 'angular') { + if (name.startsWith('[') && name.endsWith(']')) return name.slice(1, -1) + return null + } + + return null +} + /** * Check for matches against a static list or possible regex patterns */