diff --git a/index.js b/index.js index 9fd15e15bf..3074bbc5fa 100644 --- a/index.js +++ b/index.js @@ -64,7 +64,8 @@ var allRules = { 'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'), 'require-extension': require('./lib/rules/require-extension'), 'wrap-multilines': require('./lib/rules/wrap-multilines'), - 'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing') + 'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'), + 'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements') }; function filterRules(rules, predicate) { diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js new file mode 100644 index 0000000000..331abedcc5 --- /dev/null +++ b/lib/rules/no-adjacent-inline-elements.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Prevent adjacent inline elements not separated by whitespace. + * @author Sean Hayes + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements +var inlineNames = ('a b big i small tt abbr acronym cite code dfn em kbd strong' + + ' samp time var bdo br img map object q script span sub sup button input' + + ' label select textarea').split(' '); +var nbsp = ' '; + +function isInline(node) { + if (node.type === 'Literal') { + // Regular whitespace will be removed. + var raw = node.raw.trim(); + // To properly separate inline elements, each end of the literal will need + // a  . + return !(raw.startsWith(nbsp) && raw.endsWith(nbsp)); + } + if (node.type !== 'JSXElement') { + // Skip. + return false; + } + var name = node.openingElement.name.name; + if (inlineNames.indexOf(name) > -1) { + return true; + } + return false; +} + +var ERROR = 'Child elements which render as inline HTML elements should be' + + ' separated by a space or wrapped in block level elements.'; + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + ERROR: ERROR, + meta: { + docs: { + description: 'Prevent adjacent inline elements not separated by whitespace.', + category: 'Best Practices', + recommended: false + }, + schema: [] + }, + create: function(context) { + function validate(node) { + var currentIsInline = false; + var previousIsInline = false; + for (var i = 0; i < node.children.length; i++) { + currentIsInline = isInline(node.children[i]); + if (previousIsInline && currentIsInline) { + context.report({ + node: node, + message: ERROR + }); + return; + } + previousIsInline = currentIsInline; + } + + } + return { + JSXElement: function(node) { + validate(node); + } + }; + } +}; diff --git a/tests/lib/rules/no-adjacent-inline-elements.js b/tests/lib/rules/no-adjacent-inline-elements.js new file mode 100644 index 0000000000..d1a5703f3a --- /dev/null +++ b/tests/lib/rules/no-adjacent-inline-elements.js @@ -0,0 +1,78 @@ +/** + * @fileoverview Tests for no-adjacent-inline-elements + * @author Sean Hayes + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +var rule = require('../../../lib/rules/no-adjacent-inline-elements'); +var RuleTester = require('eslint').RuleTester; + +var ERROR = rule.ERROR; + +var parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +var ruleTester = new RuleTester(); +ruleTester.run('no-adjacent-inline-elements', rule, { + valid: [ + { + code: '
;', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: '

;', + parserOptions: parserOptions + }, + { + code: '

;', + parserOptions: parserOptions + }, + { + code: '
 
;', + parserOptions: parserOptions + }, + { + code: '
 some text  
;', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + } + ], + invalid: [ + { + code: '
;', + errors: [{message: ERROR}], + parserOptions: parserOptions + }, + { + code: '
;', + errors: [{message: ERROR}], + parserOptions: parserOptions + }, + { + code: '
 some text
;', + errors: [{message: ERROR}], + parserOptions: parserOptions + } + ] +});