From 475aaca431fab90a0ead7b21af7a73d4f0324514 Mon Sep 17 00:00:00 2001 From: David Dias Date: Sat, 16 May 2020 16:08:34 -0400 Subject: [PATCH] feat: Add tags checking rule - allows specify rules for any tag and validate that (#384) * adding tags check rule * fix missing commas * add polifil for old JS engines * add polifil for old JS engines * fix missing commas * fix indexOf * incrace code covarage * incrace code covarage * review fix * fix formating * fixing issues Co-authored-by: a.obitskyi --- src/rules/index.js | 1 + src/rules/tags-check.js | 118 ++++++++++++++++++++++++++++++++++ test/rules/tags-check.spec.js | 63 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/rules/tags-check.js create mode 100644 test/rules/tags-check.spec.js diff --git a/src/rules/index.js b/src/rules/index.js index f3d70fed1..2719df7a4 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -26,3 +26,4 @@ export { default as tagSelfClose } from './tag-self-close'; export { default as tagnameLowercase } from './tagname-lowercase'; export { default as tagnameSpecialChars } from './tagname-specialchars'; export { default as titleRequire } from './title-require'; +export { default as tagsCheck } from './tags-check'; diff --git a/src/rules/tags-check.js b/src/rules/tags-check.js new file mode 100644 index 000000000..4ebbcb2da --- /dev/null +++ b/src/rules/tags-check.js @@ -0,0 +1,118 @@ + +var tagsTypings = { + a: { + selfclosing: false, + attrsRequired: ['href', 'title'], + redundantAttrs: ['alt'] + }, + div: { + selfclosing: false + }, + main: { + selfclosing: false, + redundantAttrs: ['role'] + }, + nav: { + selfclosing: false, + redundantAttrs: ['role'] + }, + script: { + attrsOptional: [['async', 'async'], ['defer', 'defer']] + }, + img: { + selfclosing: true, + attrsRequired: [ + 'src', 'alt', 'title' + ] + } +}; + +var assign = function(target) { + var _source; + + for (var i = 1; i < arguments.length; i++) { + _source = arguments[i]; + for (var prop in _source) { + target[prop] = _source[prop]; + } + } + return target; +} + +export default { + id: 'tags-check', + description: 'Checks html tags.', + init: function (parser, reporter, options) { + var self = this; + + if (typeof options !== 'boolean') { + assign(tagsTypings, options); + } + + parser.addListener('tagstart', function (event) { + var attrs = event.attrs; + var col = event.col + event.tagName.length + 1; + + var tagName = event.tagName.toLowerCase(); + + if (tagsTypings[tagName]) { + var currentTagType = tagsTypings[tagName]; + + if (currentTagType.selfclosing === true && !event.close) { + reporter.warn('The <' + tagName + '> tag must be selfclosing.', event.line, event.col, self, event.raw); + } else if (currentTagType.selfclosing === false && event.close) { + reporter.warn('The <' + tagName +'> tag must not be selfclosing.', event.line, event.col, self, event.raw); + } + + if (currentTagType.attrsRequired) { + currentTagType.attrsRequired.forEach(function (id) { + if (Array.isArray(id)) { + var copyOfId = id.map(function (a) { return a;}); + var realID = copyOfId.shift(); + var values = copyOfId; + + if (attrs.some(function (attr) {return attr.name === realID;})) { + attrs.forEach(function (attr) { + if (attr.name === realID && values.indexOf(attr.value) === -1) { + reporter.error('The <' + tagName +'> tag must have attr \'' + realID + '\' with one value of \'' + values.join('\' or \'') + '\'.', event.line, col, self, event.raw); + } + }); + } else { + reporter.error('The <' + tagName + '> tag must have attr \'' + realID + '\'.', event.line, col, self, event.raw); + } + } else if (!attrs.some(function (attr) {return id.split('|').indexOf(attr.name) !== -1;})) { + reporter.error('The <' + tagName + '> tag must have attr \'' + id + '\'.', event.line, col, self, event.raw); + } + }); + } + if (currentTagType.attrsOptional) { + currentTagType.attrsOptional.forEach(function (id) { + if (Array.isArray(id)) { + var copyOfId = id.map(function (a) { return a;}); + var realID = copyOfId.shift(); + var values = copyOfId; + + if (attrs.some(function (attr) {return attr.name === realID;})) { + attrs.forEach(function (attr) { + if (attr.name === realID && values.indexOf(attr.value) === -1) { + reporter.error('The <' + tagName + '> tag must have optional attr \'' + realID + + '\' with one value of \'' + values.join('\' or \'') + '\'.', event.line, col, self, event.raw); + } + }); + } + } + }); + } + + if (currentTagType.redundantAttrs) { + currentTagType.redundantAttrs.forEach(function (attrName) { + if (attrs.some(function (attr) { return attr.name === attrName;})) { + reporter.error('The attr \'' + attrName + '\' is redundant for <' + tagName + '> and should be ommited.', event.line, col, self, event.raw); + } + }); + } + + } + }); + } +}; diff --git a/test/rules/tags-check.spec.js b/test/rules/tags-check.spec.js new file mode 100644 index 000000000..c9a5b061e --- /dev/null +++ b/test/rules/tags-check.spec.js @@ -0,0 +1,63 @@ + +const expect = require("expect.js"); + +const HTMLHint = require('../../dist/htmlhint.js').HTMLHint; + +const ruldId = 'tags-check', + ruleOptions = {}; + +ruleOptions[ruldId] = { + sometag: { + selfclosing: true, + attrsRequired: [['attrname', 'attrvalue']] + } +}; + +describe('Rules: ' + ruldId, function(){ + it('Tag should have requered attrs [title, href]', function(){ + var code = 'blabla'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(2); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[1].rule.id).to.be(ruldId); + }); + it('Tag should not be selfclosing', function(){ + var code = ''; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + }); + it('Tag should be selfclosing', function(){ + var code = 'asd'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + }); + it('Should check optional attributes', function(){ + var code = '