Skip to content

Commit 42aca0c

Browse files
Match against correct name of dynamic attributes when using regexes (#410)
* Update tests Some of these would’ve caught a bug if written correctly 🤦‍♂️ * Directly encode dynamic attribute rules 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.
1 parent 3a58565 commit 42aca0c

File tree

2 files changed

+63
-20
lines changed

2 files changed

+63
-20
lines changed

src/options.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults
7272
let dynamicAttrs = new Set<string>(defaults.dynamicAttrs)
7373
let functions = new Set<string>(defaults.functions)
7474
let staticAttrsRegex: RegExp[] = [...defaults.staticAttrsRegex]
75-
let dynamicAttrsRegex: RegExp[] = [...defaults.dynamicAttrsRegex]
7675
let functionsRegex: RegExp[] = [...defaults.functionsRegex]
7776

7877
// Create a list of "static" attributes
@@ -104,15 +103,6 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults
104103
}
105104
}
106105

107-
for (let regex of staticAttrsRegex) {
108-
if (parser === 'vue') {
109-
dynamicAttrsRegex.push(new RegExp(`:${regex.source}`, regex.flags))
110-
dynamicAttrsRegex.push(new RegExp(`v-bind:${regex.source}`, regex.flags))
111-
} else if (parser === 'angular') {
112-
dynamicAttrsRegex.push(new RegExp(`\\[${regex.source}\\]`, regex.flags))
113-
}
114-
}
115-
116106
// Generate a list of supported functions
117107
for (let fn of options.tailwindFunctions ?? []) {
118108
let regex = parseRegex(fn)
@@ -125,12 +115,47 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults
125115
}
126116

127117
return {
128-
hasStaticAttr: (name: string) => hasMatch(name, staticAttrs, staticAttrsRegex),
129-
hasDynamicAttr: (name: string) => hasMatch(name, dynamicAttrs, dynamicAttrsRegex),
118+
hasStaticAttr: (name: string) => {
119+
// If the name looks like a dynamic attribute we're not a static attr
120+
// Only applies to Vue and Angular
121+
let newName = nameFromDynamicAttr(name, parser)
122+
if (newName) return false
123+
124+
return hasMatch(name, staticAttrs, staticAttrsRegex)
125+
},
126+
127+
hasDynamicAttr: (name: string) => {
128+
// This is definitely a dynamic attribute
129+
if (hasMatch(name, dynamicAttrs, [])) return true
130+
131+
// If the name looks like a dynamic attribute compare the actual name
132+
// Only applies to Vue and Angular
133+
let newName = nameFromDynamicAttr(name, parser)
134+
if (!newName) return false
135+
136+
return hasMatch(newName, staticAttrs, staticAttrsRegex)
137+
},
138+
130139
hasFunction: (name: string) => hasMatch(name, functions, functionsRegex),
131140
}
132141
}
133142

143+
function nameFromDynamicAttr(name: string, parser: string) {
144+
if (parser === 'vue') {
145+
if (name.startsWith(':')) return name.slice(1)
146+
if (name.startsWith('v-bind:')) return name.slice(7)
147+
if (name.startsWith('v-')) return name
148+
return null
149+
}
150+
151+
if (parser === 'angular') {
152+
if (name.startsWith('[') && name.endsWith(']')) return name.slice(1, -1)
153+
return null
154+
}
155+
156+
return null
157+
}
158+
134159
/**
135160
* Check for matches against a static list or possible regex patterns
136161
*/

tests/format.test.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,21 +193,21 @@ describe('regex matching', () => {
193193
})
194194

195195
test('works with Vue dynamic bindings', async ({ expect }) => {
196-
let result = await format('<div :data-classes="sm:p-0 p-0"></div>', {
196+
let result = await format('<div :data-classes="`sm:p-0 p-0`"></div>', {
197197
parser: 'vue',
198198
tailwindAttributes: ['/data-.*/'],
199199
})
200200

201-
expect(result).toEqual('<div :data-classes="p-0 sm:p-0"></div>')
201+
expect(result).toEqual('<div :data-classes="`p-0 sm:p-0`"></div>')
202202
})
203203

204204
test('works with Angular property bindings', async ({ expect }) => {
205-
let result = await format('<div [dataClasses]="sm:p-0 p-0"></div>', {
205+
let result = await format('<div [dataClasses]="`sm:p-0 p-0`"></div>', {
206206
parser: 'angular',
207207
tailwindAttributes: ['/data.*/i'],
208208
})
209209

210-
expect(result).toEqual('<div [dataClasses]="p-0 sm:p-0"></div>')
210+
expect(result).toEqual('<div [dataClasses]="`p-0 sm:p-0`"></div>')
211211
})
212212

213213
test('invalid regex patterns do nothing', async ({ expect }) => {
@@ -218,25 +218,43 @@ describe('regex matching', () => {
218218
expect(result).toEqual('<div data-test="sm:p-0 p-0"></div>')
219219
})
220220

221+
test('dynamic attributes are not matched as static attributes', async ({ expect }) => {
222+
let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
223+
parser: 'vue',
224+
tailwindAttributes: ['/.*-class/'],
225+
})
226+
227+
expect(result).toEqual(`<div :custom-class="['flex p-0 underline sm:p-0']"></div>`)
228+
})
229+
230+
test('dynamic attributes are not matched as static attributes (2)', async ({ expect }) => {
231+
let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
232+
parser: 'vue',
233+
tailwindAttributes: ['/:custom-class/'],
234+
})
235+
236+
expect(result).toEqual(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`)
237+
})
238+
221239
// These tests pass but that is a side-effect of the implementation
222240
// If these change in the future to no longer pass that is a good thing
223241
describe('dynamic attribute matching quirks', () => {
224242
test('Vue', async ({ expect }) => {
225-
let result = await format('<div ::data-classes="sm:p-0 p-0"></div>', {
243+
let result = await format('<div ::data-classes="`sm:p-0 p-0`"></div>', {
226244
parser: 'vue',
227245
tailwindAttributes: ['/:data-.*/'],
228246
})
229247

230-
expect(result).toEqual('<div ::data-classes="p-0 sm:p-0"></div>')
248+
expect(result).toEqual('<div ::data-classes="`p-0 sm:p-0`"></div>')
231249
})
232250

233251
test('Angular', async ({ expect }) => {
234-
let result = await format('<div [[dataClasses]]="sm:p-0 p-0"></div>', {
252+
let result = await format('<div [[dataClasses]]="`sm:p-0 p-0`"></div>', {
235253
parser: 'angular',
236254
tailwindAttributes: ['/\\[data.*\\]/i'],
237255
})
238256

239-
expect(result).toEqual('<div [[dataClasses]]="p-0 sm:p-0"></div>')
257+
expect(result).toEqual('<div [[dataClasses]]="`p-0 sm:p-0`"></div>')
240258
})
241259
})
242260
})

0 commit comments

Comments
 (0)