Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export function createMatcher(options: RequiredOptions, parser: string, defaults
let dynamicAttrs = new Set<string>(defaults.dynamicAttrs)
let functions = new Set<string>(defaults.functions)
let staticAttrsRegex: RegExp[] = [...defaults.staticAttrsRegex]
let dynamicAttrsRegex: RegExp[] = [...defaults.dynamicAttrsRegex]
let functionsRegex: RegExp[] = [...defaults.functionsRegex]

// Create a list of "static" attributes
Expand Down Expand Up @@ -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)
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to slice until the = or :?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, the parser has already split the name and value and this only matches against the name.

As for the : I think that is only useful with v-bind. Can always change this in the future if we need to.

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
*/
Expand Down
34 changes: 26 additions & 8 deletions tests/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,21 +193,21 @@ describe('regex matching', () => {
})

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

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

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

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

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

test('dynamic attributes are not matched as static attributes', async ({ expect }) => {
let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
parser: 'vue',
tailwindAttributes: ['/.*-class/'],
})

expect(result).toEqual(`<div :custom-class="['flex p-0 underline sm:p-0']"></div>`)
})

test('dynamic attributes are not matched as static attributes (2)', async ({ expect }) => {
let result = await format(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`, {
parser: 'vue',
tailwindAttributes: ['/:custom-class/'],
})

expect(result).toEqual(`<div :custom-class="['sm:p-0 flex underline p-0']"></div>`)
})

// 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('<div ::data-classes="sm:p-0 p-0"></div>', {
let result = await format('<div ::data-classes="`sm:p-0 p-0`"></div>', {
parser: 'vue',
tailwindAttributes: ['/:data-.*/'],
})

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

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

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