Skip to content

Commit 50dce57

Browse files
committed
Add rule html-attributes-casing.
1 parent 3361366 commit 50dce57

File tree

7 files changed

+535
-27
lines changed

7 files changed

+535
-27
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ module.exports = {
5151
category: 'Stylistic Issues',
5252
recommended: false
5353
},
54-
fixable: 'code', // or "code" or "whitespace"
54+
fixable: 'code',
5555
schema: [
5656
{
5757
enum: casing.allowedCaseOptions

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)