Skip to content

Commit

Permalink
Add rule html-attributes-casing.
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 committed Jul 23, 2017
1 parent 29d1cb6 commit 24305db
Show file tree
Hide file tree
Showing 8 changed files with 581 additions and 67 deletions.
35 changes: 35 additions & 0 deletions docs/rules/html-attributes-casing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Define a style for the attributes casing in templates. (html-attributes-casing)

Define a style for the attributes casing in templates.

:+1: Examples of **correct** code for `PascalCase`:

```html
<template>
<component MyProp="prop"></component>
</template>
```

:+1: Examples of **correct** code for `kebab-case`:

```html
<template>
<component my-prop="prop"></component>
</template>
```

:+1: Examples of **correct** code for `camelCase`:

```html
<template>
<component myProp="prop"></component>
</template>
```

## :wrench: Options

Default casing is set to `kebab-case`

```
'vue/html-attributes-casing': [2, 'camelCase'|'kebab-case'|'PascalCase']
```
85 changes: 85 additions & 0 deletions lib/rules/html-attributes-casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @fileoverview Define a style for the props casing in templates.
* @author Armano
*/
'use strict'

const utils = require('../utils')
const casing = require('../utils/casing')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

function create (context) {
const sourceCode = context.getSourceCode()
const options = context.options[0]
const caseType = casing.allowedCaseOptions.indexOf(options) !== -1 ? options : 'kebab-case'

function reportIssue (node, name, newName) {
context.report({
node: node.key,
loc: node.loc,
message: "Attribute '{{name}}' is not {{caseType}}.",
data: {
name,
caseType,
newName
},
fix: fixer => fixer.replaceText(node.key, newName)
})
}

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

utils.registerTemplateBodyVisitor(context, {
'VStartTag' (obj) {
if (!utils.isSvgElementName(obj.id.name) && !utils.isMathMLElementName(obj.id.name)) {
obj.attributes.forEach((node) => {
if (!node.directive) {
const oldValue = node.key.name
if (oldValue.indexOf('data-') !== -1) {
return
}
const value = casing.getConverter(caseType)(oldValue)
if (value !== oldValue) {
reportIssue(node, oldValue, value)
}
} else if (node.key.name === 'bind') {
const oldValue = node.key.argument
if (oldValue.indexOf('data-') !== -1) {
return
}
const text = sourceCode.getText(node.key)
const value = casing.getConverter(caseType)(oldValue)
if (value !== oldValue) {
reportIssue(node, text, text.replace(oldValue, value))
}
}
})
}
}
})

return {}
}

module.exports = {
meta: {
docs: {
description: 'Define a style for the props casing in templates.',
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [
{
enum: casing.allowedCaseOptions
}
]
},

create
}
2 changes: 1 addition & 1 deletion lib/rules/html-no-self-closing.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const utils = require('../utils')
function create (context) {
utils.registerTemplateBodyVisitor(context, {
'VStartTag[selfClosing=true]' (node) {
if (!utils.isSvgElementName(node.id.name)) {
if (!utils.isSvgElementName(node.id.name) && !utils.isMathMLElementName(node.id.name)) {
const pos = node.range[1] - 2
context.report({
node,
Expand Down
46 changes: 5 additions & 41 deletions lib/rules/name-property-casing.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,15 @@
'use strict'

const utils = require('../utils')

function kebabCase (str) {
return str
.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
.replace(/[^a-zA-Z:]+/g, '-')
.toLowerCase()
}

function camelCase (str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
)
.replace(/[^a-zA-Z:]+/g, '')
}

function pascalCase (str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
.replace(/[^a-zA-Z:]+/g, '')
}

const allowedCaseOptions = [
'camelCase',
'kebab-case',
'PascalCase'
]

const convertersMap = {
'kebab-case': kebabCase,
'camelCase': camelCase,
'PascalCase': pascalCase
}

function getConverter (name) {
return convertersMap[name] || pascalCase
}
const casing = require('../utils/casing')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

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

// ----------------------------------------------------------------------
// Public
Expand All @@ -65,7 +29,7 @@ function create (context) {

if (!node) return

const value = getConverter(caseType)(node.value.value)
const value = casing.getConverter(caseType)(node.value.value)
if (value !== node.value.value) {
context.report({
node: node.value,
Expand All @@ -87,10 +51,10 @@ module.exports = {
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code', // or "code" or "whitespace"
fixable: 'code',
schema: [
{
enum: allowedCaseOptions
enum: casing.allowedCaseOptions
}
]
},
Expand Down
42 changes: 42 additions & 0 deletions lib/utils/casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const assert = require('assert')

function kebabCase (str) {
return str
.replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
.replace(/[^a-zA-Z:]+/g, '-')
.toLowerCase()
}

function camelCase (str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (
index === 0 ? letter.toLowerCase() : letter.toUpperCase())
)
.replace(/[^a-zA-Z:]+/g, '')
}

function pascalCase (str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
.replace(/[^a-zA-Z:]+/g, '')
}

const convertersMap = {
'kebab-case': kebabCase,
'camelCase': camelCase,
'PascalCase': pascalCase
}

module.exports = {
allowedCaseOptions: [
'camelCase',
'kebab-case',
'PascalCase'
],

getConverter (name) {
assert(typeof name === 'string')

return convertersMap[name] || pascalCase
}
}
62 changes: 37 additions & 25 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json'))
const MATHML_ELEMENT_NAMES = new Set(require('./mathml-elements.json'))
const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
const assert = require('assert')

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

return (
node.parent.type === 'Program' ||
node.parent.parent.type === 'Program'
node.parent.type === 'Program' ||
node.parent.parent.type === 'Program'
)
},

Expand Down Expand Up @@ -194,49 +195,60 @@ module.exports = {
)
},

/**
* Check whether the given node is a custom component or not.
* @param {ASTNode} node The start tag node to check.
* @returns {boolean} `true` if the node is a custom component.
*/
/**
* Check whether the given node is a custom component or not.
* @param {ASTNode} node The start tag node to check.
* @returns {boolean} `true` if the node is a custom component.
*/
isCustomComponent (node) {
assert(node && node.type === 'VStartTag')

const name = node.id.name
return (
!(this.isHtmlElementName(name) || this.isSvgElementName(name)) ||
this.hasAttribute(node, 'is') ||
this.hasDirective(node, 'bind', 'is')
!(this.isHtmlElementName(name) || this.isSvgElementName(name) || this.isMathMLElementName(name)) ||
this.hasAttribute(node, 'is') ||
this.hasDirective(node, 'bind', 'is')
)
},

/**
* Check whether the given name is a HTML element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a HTML element name.
*/
/**
* Check whether the given name is a HTML element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a HTML element name.
*/
isHtmlElementName (name) {
assert(typeof name === 'string')

return HTML_ELEMENT_NAMES.has(name.toLowerCase())
},

/**
* Check whether the given name is a SVG element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a SVG element name.
*/
/**
* Check whether the given name is a SVG element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a SVG element name.
*/
isSvgElementName (name) {
assert(typeof name === 'string')

return SVG_ELEMENT_NAMES.has(name.toLowerCase())
},

/**
* Check whether the given name is a void element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a void element name.
*/
/**
* Check whether the given name is a MathML element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a HTML element name.
*/
isMathMLElementName (name) {
assert(typeof name === 'string')

return MATHML_ELEMENT_NAMES.has(name.toLowerCase())
},

/**
* Check whether the given name is a void element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a void element name.
*/
isVoidElementName (name) {
assert(typeof name === 'string')

Expand Down
Loading

0 comments on commit 24305db

Please sign in to comment.