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 = '
';
+ 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 = '';
+ var messages = HTMLHint.verify(code, ruleOptions);
+ expect(messages.length).to.be(1);
+ expect(messages[0].rule.id).to.be(ruldId);
+ });
+ it('Should check redunant attributes', function(){
+ var code = '';
+ 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('Should be extendable trought config', function(){
+ var code = '';
+ var messages = HTMLHint.verify(code, ruleOptions);
+ expect(messages.length).to.be(2);
+ expect(messages[0].rule.id).to.be(ruldId);
+ });
+ it('Should check required attributes with specifyed values', function(){
+ var code = '';
+ var messages = HTMLHint.verify(code, ruleOptions);
+ expect(messages.length).to.be(0);
+ code = '';
+ messages = HTMLHint.verify(code, ruleOptions);
+ expect(messages.length).to.be(1);
+ });
+});