Skip to content

Commit a4be85d

Browse files
committed
Improve support for html attribute casing & add autofix
1 parent 7a3c068 commit a4be85d

File tree

4 files changed

+123
-99
lines changed

4 files changed

+123
-99
lines changed

lib/rules/html-attributes-casing.js

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,72 +5,55 @@
55
'use strict'
66

77
const utils = require('../utils')
8-
9-
function kebabCase (str) {
10-
return str.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1]).replace(/\s+/g, '-').toLowerCase()
11-
}
12-
13-
function camelCase (str) {
14-
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => index === 0 ? letter.toLowerCase() : letter.toUpperCase()).replace(/[\s-]+/g, '')
15-
}
16-
17-
function pascalCase (str) {
18-
str = camelCase(str)
19-
return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : ''
20-
}
21-
22-
function convertCase (str, caseType) {
23-
if (caseType === 'kebab-case') {
24-
return kebabCase(str)
25-
} else if (caseType === 'PascalCase') {
26-
return pascalCase(str)
27-
}
28-
return camelCase(str)
29-
}
8+
const casing = require('../utils/casing')
309

3110
// ------------------------------------------------------------------------------
3211
// Rule Definition
3312
// ------------------------------------------------------------------------------
3413

3514
function create (context) {
15+
const sourceCode = context.getSourceCode()
3616
const options = context.options[0]
37-
const caseType = ['camelCase', 'kebab-case', 'PascalCase'].indexOf(options) !== -1 ? options : 'kebab-case'
17+
const caseType = casing.allowedCaseOptions.indexOf(options) !== -1 ? options : 'kebab-case'
18+
19+
function reportIssue (node, name, newName) {
20+
context.report({
21+
node: node.key,
22+
loc: node.loc,
23+
message: "Attribute '{{name}}' is not {{caseType}}.",
24+
data: {
25+
name,
26+
caseType,
27+
newName
28+
},
29+
fix: fixer => fixer.replaceText(node.key, newName)
30+
})
31+
}
3832

3933
// ----------------------------------------------------------------------
4034
// Public
4135
// ----------------------------------------------------------------------
4236

4337
utils.registerTemplateBodyVisitor(context, {
44-
"VAttribute[directive=true][key.name='bind']" (node) {
45-
const value = convertCase(node.key.argument, caseType)
46-
if (value !== node.key.argument) {
47-
context.report({
48-
node: node.key,
49-
loc: node.loc,
50-
message: 'Attribute {{bind}}:{{name}} is not {{caseType}}.',
51-
data: {
52-
name: node.key.argument,
53-
caseType: caseType,
54-
bind: node.key.shorthand ? '' : 'v-bind'
38+
'VStartTag' (obj) {
39+
if (!utils.isSvgElementName(obj.id.name) && !utils.isMathMLElementName(obj.id.name)) {
40+
obj.attributes.forEach((node) => {
41+
if (node.directive && node.key.name === 'bind') {
42+
const text = sourceCode.getText(node.key)
43+
const oldValue = node.key.argument
44+
const value = casing.getConverter(caseType)(oldValue)
45+
if (value !== oldValue) {
46+
reportIssue(node, text, text.replace(oldValue, value))
47+
}
48+
} else if (!node.directive) {
49+
const oldValue = node.key.name
50+
const value = casing.getConverter(caseType)(oldValue)
51+
if (value !== oldValue) {
52+
reportIssue(node, oldValue, value)
53+
}
5554
}
5655
})
5756
}
58-
},
59-
'VAttribute[directive=false]' (node) {
60-
if (node.key.type === 'VIdentifier') {
61-
const value = convertCase(node.key.name, caseType)
62-
if (value !== node.key.name) {
63-
context.report({
64-
node: node.key,
65-
loc: node.loc,
66-
message: 'Attribute {{name}} is not {{caseType}}.',
67-
data: {
68-
name: node.key.name,
69-
caseType: caseType
70-
}
71-
})
72-
}
73-
}
7457
}
7558
})
7659

@@ -84,10 +67,10 @@ module.exports = {
8467
category: 'Stylistic Issues',
8568
recommended: false
8669
},
87-
fixable: null, // or "code" or "whitespace"
70+
fixable: 'code',
8871
schema: [
8972
{
90-
enum: ['camelCase', 'kebab-case', 'PascalCase']
73+
enum: casing.allowedCaseOptions
9174
}
9275
]
9376
},

lib/rules/name-property-casing.js

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,15 @@
55
'use strict'
66

77
const utils = require('../utils')
8-
9-
function kebabCase (str) {
10-
return str
11-
.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
12-
.replace(/[^a-zA-Z:]+/g, '-')
13-
.toLowerCase()
14-
}
15-
16-
function camelCase (str) {
17-
return str
18-
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (
19-
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
20-
)
21-
.replace(/[^a-zA-Z:]+/g, '')
22-
}
23-
24-
function pascalCase (str) {
25-
return str
26-
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
27-
.replace(/[^a-zA-Z:]+/g, '')
28-
}
29-
30-
const allowedCaseOptions = [
31-
'camelCase',
32-
'kebab-case',
33-
'PascalCase'
34-
]
35-
36-
const convertersMap = {
37-
'kebab-case': kebabCase,
38-
'camelCase': camelCase,
39-
'PascalCase': pascalCase
40-
}
41-
42-
function getConverter (name) {
43-
return convertersMap[name] || pascalCase
44-
}
8+
const casing = require('../utils/casing')
459

4610
// ------------------------------------------------------------------------------
4711
// Rule Definition
4812
// ------------------------------------------------------------------------------
4913

5014
function create (context) {
5115
const options = context.options[0]
52-
const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
16+
const caseType = casing.allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
5317

5418
// ----------------------------------------------------------------------
5519
// Public
@@ -65,7 +29,7 @@ function create (context) {
6529

6630
if (!node) return
6731

68-
const value = getConverter(caseType)(node.value.value)
32+
const value = casing.getConverter(caseType)(node.value.value)
6933
if (value !== node.value.value) {
7034
context.report({
7135
node: node.value,
@@ -87,10 +51,10 @@ module.exports = {
8751
category: 'Stylistic Issues',
8852
recommended: false
8953
},
90-
fixable: 'code', // or "code" or "whitespace"
54+
fixable: 'code',
9155
schema: [
9256
{
93-
enum: allowedCaseOptions
57+
enum: casing.allowedCaseOptions
9458
}
9559
]
9660
},

lib/utils/casing.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const assert = require('assert')
2+
3+
function kebabCase (str) {
4+
return str
5+
.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
6+
.replace(/[^a-zA-Z:]+/g, '-')
7+
.toLowerCase()
8+
}
9+
10+
function camelCase (str) {
11+
return str
12+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (
13+
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
14+
)
15+
.replace(/[^a-zA-Z:]+/g, '')
16+
}
17+
18+
function pascalCase (str) {
19+
return str
20+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
21+
.replace(/[^a-zA-Z:]+/g, '')
22+
}
23+
24+
const convertersMap = {
25+
'kebab-case': kebabCase,
26+
'camelCase': camelCase,
27+
'PascalCase': pascalCase
28+
}
29+
30+
module.exports = {
31+
allowedCaseOptions: [
32+
'camelCase',
33+
'kebab-case',
34+
'PascalCase'
35+
],
36+
37+
getConverter (name) {
38+
assert(typeof name === 'string')
39+
40+
return convertersMap[name] || pascalCase
41+
}
42+
}

tests/lib/rules/html-attributes-casing.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ ruleTester.run('html-attributes-casing', rule, {
5757
filename: 'test.vue',
5858
code: '<template><div><component :MyProp="prop"></component></div></template>',
5959
options: ['PascalCase']
60+
},
61+
{
62+
filename: 'test.vue',
63+
code: '<template><div><svg foo-bar="prop"></svg></div></template>',
64+
options: ['PascalCase']
6065
}
6166
],
6267

@@ -66,7 +71,7 @@ ruleTester.run('html-attributes-casing', rule, {
6671
code: '<template><div><component my-prop="prop"></component></div></template>',
6772
options: ['camelCase'],
6873
errors: [{
69-
message: 'Attribute my-prop is not camelCase.',
74+
message: "Attribute 'my-prop' is not camelCase.",
7075
type: 'VIdentifier',
7176
line: 1
7277
}]
@@ -76,7 +81,7 @@ ruleTester.run('html-attributes-casing', rule, {
7681
code: '<template><div><component my-prop="prop"></component></div></template>',
7782
options: ['PascalCase'],
7883
errors: [{
79-
message: 'Attribute my-prop is not PascalCase.',
84+
message: "Attribute 'my-prop' is not PascalCase.",
8085
type: 'VIdentifier',
8186
line: 1
8287
}]
@@ -86,7 +91,7 @@ ruleTester.run('html-attributes-casing', rule, {
8691
code: '<template><div><component MyProp="prop"></component></div></template>',
8792
options: ['kebab-case'],
8893
errors: [{
89-
message: 'Attribute MyProp is not kebab-case.',
94+
message: "Attribute 'MyProp' is not kebab-case.",
9095
type: 'VIdentifier',
9196
line: 1
9297
}]
@@ -96,7 +101,7 @@ ruleTester.run('html-attributes-casing', rule, {
96101
code: '<template><div><component :my-prop="prop"></component></div></template>',
97102
options: ['camelCase'],
98103
errors: [{
99-
message: 'Attribute :my-prop is not camelCase.',
104+
message: "Attribute ':my-prop' is not camelCase.",
100105
type: 'VDirectiveKey',
101106
line: 1
102107
}]
@@ -106,7 +111,7 @@ ruleTester.run('html-attributes-casing', rule, {
106111
code: '<template><div><component :my-prop="prop"></component></div></template>',
107112
options: ['PascalCase'],
108113
errors: [{
109-
message: 'Attribute :my-prop is not PascalCase.',
114+
message: "Attribute ':my-prop' is not PascalCase.",
110115
type: 'VDirectiveKey',
111116
line: 1
112117
}]
@@ -116,7 +121,37 @@ ruleTester.run('html-attributes-casing', rule, {
116121
code: '<template><div><component :MyProp="prop"></component></div></template>',
117122
options: ['kebab-case'],
118123
errors: [{
119-
message: 'Attribute :MyProp is not kebab-case.',
124+
message: "Attribute ':MyProp' is not kebab-case.",
125+
type: 'VDirectiveKey',
126+
line: 1
127+
}]
128+
},
129+
{
130+
filename: 'test.vue',
131+
code: '<template><div><component v-bind:my-prop="prop"></component></div></template>',
132+
options: ['camelCase'],
133+
errors: [{
134+
message: "Attribute 'v-bind:my-prop' is not camelCase.",
135+
type: 'VDirectiveKey',
136+
line: 1
137+
}]
138+
},
139+
{
140+
filename: 'test.vue',
141+
code: '<template><div><component v-bind:my-prop="prop"></component></div></template>',
142+
options: ['PascalCase'],
143+
errors: [{
144+
message: "Attribute 'v-bind:my-prop' is not PascalCase.",
145+
type: 'VDirectiveKey',
146+
line: 1
147+
}]
148+
},
149+
{
150+
filename: 'test.vue',
151+
code: '<template><div><component v-bind:MyProp="prop"></component></div></template>',
152+
options: ['kebab-case'],
153+
errors: [{
154+
message: "Attribute 'v-bind:MyProp' is not kebab-case.",
120155
type: 'VDirectiveKey',
121156
line: 1
122157
}]

0 commit comments

Comments
 (0)