Skip to content

Commit 24305db

Browse files
committed
Add rule html-attributes-casing.
1 parent 29d1cb6 commit 24305db

File tree

8 files changed

+581
-67
lines changed

8 files changed

+581
-67
lines changed

docs/rules/html-attributes-casing.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Define a style for the attributes casing in templates. (html-attributes-casing)
2+
3+
Define a style for the attributes casing in templates.
4+
5+
:+1: Examples of **correct** code for `PascalCase`:
6+
7+
```html
8+
<template>
9+
<component MyProp="prop"></component>
10+
</template>
11+
```
12+
13+
:+1: Examples of **correct** code for `kebab-case`:
14+
15+
```html
16+
<template>
17+
<component my-prop="prop"></component>
18+
</template>
19+
```
20+
21+
:+1: Examples of **correct** code for `camelCase`:
22+
23+
```html
24+
<template>
25+
<component myProp="prop"></component>
26+
</template>
27+
```
28+
29+
## :wrench: Options
30+
31+
Default casing is set to `kebab-case`
32+
33+
```
34+
'vue/html-attributes-casing': [2, 'camelCase'|'kebab-case'|'PascalCase']
35+
```

lib/rules/html-attributes-casing.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @fileoverview Define a style for the props casing in templates.
3+
* @author Armano
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const casing = require('../utils/casing')
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
function create (context) {
15+
const sourceCode = context.getSourceCode()
16+
const options = context.options[0]
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+
}
32+
33+
// ----------------------------------------------------------------------
34+
// Public
35+
// ----------------------------------------------------------------------
36+
37+
utils.registerTemplateBodyVisitor(context, {
38+
'VStartTag' (obj) {
39+
if (!utils.isSvgElementName(obj.id.name) && !utils.isMathMLElementName(obj.id.name)) {
40+
obj.attributes.forEach((node) => {
41+
if (!node.directive) {
42+
const oldValue = node.key.name
43+
if (oldValue.indexOf('data-') !== -1) {
44+
return
45+
}
46+
const value = casing.getConverter(caseType)(oldValue)
47+
if (value !== oldValue) {
48+
reportIssue(node, oldValue, value)
49+
}
50+
} else if (node.key.name === 'bind') {
51+
const oldValue = node.key.argument
52+
if (oldValue.indexOf('data-') !== -1) {
53+
return
54+
}
55+
const text = sourceCode.getText(node.key)
56+
const value = casing.getConverter(caseType)(oldValue)
57+
if (value !== oldValue) {
58+
reportIssue(node, text, text.replace(oldValue, value))
59+
}
60+
}
61+
})
62+
}
63+
}
64+
})
65+
66+
return {}
67+
}
68+
69+
module.exports = {
70+
meta: {
71+
docs: {
72+
description: 'Define a style for the props casing in templates.',
73+
category: 'Stylistic Issues',
74+
recommended: false
75+
},
76+
fixable: 'code',
77+
schema: [
78+
{
79+
enum: casing.allowedCaseOptions
80+
}
81+
]
82+
},
83+
84+
create
85+
}

lib/rules/html-no-self-closing.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const utils = require('../utils')
2424
function create (context) {
2525
utils.registerTemplateBodyVisitor(context, {
2626
'VStartTag[selfClosing=true]' (node) {
27-
if (!utils.isSvgElementName(node.id.name)) {
27+
if (!utils.isSvgElementName(node.id.name) && !utils.isMathMLElementName(node.id.name)) {
2828
const pos = node.range[1] - 2
2929
context.report({
3030
node,

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+
}

lib/utils/index.js

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
1313
const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json'))
14+
const MATHML_ELEMENT_NAMES = new Set(require('./mathml-elements.json'))
1415
const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
1516
const assert = require('assert')
1617

@@ -67,8 +68,8 @@ module.exports = {
6768
assert(node && node.type === 'VElement')
6869

6970
return (
70-
node.parent.type === 'Program' ||
71-
node.parent.parent.type === 'Program'
71+
node.parent.type === 'Program' ||
72+
node.parent.parent.type === 'Program'
7273
)
7374
},
7475

@@ -194,49 +195,60 @@ module.exports = {
194195
)
195196
},
196197

197-
/**
198-
* Check whether the given node is a custom component or not.
199-
* @param {ASTNode} node The start tag node to check.
200-
* @returns {boolean} `true` if the node is a custom component.
201-
*/
198+
/**
199+
* Check whether the given node is a custom component or not.
200+
* @param {ASTNode} node The start tag node to check.
201+
* @returns {boolean} `true` if the node is a custom component.
202+
*/
202203
isCustomComponent (node) {
203204
assert(node && node.type === 'VStartTag')
204205

205206
const name = node.id.name
206207
return (
207-
!(this.isHtmlElementName(name) || this.isSvgElementName(name)) ||
208-
this.hasAttribute(node, 'is') ||
209-
this.hasDirective(node, 'bind', 'is')
208+
!(this.isHtmlElementName(name) || this.isSvgElementName(name) || this.isMathMLElementName(name)) ||
209+
this.hasAttribute(node, 'is') ||
210+
this.hasDirective(node, 'bind', 'is')
210211
)
211212
},
212213

213-
/**
214-
* Check whether the given name is a HTML element name or not.
215-
* @param {string} name The name to check.
216-
* @returns {boolean} `true` if the name is a HTML element name.
217-
*/
214+
/**
215+
* Check whether the given name is a HTML element name or not.
216+
* @param {string} name The name to check.
217+
* @returns {boolean} `true` if the name is a HTML element name.
218+
*/
218219
isHtmlElementName (name) {
219220
assert(typeof name === 'string')
220221

221222
return HTML_ELEMENT_NAMES.has(name.toLowerCase())
222223
},
223224

224-
/**
225-
* Check whether the given name is a SVG element name or not.
226-
* @param {string} name The name to check.
227-
* @returns {boolean} `true` if the name is a SVG element name.
228-
*/
225+
/**
226+
* Check whether the given name is a SVG element name or not.
227+
* @param {string} name The name to check.
228+
* @returns {boolean} `true` if the name is a SVG element name.
229+
*/
229230
isSvgElementName (name) {
230231
assert(typeof name === 'string')
231232

232233
return SVG_ELEMENT_NAMES.has(name.toLowerCase())
233234
},
234235

235-
/**
236-
* Check whether the given name is a void element name or not.
237-
* @param {string} name The name to check.
238-
* @returns {boolean} `true` if the name is a void element name.
239-
*/
236+
/**
237+
* Check whether the given name is a MathML element name or not.
238+
* @param {string} name The name to check.
239+
* @returns {boolean} `true` if the name is a HTML element name.
240+
*/
241+
isMathMLElementName (name) {
242+
assert(typeof name === 'string')
243+
244+
return MATHML_ELEMENT_NAMES.has(name.toLowerCase())
245+
},
246+
247+
/**
248+
* Check whether the given name is a void element name or not.
249+
* @param {string} name The name to check.
250+
* @returns {boolean} `true` if the name is a void element name.
251+
*/
240252
isVoidElementName (name) {
241253
assert(typeof name === 'string')
242254

0 commit comments

Comments
 (0)