Skip to content

Commit

Permalink
Improve support for html attribute casing & add autofix
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 committed Jul 23, 2017
1 parent 7a3c068 commit a4be85d
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 99 deletions.
87 changes: 35 additions & 52 deletions lib/rules/html-attributes-casing.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,55 @@
'use strict'

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

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

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

function pascalCase (str) {
str = camelCase(str)
return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : ''
}

function convertCase (str, caseType) {
if (caseType === 'kebab-case') {
return kebabCase(str)
} else if (caseType === 'PascalCase') {
return pascalCase(str)
}
return camelCase(str)
}
const casing = require('../utils/casing')

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

function create (context) {
const sourceCode = context.getSourceCode()
const options = context.options[0]
const caseType = ['camelCase', 'kebab-case', 'PascalCase'].indexOf(options) !== -1 ? options : 'kebab-case'
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, {
"VAttribute[directive=true][key.name='bind']" (node) {
const value = convertCase(node.key.argument, caseType)
if (value !== node.key.argument) {
context.report({
node: node.key,
loc: node.loc,
message: 'Attribute {{bind}}:{{name}} is not {{caseType}}.',
data: {
name: node.key.argument,
caseType: caseType,
bind: node.key.shorthand ? '' : 'v-bind'
'VStartTag' (obj) {
if (!utils.isSvgElementName(obj.id.name) && !utils.isMathMLElementName(obj.id.name)) {
obj.attributes.forEach((node) => {
if (node.directive && node.key.name === 'bind') {
const text = sourceCode.getText(node.key)
const oldValue = node.key.argument
const value = casing.getConverter(caseType)(oldValue)
if (value !== oldValue) {
reportIssue(node, text, text.replace(oldValue, value))
}
} else if (!node.directive) {
const oldValue = node.key.name
const value = casing.getConverter(caseType)(oldValue)
if (value !== oldValue) {
reportIssue(node, oldValue, value)
}
}
})
}
},
'VAttribute[directive=false]' (node) {
if (node.key.type === 'VIdentifier') {
const value = convertCase(node.key.name, caseType)
if (value !== node.key.name) {
context.report({
node: node.key,
loc: node.loc,
message: 'Attribute {{name}} is not {{caseType}}.',
data: {
name: node.key.name,
caseType: caseType
}
})
}
}
}
})

Expand All @@ -84,10 +67,10 @@ module.exports = {
category: 'Stylistic Issues',
recommended: false
},
fixable: null, // or "code" or "whitespace"
fixable: 'code',
schema: [
{
enum: ['camelCase', 'kebab-case', 'PascalCase']
enum: casing.allowedCaseOptions
}
]
},
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
}
}
47 changes: 41 additions & 6 deletions tests/lib/rules/html-attributes-casing.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ ruleTester.run('html-attributes-casing', rule, {
filename: 'test.vue',
code: '<template><div><component :MyProp="prop"></component></div></template>',
options: ['PascalCase']
},
{
filename: 'test.vue',
code: '<template><div><svg foo-bar="prop"></svg></div></template>',
options: ['PascalCase']
}
],

Expand All @@ -66,7 +71,7 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component my-prop="prop"></component></div></template>',
options: ['camelCase'],
errors: [{
message: 'Attribute my-prop is not camelCase.',
message: "Attribute 'my-prop' is not camelCase.",
type: 'VIdentifier',
line: 1
}]
Expand All @@ -76,7 +81,7 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component my-prop="prop"></component></div></template>',
options: ['PascalCase'],
errors: [{
message: 'Attribute my-prop is not PascalCase.',
message: "Attribute 'my-prop' is not PascalCase.",
type: 'VIdentifier',
line: 1
}]
Expand All @@ -86,7 +91,7 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component MyProp="prop"></component></div></template>',
options: ['kebab-case'],
errors: [{
message: 'Attribute MyProp is not kebab-case.',
message: "Attribute 'MyProp' is not kebab-case.",
type: 'VIdentifier',
line: 1
}]
Expand All @@ -96,7 +101,7 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component :my-prop="prop"></component></div></template>',
options: ['camelCase'],
errors: [{
message: 'Attribute :my-prop is not camelCase.',
message: "Attribute ':my-prop' is not camelCase.",
type: 'VDirectiveKey',
line: 1
}]
Expand All @@ -106,7 +111,7 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component :my-prop="prop"></component></div></template>',
options: ['PascalCase'],
errors: [{
message: 'Attribute :my-prop is not PascalCase.',
message: "Attribute ':my-prop' is not PascalCase.",
type: 'VDirectiveKey',
line: 1
}]
Expand All @@ -116,7 +121,37 @@ ruleTester.run('html-attributes-casing', rule, {
code: '<template><div><component :MyProp="prop"></component></div></template>',
options: ['kebab-case'],
errors: [{
message: 'Attribute :MyProp is not kebab-case.',
message: "Attribute ':MyProp' is not kebab-case.",
type: 'VDirectiveKey',
line: 1
}]
},
{
filename: 'test.vue',
code: '<template><div><component v-bind:my-prop="prop"></component></div></template>',
options: ['camelCase'],
errors: [{
message: "Attribute 'v-bind:my-prop' is not camelCase.",
type: 'VDirectiveKey',
line: 1
}]
},
{
filename: 'test.vue',
code: '<template><div><component v-bind:my-prop="prop"></component></div></template>',
options: ['PascalCase'],
errors: [{
message: "Attribute 'v-bind:my-prop' is not PascalCase.",
type: 'VDirectiveKey',
line: 1
}]
},
{
filename: 'test.vue',
code: '<template><div><component v-bind:MyProp="prop"></component></div></template>',
options: ['kebab-case'],
errors: [{
message: "Attribute 'v-bind:MyProp' is not kebab-case.",
type: 'VDirectiveKey',
line: 1
}]
Expand Down

0 comments on commit a4be85d

Please sign in to comment.