diff --git a/CHANGE.md b/CHANGE.md index a12e4a959..26a75df21 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -1,11 +1,13 @@ HTMLHint change log ==================== -## ver 0.9.6 (2014-6-14) +## ver 0.9.6 (2014-6-18) 1. add rule: attr-no-duplication 2. add rule: space-tab-mixed-disabled -2. add default rule: attr-no-duplication +3. add rule: id-class-ad-disabled +4. add default rule: attr-no-duplication +5. add inline ruleset support ## ver 0.9.4 (2013-9-27) diff --git a/README.md b/README.md index 3237b6d9f..afe511e22 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ HTMLHint is released under the MIT license: > The MIT License > -> Copyright (c) 2013 Yanis Wang \< yanis.wang@gmail.com \> +> Copyright (c) 2014 Yanis Wang \< yanis.wang@gmail.com \> > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal diff --git a/TODO.md b/TODO.md index d92ffb360..16d806046 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,5 @@ TODO 1. add rule: Relative path 2. add rule: Absolute path -3. add rule: adblock -4. add comment support: `` 4. reporter support 5. w3c rule \ No newline at end of file diff --git a/coverage.html b/coverage.html index 34f9d0baa..59327bb97 100644 --- a/coverage.html +++ b/coverage.html @@ -338,4 +338,4 @@ code .string { color: #5890AD } code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

98%
366
361
5

htmlhint.js

98%
366
361
5
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var HTMLHint = {};
8
91 HTMLHint.version = '@VERSION';
10
111 HTMLHint.rules = {};
12
13 //默认配置
141 HTMLHint.defaultRuleset = {
15 'tagname-lowercase': true,
16 'attr-lowercase': true,
17 'attr-value-double-quotes': true,
18 'doctype-first': true,
19 'tag-pair': true,
20 'spec-char-escape': true,
21 'id-unique': true,
22 'src-not-empty': true,
23 'attr-no-duplication': true // added: 2014-6-14
24 };
25
261 HTMLHint.addRule = function(rule){
2719 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, ruleset){
3154 if(ruleset === undefined){
321 ruleset = HTMLHint.defaultRuleset;
33 }
3454 var parser = new HTMLParser(),
35 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
36
3754 var rules = HTMLHint.rules,
38 rule;
3954 for (var id in ruleset){
4062 rule = rules[id];
4162 if (rule !== undefined && ruleset[id] !== false){
4261 rule.init(parser, reporter, ruleset[id]);
43 }
44 }
45
4654 parser.parse(html);
47
4854 return reporter.messages;
49 };
50
511 return HTMLHint;
52
53})();
54
551if (typeof exports === 'object' && exports){
561 exports.HTMLHint = HTMLHint;
57}
58/**
59 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
60 * MIT Licensed
61 */
621(function(HTMLHint, undefined){
63
641 var Reporter = function(){
6554 var self = this;
6654 self._init.apply(self,arguments);
67 };
68
691 Reporter.prototype = {
70 _init: function(lines, ruleset){
7154 var self = this;
7254 self.lines = lines;
7354 self.ruleset = ruleset;
7454 self.messages = [];
75 },
76 //错误
77 error: function(message, line, col, rule, raw){
7834 this.report('error', message, line, col, rule, raw);
79 },
80 //警告
81 warn: function(message, line, col, rule, raw){
8228 this.report('warning', message, line, col, rule, raw);
83 },
84 //信息
85 info: function(message, line, col, rule, raw){
860 this.report('info', message, line, col, rule, raw);
87 },
88 //报告
89 report: function(type, message, line, col, rule, raw){
9062 var self = this;
9162 self.messages.push({
92 type: type,
93 message: message,
94 raw: raw,
95 evidence: self.lines[line-1],
96 line: line,
97 col: col,
98 rule: {
99 id: rule.id,
100 description: rule.description,
101 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
102 }
103 });
104 }
105 };
106
1071 HTMLHint.Reporter = Reporter;
108
109})(HTMLHint);
110/**
111 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
112 * MIT Licensed
113 */
1141var HTMLParser = (function(undefined){
115
1161 var HTMLParser = function(){
11779 var self = this;
11879 self._init.apply(self,arguments);
119 };
120
1211 HTMLParser.prototype = {
122 _init: function(){
12379 var self = this;
12479 self._listeners = {};
12579 self._mapCdataTags = self.makeMap("script,style");
12679 self._arrBlocks = [];
127 },
128
129 makeMap: function(str){
13085 var obj = {}, items = str.split(",");
13185 for ( var i = 0; i < items.length; i++ ){
132242 obj[ items[i] ] = true;
133 }
13485 return obj;
135 },
136
137 // parse html code
138 parse: function(html){
139
14079 var self = this,
141 mapCdataTags = self._mapCdataTags;
142
14379 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
144 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
145 regLine = /\r?\n/g;
146
14779 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14879 var lastLineIndex = 0, line = 1;
14979 var arrBlocks = self._arrBlocks;
150
15179 self.fire('start', {
152 pos: 0,
153 line: 1,
154 col: 1
155 });
156
15779 while((match = regTag.exec(html))){
158174 matchIndex = match.index;
159174 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
16036 text = html.substring(lastIndex, matchIndex);
16136 if(tagCDATA){
16210 arrCDATA.push(text);
163 }
164 else{//文本
16526 saveBlock('text', text, lastIndex);
166 }
167 }
168174 lastIndex = regTag.lastIndex;
169
170174 if((tagName = match[1])){
17153 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
17215 text = arrCDATA.join('');
17315 saveBlock('cdata', text, lastCDATAIndex, {
174 'tagName': tagCDATA,
175 'attrs': attrsCDATA
176 });
17715 tagCDATA = null;
17815 attrsCDATA = null;
17915 arrCDATA = null;
180 }
18153 if(!tagCDATA){
182 //标签结束
18352 saveBlock('tagend', match[0], matchIndex, {
184 'tagName': tagName
185 });
18652 continue;
187 }
188 }
189
190122 if(tagCDATA){
1911 arrCDATA.push(match[0]);
192 }
193 else{
194121 if((tagName = match[4])){//标签开始
195108 arrAttrs = [];
196108 var attrs = match[5],
197 attrMatch,
198 attrMatchCount = 0;
199108 while((attrMatch = regAttr.exec(attrs))){
20097 var name = attrMatch[1],
201 quote = attrMatch[2] ? attrMatch[2] :
202 attrMatch[4] ? attrMatch[4] : '',
203 value = attrMatch[3] ? attrMatch[3] :
204 attrMatch[5] ? attrMatch[5] :
205 attrMatch[6] ? attrMatch[6] : '';
20697 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20797 attrMatchCount += attrMatch[0].length;
208 }
209108 if(attrMatchCount === attrs.length){
210108 saveBlock('tagstart', match[0], matchIndex, {
211 'tagName': tagName,
212 'attrs': arrAttrs,
213 'close': match[6]
214 });
215108 if(mapCdataTags[tagName]){
21615 tagCDATA = tagName;
21715 attrsCDATA = arrAttrs.concat();
21815 arrCDATA = [];
21915 lastCDATAIndex = lastIndex;
220 }
221 }
222 else{//如果出现漏匹配,则把当前内容匹配为text
2230 saveBlock('text', match[0], matchIndex);
224 }
225 }
22613 else if(match[2] || match[3]){//注释标签
22713 saveBlock('comment', match[0], matchIndex, {
228 'content': match[2] || match[3],
229 'long': match[2]?true:false
230 });
231 }
232 }
233 }
234
23579 if(html.length > lastIndex){
236 //结尾文本
23713 text = html.substring(lastIndex, html.length);
23813 saveBlock('text', text, lastIndex);
239 }
240
24179 self.fire('end', {
242 pos: lastIndex,
243 line: line,
244 col: lastIndex - lastLineIndex + 1
245 });
246
247 //存储区块
24879 function saveBlock(type, raw, pos, data){
249227 var col = pos - lastLineIndex + 1;
250227 if(data === undefined){
25139 data = {};
252 }
253227 data.raw = raw;
254227 data.pos = pos;
255227 data.line = line;
256227 data.col = col;
257227 arrBlocks.push(data);
258227 self.fire(type, data);
259227 var lineMatch;
260227 while((lineMatch = regLine.exec(raw))){
26118 line ++;
26218 lastLineIndex = pos + regLine.lastIndex;
263 }
264 }
265
266 },
267
268 // add event
269 addListener: function(types, listener){
27099 var _listeners = this._listeners;
27199 var arrTypes = types.split(/[,\s]/), type;
27299 for(var i=0, l = arrTypes.length;i<l;i++){
273102 type = arrTypes[i];
274102 if (_listeners[type] === undefined){
27595 _listeners[type] = [];
276 }
277102 _listeners[type].push(listener);
278 }
279 },
280
281 // fire event
282 fire: function(type, data){
283385 if (data === undefined){
2840 data = {};
285 }
286385 data.type = type;
287385 var self = this,
288 listeners = [],
289 listenersType = self._listeners[type],
290 listenersAll = self._listeners['all'];
291385 if (listenersType !== undefined){
292112 listeners = listeners.concat(listenersType);
293 }
294385 if (listenersAll !== undefined){
295123 listeners = listeners.concat(listenersAll);
296 }
297385 for (var i = 0, l = listeners.length; i < l; i++){
298234 listeners[i].call(self, data);
299 }
300 },
301
302 // remove event
303 removeListener: function(type, listener){
30413 var listenersType = this._listeners[type];
30513 if(listenersType !== undefined){
30611 for (var i = 0, l = listenersType.length; i < l; i++){
3078 if (listenersType[i] === listener){
3088 listenersType.splice(i, 1);
3098 break;
310 }
311 }
312 }
313 },
314
315 //fix pos if event.raw have \n
316 fixPos: function(event, index){
3173 var text = event.raw.substr(0, index);
3183 var arrLines = text.split(/\r?\n/),
319 lineCount = arrLines.length - 1,
320 line = event.line, col;
3213 if(lineCount > 0){
3221 line += lineCount;
3231 col = arrLines[lineCount].length + 1;
324 }
325 else{
3262 col = event.col + index;
327 }
3283 return {
329 line: line,
330 col: col
331 };
332 },
333
334 // covert array type of attrs to map
335 getMapAttrs: function(arrAttrs){
3366 var mapAttrs = {},
337 attr;
3386 for(var i=0,l=arrAttrs.length;i<l;i++){
3396 attr = arrAttrs[i];
3406 mapAttrs[attr.name] = attr.value;
341 }
3426 return mapAttrs;
343 }
344 };
345
3461 return HTMLParser;
347
348})();
349
3501if (typeof exports === 'object' && exports){
3511 exports.HTMLParser = HTMLParser;
352}
353/**
354 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
355 * MIT Licensed
356 */
3571HTMLHint.addRule({
358 id: 'attr-lowercase',
359 description: 'Attribute name must be lowercase.',
360 init: function(parser, reporter){
3613 var self = this;
3623 parser.addListener('tagstart', function(event){
3633 var attrs = event.attrs,
364 attr,
365 col = event.col + event.tagName.length + 1;
3663 for(var i=0, l=attrs.length;i<l;i++){
3673 attr = attrs[i];
3683 var attrName = attr.name;
3693 if(attrName !== attrName.toLowerCase()){
3702 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
371 }
372 }
373 });
374 }
375});
376/**
377 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
378 * MIT Licensed
379 */
3801HTMLHint.addRule({
381 id: 'attr-no-duplication',
382 description: 'Attribute name can not been duplication.',
383 init: function(parser, reporter){
3843 var self = this;
3853 parser.addListener('tagstart', function(event){
3863 var attrs = event.attrs;
3873 var attr;
3883 var attrName;
3893 var col = event.col + event.tagName.length + 1;
390
3913 var mapAttrName = {};
3923 for(var i=0, l=attrs.length;i<l;i++){
3934 attr = attrs[i];
3944 attrName = attr.name;
3954 if(mapAttrName[attrName] === true){
3961 reporter.error('The name of attribute [ '+attr.name+' ] been duplication.', event.line, col + attr.index, self, attr.raw);
397 }
3984 mapAttrName[attrName] = true;
399 }
400 });
401 }
402});
403/**
404 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
405 * MIT Licensed
406 */
4071HTMLHint.addRule({
408 id: 'attr-value-double-quotes',
409 description: 'Attribute value must closed by double quotes.',
410 init: function(parser, reporter){
4113 var self = this;
4123 parser.addListener('tagstart', function(event){
4133 var attrs = event.attrs,
414 attr,
415 col = event.col + event.tagName.length + 1;
4163 for(var i=0, l=attrs.length;i<l;i++){
4177 attr = attrs[i];
4187 if((attr.value !== '' && attr.quote !== '"') ||
419 (attr.value === '' && attr.quote === "'")){
4203 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
421 }
422 }
423 });
424 }
425});
426/**
427 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
428 * MIT Licensed
429 */
4301HTMLHint.addRule({
431 id: 'attr-value-not-empty',
432 description: 'Attribute must set value.',
433 init: function(parser, reporter){
4343 var self = this;
4353 parser.addListener('tagstart', function(event){
4363 var attrs = event.attrs,
437 attr,
438 col = event.col + event.tagName.length + 1;
4393 for(var i=0, l=attrs.length;i<l;i++){
4403 attr = attrs[i];
4413 if(attr.quote === '' && attr.value === ''){
4421 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
443 }
444 }
445 });
446 }
447});
448/**
449 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
450 * MIT Licensed
451 */
4521HTMLHint.addRule({
453 id: 'csslint',
454 description: 'Scan css with csslint.',
455 init: function(parser, reporter, options){
4561 var self = this;
4571 parser.addListener('cdata', function(event){
4581 if(event.tagName.toLowerCase() === 'style'){
459
4601 var cssVerify;
461
4621 if(typeof exports === 'object' && require){
4631 cssVerify = require("csslint").CSSLint.verify;
464 }
465 else{
4660 cssVerify = CSSLint.verify;
467 }
468
4691 if(options !== undefined){
4701 var styleLine = event.line - 1,
471 styleCol = event.col - 1;
4721 try{
4731 var messages = cssVerify(event.raw, options).messages;
4741 messages.forEach(function(error){
4752 var line = error.line;
4762 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
477 });
478 }
479 catch(e){}
480 }
481
482 }
483 });
484 }
485});
486/**
487 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
488 * MIT Licensed
489 */
4901HTMLHint.addRule({
491 id: 'doctype-first',
492 description: 'Doctype must be first.',
493 init: function(parser, reporter){
4943 var self = this;
4953 var allEvent = function(event){
4966 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4973 return;
498 }
4993 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5002 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
501 }
5023 parser.removeListener('all', allEvent);
503 };
5043 parser.addListener('all', allEvent);
505 }
506});
507/**
508 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
509 * MIT Licensed
510 */
5111HTMLHint.addRule({
512 id: 'doctype-html5',
513 description: 'Doctype must be html5.',
514 init: function(parser, reporter){
5152 var self = this;
5162 function onComment(event){
5179 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5181 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
519 }
520 }
5212 function onTagStart(){
5222 parser.removeListener('comment', onComment);
5232 parser.removeListener('tagstart', onTagStart);
524 }
5252 parser.addListener('all', onComment);
5262 parser.addListener('tagstart', onTagStart);
527 }
528});
529/**
530 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
531 * MIT Licensed
532 */
5331HTMLHint.addRule({
534 id: 'head-script-disabled',
535 description: 'The script tag can not be used in head.',
536 init: function(parser, reporter){
5373 var self = this;
5383 function onTagStart(event){
5395 if(event.tagName.toLowerCase() === 'script'){
5402 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
541 }
542 }
5433 function onTagEnd(event){
5447 if(event.tagName.toLowerCase() === 'head'){
5453 parser.removeListener('tagstart', onTagStart);
5463 parser.removeListener('tagstart', onTagEnd);
547 }
548 }
5493 parser.addListener('tagstart', onTagStart);
5503 parser.addListener('tagend', onTagEnd);
551 }
552});
553/**
554 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
555 * MIT Licensed
556 */
5571HTMLHint.addRule({
558 id: 'id-class-value',
559 description: 'Id and class value must meet some rules.',
560 init: function(parser, reporter, options){
5618 var self = this;
5628 var arrRules = {
563 'underline': {
564 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
565 'message': 'Id and class value must lower case and split by underline.'
566 },
567 'dash': {
568 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
569 'message': 'Id and class value must lower case and split by dash.'
570 },
571 'hump': {
572 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
573 'message': 'Id and class value must meet hump style.'
574 }
575 }, rule;
5768 if(typeof options === 'string'){
5776 rule = arrRules[options];
578 }
579 else{
5802 rule = options;
581 }
5828 if(rule && rule.regId){
5838 var regId = rule.regId,
584 message = rule.message;
5858 parser.addListener('tagstart', function(event){
5868 var attrs = event.attrs,
587 attr,
588 col = event.col + event.tagName.length + 1;
5898 for(var i=0, l1=attrs.length;i<l1;i++){
59016 attr = attrs[i];
59116 if(attr.name.toLowerCase() === 'id'){
5928 if(regId.test(attr.value) === false){
5934 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
594 }
595 }
59616 if(attr.name.toLowerCase() === 'class'){
5978 var arrClass = attr.value.split(/\s+/g), classValue;
5988 for(var j=0, l2=arrClass.length;j<l2;j++){
5998 classValue = arrClass[j];
6008 if(classValue && regId.test(classValue) === false){
6014 reporter.warn(message, event.line, col + attr.index, self, classValue);
602 }
603 }
604 }
605 }
606 });
607 }
608 }
609});
610/**
611 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
612 * MIT Licensed
613 */
6141HTMLHint.addRule({
615 id: 'id-unique',
616 description: 'Id must be unique.',
617 init: function(parser, reporter){
6183 var self = this;
6193 var mapIdCount = {};
6203 parser.addListener('tagstart', function(event){
6215 var attrs = event.attrs,
622 attr,
623 id,
624 col = event.col + event.tagName.length + 1;
6255 for(var i=0, l=attrs.length;i<l;i++){
6265 attr = attrs[i];
6275 if(attr.name.toLowerCase() === 'id'){
6284 id = attr.value;
6294 if(id){
6304 if(mapIdCount[id] === undefined){
6313 mapIdCount[id] = 1;
632 }
633 else{
6341 mapIdCount[id] ++;
635 }
6364 if(mapIdCount[id] > 1){
6371 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
638 }
639 }
6404 break;
641 }
642 }
643 });
644 }
645});
646/**
647 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
648 * MIT Licensed
649 */
6501HTMLHint.addRule({
651 id: 'img-alt-require',
652 description: 'Alt of img tag must be set value.',
653 init: function(parser, reporter){
6543 var self = this;
6553 parser.addListener('tagstart', function(event){
6563 if(event.tagName.toLowerCase() === 'img'){
6573 var attrs = event.attrs;
6583 var haveAlt = false;
6593 for(var i=0, l=attrs.length;i<l;i++){
6608 if(attrs[i].name.toLowerCase() === 'alt'){
6612 haveAlt = true;
6622 break;
663 }
664 }
6653 if(haveAlt === false){
6661 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
667 }
668 }
669 });
670 }
671});
672/**
673 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
674 * MIT Licensed
675 */
6761HTMLHint.addRule({
677 id: 'jshint',
678 description: 'Scan script with jshint.',
679 init: function(parser, reporter, options){
6804 var self = this;
6814 parser.addListener('cdata', function(event){
6824 if(event.tagName.toLowerCase() === 'script'){
683
6844 var mapAttrs = parser.getMapAttrs(event.attrs),
685 type = mapAttrs.type;
686
687 // Only scan internal javascript
6884 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6892 return;
690 }
691
6922 var jsVerify;
693
6942 if(typeof exports === 'object' && require){
6952 jsVerify = require('jshint').JSHINT;
696 }
697 else{
6980 jsVerify = JSHINT;
699 }
700
7012 if(options !== undefined){
7022 var styleLine = event.line - 1,
703 styleCol = event.col - 1;
7042 var code = event.raw.replace(/\t/g,' ');
7052 try{
7062 var status = jsVerify(code, options);
7072 if(status === false){
7082 jsVerify.errors.forEach(function(error){
7098 var line = error.line;
7108 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
711 });
712 }
713 }
714 catch(e){}
715 }
716
717 }
718 });
719 }
720});
721/**
722 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
723 * MIT Licensed
724 */
7251HTMLHint.addRule({
726 id: 'space-tab-mixed-disabled',
727 description: 'Spaces and tabs can not mixed in front of line.',
728 init: function(parser, reporter){
7294 var self = this;
7304 parser.addListener('text', function(event){
7318 if(event.pos === 0 && /^( +\t|\t+ )/.test(event.raw) === true){
7322 reporter.warn('Mixed spaces and tabs in front of line.', event.line, 0, self, event.raw);
733 }
734 });
735 }
736});
737/**
738 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
739 * MIT Licensed
740 */
7411HTMLHint.addRule({
742 id: 'spec-char-escape',
743 description: 'Special characters must be escaped.',
744 init: function(parser, reporter){
7453 var self = this;
7463 parser.addListener('text', function(event){
7473 var raw = event.raw,
748 reSpecChar = /[<>]/g,
749 match;
7503 while((match = reSpecChar.exec(raw))){
7513 var fixedPos = parser.fixPos(event, match.index);
7523 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
753 }
754 });
755 }
756});
757/**
758 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
759 * MIT Licensed
760 */
7611HTMLHint.addRule({
762 id: 'src-not-empty',
763 description: 'Src of img(script,link) must set value.',
764 init: function(parser, reporter){
7654 var self = this;
7664 parser.addListener('tagstart', function(event){
76729 var tagName = event.tagName,
768 attrs = event.attrs,
769 attr,
770 col = event.col + tagName.length + 1;
77129 for(var i=0, l=attrs.length;i<l;i++){
77230 attr = attrs[i];
77330 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
774 (tagName === 'link' && attr.name === 'href') ||
775 (tagName === 'object' && attr.name === 'data')) &&
776 attr.value === ''){
77714 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
778 }
779 }
780 });
781 }
782});
783/**
784 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
785 * MIT Licensed
786 */
7871HTMLHint.addRule({
788 id: 'style-disabled',
789 description: 'Style tag can not be use.',
790 init: function(parser, reporter){
7912 var self = this;
7922 parser.addListener('tagstart', function(event){
7934 if(event.tagName.toLowerCase() === 'style'){
7941 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
795 }
796 });
797 }
798});
799/**
800 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
801 * MIT Licensed
802 */
8031HTMLHint.addRule({
804 id: 'tag-pair',
805 description: 'Tag must be paired.',
806 init: function(parser, reporter){
8074 var self = this;
8084 var stack=[],
809 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8104 parser.addListener('tagstart', function(event){
8115 var tagName = event.tagName.toLowerCase();
8125 if (mapEmptyTags[tagName] === undefined && !event.close){
8135 stack.push(tagName);
814 }
815 });
8164 parser.addListener('tagend', function(event){
8173 var tagName = event.tagName.toLowerCase();
818 //向上寻找匹配的开始标签
8193 for(var pos = stack.length-1;pos >= 0; pos--){
8203 if(stack[pos] === tagName){
8212 break;
822 }
823 }
8243 if(pos >= 0){
8252 var arrTags = [];
8262 for(var i=stack.length-1;i>pos;i--){
8271 arrTags.push('</'+stack[i]+'>');
828 }
8292 if(arrTags.length > 0){
8301 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
831 }
8322 stack.length=pos;
833 }
834 else{
8351 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
836 }
837 });
8384 parser.addListener('end', function(event){
8394 var arrTags = [];
8404 for(var i=stack.length-1;i>=0;i--){
8412 arrTags.push('</'+stack[i]+'>');
842 }
8434 if(arrTags.length > 0){
8442 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
845 }
846 });
847 }
848});
849/**
850 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
851 * MIT Licensed
852 */
8531HTMLHint.addRule({
854 id: 'tag-self-close',
855 description: 'The empty tag must closed by self.',
856 init: function(parser, reporter){
8572 var self = this;
8582 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8592 parser.addListener('tagstart', function(event){
8604 var tagName = event.tagName.toLowerCase();
8614 if(mapEmptyTags[tagName] !== undefined){
8624 if(!event.close){
8632 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
864 }
865 }
866 });
867 }
868});
869/**
870 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
871 * MIT Licensed
872 */
8731HTMLHint.addRule({
874 id: 'tagname-lowercase',
875 description: 'Tagname must be lowercase.',
876 init: function(parser, reporter){
8773 var self = this;
8783 parser.addListener('tagstart,tagend', function(event){
8799 var tagName = event.tagName;
8809 if(tagName !== tagName.toLowerCase()){
8814 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
882 }
883 });
884 }
885});
\ No newline at end of file +

Coverage

98%
395
389
6

htmlhint.js

98%
395
389
6
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var HTMLHint = {};
8
91 HTMLHint.version = '@VERSION';
10
111 HTMLHint.rules = {};
12
13 //默认配置
141 HTMLHint.defaultRuleset = {
15 'tagname-lowercase': true,
16 'attr-lowercase': true,
17 'attr-value-double-quotes': true,
18 'doctype-first': true,
19 'tag-pair': true,
20 'spec-char-escape': true,
21 'id-unique': true,
22 'src-not-empty': true,
23 'attr-no-duplication': true
24 };
25
261 HTMLHint.addRule = function(rule){
2720 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, newRuleset){
3174 var id;
3274 var ruleset;
33 // parse inline ruleset
3474 html = html.replace(/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->\s*/i, function(all, strRuleset){
352 ruleset = {};
362 strRuleset.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g, function(all, key, value){
372 if(value === 'false'){
380 value = false;
39 }
402 else if(value === 'true'){
412 value = true;
42 }
432 ruleset[key] = value;
44 });
452 return '';
46 });
4774 if(newRuleset !== undefined){
4871 ruleset = ruleset || {};
4971 for (id in newRuleset){
5071 ruleset[id] = newRuleset[id];
51 }
52 }
5374 if(ruleset === undefined){
542 ruleset = HTMLHint.defaultRuleset;
55 }
56
5774 var parser = new HTMLParser();
5874 var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
59
6074 var rules = HTMLHint.rules,
61 rule;
6274 for (id in ruleset){
6390 rule = rules[id];
6490 if (rule !== undefined && ruleset[id] !== false){
6588 rule.init(parser, reporter, ruleset[id]);
66 }
67 }
68
6974 parser.parse(html);
70
7174 return reporter.messages;
72 };
73
741 return HTMLHint;
75
76})();
77
781if (typeof exports === 'object' && exports){
791 exports.HTMLHint = HTMLHint;
80}
81/**
82 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
83 * MIT Licensed
84 */
851(function(HTMLHint, undefined){
86
871 var Reporter = function(){
8874 var self = this;
8974 self._init.apply(self,arguments);
90 };
91
921 Reporter.prototype = {
93 _init: function(lines, ruleset){
9474 var self = this;
9574 self.lines = lines;
9674 self.ruleset = ruleset;
9774 self.messages = [];
98 },
99 //错误
100 error: function(message, line, col, rule, raw){
10143 this.report('error', message, line, col, rule, raw);
102 },
103 //警告
104 warn: function(message, line, col, rule, raw){
10543 this.report('warning', message, line, col, rule, raw);
106 },
107 //信息
108 info: function(message, line, col, rule, raw){
1090 this.report('info', message, line, col, rule, raw);
110 },
111 //报告
112 report: function(type, message, line, col, rule, raw){
11386 var self = this;
11486 self.messages.push({
115 type: type,
116 message: message,
117 raw: raw,
118 evidence: self.lines[line-1],
119 line: line,
120 col: col,
121 rule: {
122 id: rule.id,
123 description: rule.description,
124 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
125 }
126 });
127 }
128 };
129
1301 HTMLHint.Reporter = Reporter;
131
132})(HTMLHint);
133/**
134 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
135 * MIT Licensed
136 */
1371var HTMLParser = (function(undefined){
138
1391 var HTMLParser = function(){
14099 var self = this;
14199 self._init.apply(self,arguments);
142 };
143
1441 HTMLParser.prototype = {
145 _init: function(){
14699 var self = this;
14799 self._listeners = {};
14899 self._mapCdataTags = self.makeMap("script,style");
14999 self._arrBlocks = [];
150 },
151
152 makeMap: function(str){
153106 var obj = {}, items = str.split(",");
154106 for ( var i = 0; i < items.length; i++ ){
155296 obj[ items[i] ] = true;
156 }
157106 return obj;
158 },
159
160 // parse html code
161 parse: function(html){
162
16399 var self = this,
164 mapCdataTags = self._mapCdataTags;
165
16699 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
167 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
168 regLine = /\r?\n/g;
169
17099 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
17199 var lastLineIndex = 0, line = 1;
17299 var arrBlocks = self._arrBlocks;
173
17499 self.fire('start', {
175 pos: 0,
176 line: 1,
177 col: 1
178 });
179
18099 while((match = regTag.exec(html))){
181214 matchIndex = match.index;
182214 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
18354 text = html.substring(lastIndex, matchIndex);
18454 if(tagCDATA){
18510 arrCDATA.push(text);
186 }
187 else{//文本
18844 saveBlock('text', text, lastIndex);
189 }
190 }
191214 lastIndex = regTag.lastIndex;
192
193214 if((tagName = match[1])){
19471 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
19515 text = arrCDATA.join('');
19615 saveBlock('cdata', text, lastCDATAIndex, {
197 'tagName': tagCDATA,
198 'attrs': attrsCDATA
199 });
20015 tagCDATA = null;
20115 attrsCDATA = null;
20215 arrCDATA = null;
203 }
20471 if(!tagCDATA){
205 //标签结束
20670 saveBlock('tagend', match[0], matchIndex, {
207 'tagName': tagName
208 });
20970 continue;
210 }
211 }
212
213144 if(tagCDATA){
2141 arrCDATA.push(match[0]);
215 }
216 else{
217143 if((tagName = match[4])){//标签开始
218130 arrAttrs = [];
219130 var attrs = match[5],
220 attrMatch,
221 attrMatchCount = 0;
222130 while((attrMatch = regAttr.exec(attrs))){
223125 var name = attrMatch[1],
224 quote = attrMatch[2] ? attrMatch[2] :
225 attrMatch[4] ? attrMatch[4] : '',
226 value = attrMatch[3] ? attrMatch[3] :
227 attrMatch[5] ? attrMatch[5] :
228 attrMatch[6] ? attrMatch[6] : '';
229125 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
230125 attrMatchCount += attrMatch[0].length;
231 }
232130 if(attrMatchCount === attrs.length){
233130 saveBlock('tagstart', match[0], matchIndex, {
234 'tagName': tagName,
235 'attrs': arrAttrs,
236 'close': match[6]
237 });
238130 if(mapCdataTags[tagName]){
23915 tagCDATA = tagName;
24015 attrsCDATA = arrAttrs.concat();
24115 arrCDATA = [];
24215 lastCDATAIndex = lastIndex;
243 }
244 }
245 else{//如果出现漏匹配,则把当前内容匹配为text
2460 saveBlock('text', match[0], matchIndex);
247 }
248 }
24913 else if(match[2] || match[3]){//注释标签
25013 saveBlock('comment', match[0], matchIndex, {
251 'content': match[2] || match[3],
252 'long': match[2]?true:false
253 });
254 }
255 }
256 }
257
25899 if(html.length > lastIndex){
259 //结尾文本
26013 text = html.substring(lastIndex, html.length);
26113 saveBlock('text', text, lastIndex);
262 }
263
26499 self.fire('end', {
265 pos: lastIndex,
266 line: line,
267 col: lastIndex - lastLineIndex + 1
268 });
269
270 //存储区块
27199 function saveBlock(type, raw, pos, data){
272285 var col = pos - lastLineIndex + 1;
273285 if(data === undefined){
27457 data = {};
275 }
276285 data.raw = raw;
277285 data.pos = pos;
278285 data.line = line;
279285 data.col = col;
280285 arrBlocks.push(data);
281285 self.fire(type, data);
282285 var lineMatch;
283285 while((lineMatch = regLine.exec(raw))){
28418 line ++;
28518 lastLineIndex = pos + regLine.lastIndex;
286 }
287 }
288
289 },
290
291 // add event
292 addListener: function(types, listener){
293128 var _listeners = this._listeners;
294128 var arrTypes = types.split(/[,\s]/), type;
295128 for(var i=0, l = arrTypes.length;i<l;i++){
296132 type = arrTypes[i];
297132 if (_listeners[type] === undefined){
298118 _listeners[type] = [];
299 }
300132 _listeners[type].push(listener);
301 }
302 },
303
304 // fire event
305 fire: function(type, data){
306483 if (data === undefined){
3070 data = {};
308 }
309483 data.type = type;
310483 var self = this,
311 listeners = [],
312 listenersType = self._listeners[type],
313 listenersAll = self._listeners['all'];
314483 if (listenersType !== undefined){
315136 listeners = listeners.concat(listenersType);
316 }
317483 if (listenersAll !== undefined){
318130 listeners = listeners.concat(listenersAll);
319 }
320483 for (var i = 0, l = listeners.length; i < l; i++){
321279 listeners[i].call(self, data);
322 }
323 },
324
325 // remove event
326 removeListener: function(type, listener){
32714 var listenersType = this._listeners[type];
32814 if(listenersType !== undefined){
32912 for (var i = 0, l = listenersType.length; i < l; i++){
3309 if (listenersType[i] === listener){
3319 listenersType.splice(i, 1);
3329 break;
333 }
334 }
335 }
336 },
337
338 //fix pos if event.raw have \n
339 fixPos: function(event, index){
3404 var text = event.raw.substr(0, index);
3414 var arrLines = text.split(/\r?\n/),
342 lineCount = arrLines.length - 1,
343 line = event.line, col;
3444 if(lineCount > 0){
3451 line += lineCount;
3461 col = arrLines[lineCount].length + 1;
347 }
348 else{
3493 col = event.col + index;
350 }
3514 return {
352 line: line,
353 col: col
354 };
355 },
356
357 // covert array type of attrs to map
358 getMapAttrs: function(arrAttrs){
3596 var mapAttrs = {},
360 attr;
3616 for(var i=0,l=arrAttrs.length;i<l;i++){
3626 attr = arrAttrs[i];
3636 mapAttrs[attr.name] = attr.value;
364 }
3656 return mapAttrs;
366 }
367 };
368
3691 return HTMLParser;
370
371})();
372
3731if (typeof exports === 'object' && exports){
3741 exports.HTMLParser = HTMLParser;
375}
376/**
377 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
378 * MIT Licensed
379 */
3801HTMLHint.addRule({
381 id: 'attr-lowercase',
382 description: 'Attribute name must be lowercase.',
383 init: function(parser, reporter){
3844 var self = this;
3854 parser.addListener('tagstart', function(event){
3866 var attrs = event.attrs,
387 attr,
388 col = event.col + event.tagName.length + 1;
3896 for(var i=0, l=attrs.length;i<l;i++){
3909 attr = attrs[i];
3919 var attrName = attr.name;
3929 if(attrName !== attrName.toLowerCase()){
3933 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
394 }
395 }
396 });
397 }
398});
399/**
400 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
401 * MIT Licensed
402 */
4031HTMLHint.addRule({
404 id: 'attr-no-duplication',
405 description: 'Attribute name can not been duplication.',
406 init: function(parser, reporter){
4074 var self = this;
4084 parser.addListener('tagstart', function(event){
4096 var attrs = event.attrs;
4106 var attr;
4116 var attrName;
4126 var col = event.col + event.tagName.length + 1;
413
4146 var mapAttrName = {};
4156 for(var i=0, l=attrs.length;i<l;i++){
41610 attr = attrs[i];
41710 attrName = attr.name;
41810 if(mapAttrName[attrName] === true){
4192 reporter.error('The name of attribute [ '+attr.name+' ] been duplication.', event.line, col + attr.index, self, attr.raw);
420 }
42110 mapAttrName[attrName] = true;
422 }
423 });
424 }
425});
426/**
427 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
428 * MIT Licensed
429 */
4301HTMLHint.addRule({
431 id: 'attr-value-double-quotes',
432 description: 'Attribute value must closed by double quotes.',
433 init: function(parser, reporter){
4344 var self = this;
4354 parser.addListener('tagstart', function(event){
4366 var attrs = event.attrs,
437 attr,
438 col = event.col + event.tagName.length + 1;
4396 for(var i=0, l=attrs.length;i<l;i++){
44013 attr = attrs[i];
44113 if((attr.value !== '' && attr.quote !== '"') ||
442 (attr.value === '' && attr.quote === "'")){
4434 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
444 }
445 }
446 });
447 }
448});
449/**
450 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
451 * MIT Licensed
452 */
4531HTMLHint.addRule({
454 id: 'attr-value-not-empty',
455 description: 'Attribute must set value.',
456 init: function(parser, reporter){
4573 var self = this;
4583 parser.addListener('tagstart', function(event){
4593 var attrs = event.attrs,
460 attr,
461 col = event.col + event.tagName.length + 1;
4623 for(var i=0, l=attrs.length;i<l;i++){
4633 attr = attrs[i];
4643 if(attr.quote === '' && attr.value === ''){
4651 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
466 }
467 }
468 });
469 }
470});
471/**
472 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
473 * MIT Licensed
474 */
4751HTMLHint.addRule({
476 id: 'csslint',
477 description: 'Scan css with csslint.',
478 init: function(parser, reporter, options){
4791 var self = this;
4801 parser.addListener('cdata', function(event){
4811 if(event.tagName.toLowerCase() === 'style'){
482
4831 var cssVerify;
484
4851 if(typeof exports === 'object' && require){
4861 cssVerify = require("csslint").CSSLint.verify;
487 }
488 else{
4890 cssVerify = CSSLint.verify;
490 }
491
4921 if(options !== undefined){
4931 var styleLine = event.line - 1,
494 styleCol = event.col - 1;
4951 try{
4961 var messages = cssVerify(event.raw, options).messages;
4971 messages.forEach(function(error){
4982 var line = error.line;
4992 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
500 });
501 }
502 catch(e){}
503 }
504
505 }
506 });
507 }
508});
509/**
510 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
511 * MIT Licensed
512 */
5131HTMLHint.addRule({
514 id: 'doctype-first',
515 description: 'Doctype must be first.',
516 init: function(parser, reporter){
5174 var self = this;
5184 var allEvent = function(event){
5198 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
5204 return;
521 }
5224 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5233 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
524 }
5254 parser.removeListener('all', allEvent);
526 };
5274 parser.addListener('all', allEvent);
528 }
529});
530/**
531 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
532 * MIT Licensed
533 */
5341HTMLHint.addRule({
535 id: 'doctype-html5',
536 description: 'Doctype must be html5.',
537 init: function(parser, reporter){
5382 var self = this;
5392 function onComment(event){
5409 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5411 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
542 }
543 }
5442 function onTagStart(){
5452 parser.removeListener('comment', onComment);
5462 parser.removeListener('tagstart', onTagStart);
547 }
5482 parser.addListener('all', onComment);
5492 parser.addListener('tagstart', onTagStart);
550 }
551});
552/**
553 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
554 * MIT Licensed
555 */
5561HTMLHint.addRule({
557 id: 'head-script-disabled',
558 description: 'The script tag can not be used in head.',
559 init: function(parser, reporter){
5603 var self = this;
5613 function onTagStart(event){
5625 if(event.tagName.toLowerCase() === 'script'){
5632 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
564 }
565 }
5663 function onTagEnd(event){
5677 if(event.tagName.toLowerCase() === 'head'){
5683 parser.removeListener('tagstart', onTagStart);
5693 parser.removeListener('tagstart', onTagEnd);
570 }
571 }
5723 parser.addListener('tagstart', onTagStart);
5733 parser.addListener('tagend', onTagEnd);
574 }
575});
576/**
577 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
578 * MIT Licensed
579 */
5801HTMLHint.addRule({
581 id: 'id-class-ad-disabled',
582 description: 'Id and class can not use ad keyword, it will blocked by adblock software.',
583 init: function(parser, reporter){
58417 var self = this;
58517 parser.addListener('tagstart', function(event){
58617 var attrs = event.attrs;
58717 var attr;
58817 var attrName;
58917 var col = event.col + event.tagName.length + 1;
590
59117 for(var i=0, l=attrs.length;i<l;i++){
59220 attr = attrs[i];
59320 attrName = attr.name;
59420 if(/^(id|class)$/i.test(attrName)){
59520 if(/(^|[-\_])ad([-\_]|$)/i.test(attr.value)){
59614 reporter.warn('The value of '+attrName+' can not use ad keyword.', event.line, col + attr.index, self, attr.raw);
597 }
598 }
599 }
600 });
601 }
602});
603/**
604 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
605 * MIT Licensed
606 */
6071HTMLHint.addRule({
608 id: 'id-class-value',
609 description: 'Id and class value must meet some rules.',
610 init: function(parser, reporter, options){
6118 var self = this;
6128 var arrRules = {
613 'underline': {
614 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
615 'message': 'Id and class value must lower case and split by underline.'
616 },
617 'dash': {
618 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
619 'message': 'Id and class value must lower case and split by dash.'
620 },
621 'hump': {
622 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
623 'message': 'Id and class value must meet hump style.'
624 }
625 }, rule;
6268 if(typeof options === 'string'){
6276 rule = arrRules[options];
628 }
629 else{
6302 rule = options;
631 }
6328 if(rule && rule.regId){
6338 var regId = rule.regId,
634 message = rule.message;
6358 parser.addListener('tagstart', function(event){
6368 var attrs = event.attrs,
637 attr,
638 col = event.col + event.tagName.length + 1;
6398 for(var i=0, l1=attrs.length;i<l1;i++){
64016 attr = attrs[i];
64116 if(attr.name.toLowerCase() === 'id'){
6428 if(regId.test(attr.value) === false){
6434 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
644 }
645 }
64616 if(attr.name.toLowerCase() === 'class'){
6478 var arrClass = attr.value.split(/\s+/g), classValue;
6488 for(var j=0, l2=arrClass.length;j<l2;j++){
6498 classValue = arrClass[j];
6508 if(classValue && regId.test(classValue) === false){
6514 reporter.warn(message, event.line, col + attr.index, self, classValue);
652 }
653 }
654 }
655 }
656 });
657 }
658 }
659});
660/**
661 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
662 * MIT Licensed
663 */
6641HTMLHint.addRule({
665 id: 'id-unique',
666 description: 'Id must be unique.',
667 init: function(parser, reporter){
6684 var self = this;
6694 var mapIdCount = {};
6704 parser.addListener('tagstart', function(event){
6718 var attrs = event.attrs,
672 attr,
673 id,
674 col = event.col + event.tagName.length + 1;
6758 for(var i=0, l=attrs.length;i<l;i++){
67611 attr = attrs[i];
67711 if(attr.name.toLowerCase() === 'id'){
6786 id = attr.value;
6796 if(id){
6806 if(mapIdCount[id] === undefined){
6814 mapIdCount[id] = 1;
682 }
683 else{
6842 mapIdCount[id] ++;
685 }
6866 if(mapIdCount[id] > 1){
6872 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
688 }
689 }
6906 break;
691 }
692 }
693 });
694 }
695});
696/**
697 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
698 * MIT Licensed
699 */
7001HTMLHint.addRule({
701 id: 'img-alt-require',
702 description: 'Alt of img tag must be set value.',
703 init: function(parser, reporter){
7044 var self = this;
7054 parser.addListener('tagstart', function(event){
7064 if(event.tagName.toLowerCase() === 'img'){
7074 var attrs = event.attrs;
7084 var haveAlt = false;
7094 for(var i=0, l=attrs.length;i<l;i++){
7109 if(attrs[i].name.toLowerCase() === 'alt'){
7112 haveAlt = true;
7122 break;
713 }
714 }
7154 if(haveAlt === false){
7162 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
717 }
718 }
719 });
720 }
721});
722/**
723 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
724 * MIT Licensed
725 */
7261HTMLHint.addRule({
727 id: 'jshint',
728 description: 'Scan script with jshint.',
729 init: function(parser, reporter, options){
7304 var self = this;
7314 parser.addListener('cdata', function(event){
7324 if(event.tagName.toLowerCase() === 'script'){
733
7344 var mapAttrs = parser.getMapAttrs(event.attrs),
735 type = mapAttrs.type;
736
737 // Only scan internal javascript
7384 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
7392 return;
740 }
741
7422 var jsVerify;
743
7442 if(typeof exports === 'object' && require){
7452 jsVerify = require('jshint').JSHINT;
746 }
747 else{
7480 jsVerify = JSHINT;
749 }
750
7512 if(options !== undefined){
7522 var styleLine = event.line - 1,
753 styleCol = event.col - 1;
7542 var code = event.raw.replace(/\t/g,' ');
7552 try{
7562 var status = jsVerify(code, options);
7572 if(status === false){
7582 jsVerify.errors.forEach(function(error){
7598 var line = error.line;
7608 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
761 });
762 }
763 }
764 catch(e){}
765 }
766
767 }
768 });
769 }
770});
771/**
772 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
773 * MIT Licensed
774 */
7751HTMLHint.addRule({
776 id: 'space-tab-mixed-disabled',
777 description: 'Spaces and tabs can not mixed in front of line.',
778 init: function(parser, reporter){
7794 var self = this;
7804 parser.addListener('text', function(event){
7818 if(event.pos === 0 && /^( +\t|\t+ )/.test(event.raw) === true){
7822 reporter.warn('Mixed spaces and tabs in front of line.', event.line, 0, self, event.raw);
783 }
784 });
785 }
786});
787/**
788 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
789 * MIT Licensed
790 */
7911HTMLHint.addRule({
792 id: 'spec-char-escape',
793 description: 'Special characters must be escaped.',
794 init: function(parser, reporter){
7954 var self = this;
7964 parser.addListener('text', function(event){
7974 var raw = event.raw,
798 reSpecChar = /[<>]/g,
799 match;
8004 while((match = reSpecChar.exec(raw))){
8014 var fixedPos = parser.fixPos(event, match.index);
8024 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
803 }
804 });
805 }
806});
807/**
808 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
809 * MIT Licensed
810 */
8111HTMLHint.addRule({
812 id: 'src-not-empty',
813 description: 'Src of img(script,link) must set value.',
814 init: function(parser, reporter){
8155 var self = this;
8165 parser.addListener('tagstart', function(event){
81732 var tagName = event.tagName,
818 attrs = event.attrs,
819 attr,
820 col = event.col + tagName.length + 1;
82132 for(var i=0, l=attrs.length;i<l;i++){
82236 attr = attrs[i];
82336 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
824 (tagName === 'link' && attr.name === 'href') ||
825 (tagName === 'object' && attr.name === 'data')) &&
826 attr.value === ''){
82715 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
828 }
829 }
830 });
831 }
832});
833/**
834 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
835 * MIT Licensed
836 */
8371HTMLHint.addRule({
838 id: 'style-disabled',
839 description: 'Style tag can not be use.',
840 init: function(parser, reporter){
8412 var self = this;
8422 parser.addListener('tagstart', function(event){
8434 if(event.tagName.toLowerCase() === 'style'){
8441 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
845 }
846 });
847 }
848});
849/**
850 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
851 * MIT Licensed
852 */
8531HTMLHint.addRule({
854 id: 'tag-pair',
855 description: 'Tag must be paired.',
856 init: function(parser, reporter){
8575 var self = this;
8585 var stack=[],
859 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8605 parser.addListener('tagstart', function(event){
8618 var tagName = event.tagName.toLowerCase();
8628 if (mapEmptyTags[tagName] === undefined && !event.close){
8637 stack.push(tagName);
864 }
865 });
8665 parser.addListener('tagend', function(event){
8674 var tagName = event.tagName.toLowerCase();
868 //向上寻找匹配的开始标签
8694 for(var pos = stack.length-1;pos >= 0; pos--){
8704 if(stack[pos] === tagName){
8713 break;
872 }
873 }
8744 if(pos >= 0){
8753 var arrTags = [];
8763 for(var i=stack.length-1;i>pos;i--){
8771 arrTags.push('</'+stack[i]+'>');
878 }
8793 if(arrTags.length > 0){
8801 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
881 }
8823 stack.length=pos;
883 }
884 else{
8851 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
886 }
887 });
8885 parser.addListener('end', function(event){
8895 var arrTags = [];
8905 for(var i=stack.length-1;i>=0;i--){
8913 arrTags.push('</'+stack[i]+'>');
892 }
8935 if(arrTags.length > 0){
8943 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
895 }
896 });
897 }
898});
899/**
900 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
901 * MIT Licensed
902 */
9031HTMLHint.addRule({
904 id: 'tag-self-close',
905 description: 'The empty tag must closed by self.',
906 init: function(parser, reporter){
9072 var self = this;
9082 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9092 parser.addListener('tagstart', function(event){
9104 var tagName = event.tagName.toLowerCase();
9114 if(mapEmptyTags[tagName] !== undefined){
9124 if(!event.close){
9132 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
914 }
915 }
916 });
917 }
918});
919/**
920 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
921 * MIT Licensed
922 */
9231HTMLHint.addRule({
924 id: 'tagname-lowercase',
925 description: 'Tagname must be lowercase.',
926 init: function(parser, reporter){
9274 var self = this;
9284 parser.addListener('tagstart,tagend', function(event){
92913 var tagName = event.tagName;
93013 if(tagName !== tagName.toLowerCase()){
9315 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
932 }
933 });
934 }
935});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 23ec4c12a..a2b303eaa 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -5,4 +5,4 @@ * (c) 2013 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(e){var t={};return t.version="0.9.6",t.rules={},t.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0,"id-unique":!0,"src-not-empty":!0,"attr-no-duplication":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,n){n===e&&(n=t.defaultRuleset);var i,r=new HTMLParser,s=new t.Reporter(a.split(/\r?\n/),n),o=t.rules;for(var l in n)i=o[l],i!==e&&n[l]!==!1&&i.init(r,s,n[l]);return r.parse(a),s.messages},t}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,n,i){this.report("error",e,t,a,n,i)},warn:function(e,t,a,n,i){this.report("warning",e,t,a,n,i)},info:function(e,t,a,n,i){this.report("info",e,t,a,n,i)},report:function(e,t,a,n,i,r){var s=this;s.messages.push({type:e,message:t,raw:r,evidence:s.lines[a-1],line:a,col:n,rule:{id:i.id,description:i.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+i.id}})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,i){var r=n-w+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=L,i.col=r,b.push(i),u.fire(t,i);for(var s;s=m.exec(a);)L++,w=n+m.lastIndex}var n,i,r,s,o,l,d,c,u=this,f=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,p=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,m=/\r?\n/g,v=0,h=0,w=0,L=1,b=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=g.lastIndex,!(r=n[1])||(o&&r===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(n[0]);else if(r=n[4]){s=[];for(var H,y=n[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",R=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:R,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),d=[],h=v)):a("text",n[0],i)}else(n[2]||n[3])&&a("comment",n[0],i,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],i,{tagName:r});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:L,col:v-w+1})},addListener:function(t,a){for(var n,i=this._listeners,r=t.split(/[,\s]/),s=0,o=r.length;o>s;s++)n=r[s],i[n]===e&&(i[n]=[]),i[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,i=[],r=n._listeners[t],s=n._listeners.all;r!==e&&(i=i.concat(r)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var i=0,r=n.length;r>i;i++)if(n[i]===a){n.splice(i,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),i=n.split(/\r?\n/),r=i.length-1,s=e.line;return r>0?(s+=r,a=i[r].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},n=0,i=e.length;i>n;n++)t=e[n],a[t.name]=t.value;return a}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){n=i[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Attribute name can not been duplication.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o={},l=0,d=r.length;d>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("The name of attribute [ "+n.name+" ] been duplication.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],(""!==n.value&&'"'!==n.quote||""===n.value&&"'"===n.quote)&&t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var r=e.line-1,s=e.col-1;try{var o=i(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"]("["+e.rule.id+"] "+e.message,r+a,(1===a?s:0)+e.col,n,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=function(i){"start"===i.type||"text"===i.type&&/^\s*$/.test(i.raw)||(("comment"!==i.type&&i.long===!1||/^DOCTYPE\s+/i.test(i.content)===!1)&&t.error("Doctype must be first.",i.line,i.col,a,i.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,i,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var i=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,i,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,i=this,r={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(n="string"==typeof a?r[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,r=e.col+e.tagName.length+1,l=0,d=n.length;d>l;l++)if(a=n[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,r+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),f=0,g=u.length;g>f;f++)c=u[f],c&&s.test(c)===!1&&t.warn(o,e.line,r+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,n={};e.addListener("tagstart",function(e){for(var i,r,s=e.attrs,o=e.col+e.tagName.length+1,l=0,d=s.length;d>l;l++)if(i=s[l],"id"===i.name.toLowerCase()){r=i.value,r&&(void 0===n[r]?n[r]=1:n[r]++,n[r]>1&&t.error("Id redefinition of [ "+r+" ].",e.line,o+i.index,a,i.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var n=e.attrs,i=!1,r=0,s=n.length;s>r;r++)if("alt"===n[r].name.toLowerCase()){i=!0;break}i===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var r=e.getMapAttrs(i.attrs),s=r.type;if(void 0!==r.src||s&&/^(text\/javascript)$/i.test(s)===!1)return;var o;if(o="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==a){var l=i.line-1,d=i.col-1,c=i.raw.replace(/\t/g," ");try{var u=o(c,a);u===!1&&o.errors.forEach(function(e){var a=e.line;t.warn(e.reason,l+a,(1===a?d:0)+e.character,n,e.evidence)})}catch(f){}}}})}}),HTMLHint.addRule({id:"space-tab-mixed-disabled",description:"Spaces and tabs can not mixed in front of line.",init:function(e,t){var a=this;e.addListener("text",function(e){0===e.pos&&/^( +\t|\t+ )/.test(e.raw)===!0&&t.warn("Mixed spaces and tabs in front of line.",e.line,0,a,e.raw)})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var i,r=n.raw,s=/[<>]/g;i=s.exec(r);){var o=e.fixPos(n,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.tagName,r=e.attrs,s=e.col+i.length+1,o=0,l=r.length;l>o;o++)n=r[o],(/^(img|script|embed|bgsound|iframe)$/.test(i)===!0&&"src"===n.name||"link"===i&&"href"===n.name||"object"===i&&"data"===n.name)&&""===n.value&&t.error("[ "+n.name+"] of [ "+i+" ] must set value.",e.line,s+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,n=[],i=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var t=e.tagName.toLowerCase();void 0!==i[t]||e.close||n.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),r=n.length-1;r>=0&&n[r]!==i;r--);if(r>=0){for(var s=[],o=n.length-1;o>r;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),n.length=r}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var i=[],r=n.length-1;r>=0;r--)i.push("");i.length>0&&t.error("Tag must be paired, Missing: [ "+i.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,n=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var i=e.tagName.toLowerCase();void 0!==n[i]&&(e.close||t.warn("The empty tag : [ "+i+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var n=e.tagName;n!==n.toLowerCase()&&t.error("Tagname [ "+n+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file +var HTMLHint=function(e){var t={};return t.version="0.9.6",t.rules={},t.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0,"id-unique":!0,"src-not-empty":!0,"attr-no-duplication":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,n){var i,r;if(a=a.replace(/^\s*\s*/i,function(e,t){return r={},t.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g,function(e,t,a){"false"===a?a=!1:"true"===a&&(a=!0),r[t]=a}),""}),n!==e){r=r||{};for(i in n)r[i]=n[i]}r===e&&(r=t.defaultRuleset);var s,o=new HTMLParser,l=new t.Reporter(a.split(/\r?\n/),r),d=t.rules;for(i in r)s=d[i],s!==e&&r[i]!==!1&&s.init(o,l,r[i]);return o.parse(a),l.messages},t}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,n,i){this.report("error",e,t,a,n,i)},warn:function(e,t,a,n,i){this.report("warning",e,t,a,n,i)},info:function(e,t,a,n,i){this.report("info",e,t,a,n,i)},report:function(e,t,a,n,i,r){var s=this;s.messages.push({type:e,message:t,raw:r,evidence:s.lines[a-1],line:a,col:n,rule:{id:i.id,description:i.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+i.id}})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,i){var r=n-w+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=L,i.col=r,b.push(i),u.fire(t,i);for(var s;s=m.exec(a);)L++,w=n+m.lastIndex}var n,i,r,s,o,l,d,c,u=this,f=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,p=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,m=/\r?\n/g,v=0,h=0,w=0,L=1,b=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=g.lastIndex,!(r=n[1])||(o&&r===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(n[0]);else if(r=n[4]){s=[];for(var H,y=n[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",R=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:R,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),d=[],h=v)):a("text",n[0],i)}else(n[2]||n[3])&&a("comment",n[0],i,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],i,{tagName:r});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:L,col:v-w+1})},addListener:function(t,a){for(var n,i=this._listeners,r=t.split(/[,\s]/),s=0,o=r.length;o>s;s++)n=r[s],i[n]===e&&(i[n]=[]),i[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,i=[],r=n._listeners[t],s=n._listeners.all;r!==e&&(i=i.concat(r)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var i=0,r=n.length;r>i;i++)if(n[i]===a){n.splice(i,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),i=n.split(/\r?\n/),r=i.length-1,s=e.line;return r>0?(s+=r,a=i[r].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},n=0,i=e.length;i>n;n++)t=e[n],a[t.name]=t.value;return a}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){n=i[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Attribute name can not been duplication.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o={},l=0,d=r.length;d>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("The name of attribute [ "+n.name+" ] been duplication.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],(""!==n.value&&'"'!==n.quote||""===n.value&&"'"===n.quote)&&t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var r=e.line-1,s=e.col-1;try{var o=i(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"]("["+e.rule.id+"] "+e.message,r+a,(1===a?s:0)+e.col,n,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=function(i){"start"===i.type||"text"===i.type&&/^\s*$/.test(i.raw)||(("comment"!==i.type&&i.long===!1||/^DOCTYPE\s+/i.test(i.content)===!1)&&t.error("Doctype must be first.",i.line,i.col,a,i.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,i,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var i=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,i,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"id-class-ad-disabled",description:"Id and class can not use ad keyword, it will blocked by adblock software.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o=0,l=r.length;l>o;o++)n=r[o],i=n.name,/^(id|class)$/i.test(i)&&/(^|[-\_])ad([-\_]|$)/i.test(n.value)&&t.warn("The value of "+i+" can not use ad keyword.",e.line,s+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,i=this,r={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(n="string"==typeof a?r[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,r=e.col+e.tagName.length+1,l=0,d=n.length;d>l;l++)if(a=n[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,r+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),f=0,g=u.length;g>f;f++)c=u[f],c&&s.test(c)===!1&&t.warn(o,e.line,r+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,n={};e.addListener("tagstart",function(e){for(var i,r,s=e.attrs,o=e.col+e.tagName.length+1,l=0,d=s.length;d>l;l++)if(i=s[l],"id"===i.name.toLowerCase()){r=i.value,r&&(void 0===n[r]?n[r]=1:n[r]++,n[r]>1&&t.error("Id redefinition of [ "+r+" ].",e.line,o+i.index,a,i.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var n=e.attrs,i=!1,r=0,s=n.length;s>r;r++)if("alt"===n[r].name.toLowerCase()){i=!0;break}i===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var r=e.getMapAttrs(i.attrs),s=r.type;if(void 0!==r.src||s&&/^(text\/javascript)$/i.test(s)===!1)return;var o;if(o="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==a){var l=i.line-1,d=i.col-1,c=i.raw.replace(/\t/g," ");try{var u=o(c,a);u===!1&&o.errors.forEach(function(e){var a=e.line;t.warn(e.reason,l+a,(1===a?d:0)+e.character,n,e.evidence)})}catch(f){}}}})}}),HTMLHint.addRule({id:"space-tab-mixed-disabled",description:"Spaces and tabs can not mixed in front of line.",init:function(e,t){var a=this;e.addListener("text",function(e){0===e.pos&&/^( +\t|\t+ )/.test(e.raw)===!0&&t.warn("Mixed spaces and tabs in front of line.",e.line,0,a,e.raw)})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var i,r=n.raw,s=/[<>]/g;i=s.exec(r);){var o=e.fixPos(n,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.tagName,r=e.attrs,s=e.col+i.length+1,o=0,l=r.length;l>o;o++)n=r[o],(/^(img|script|embed|bgsound|iframe)$/.test(i)===!0&&"src"===n.name||"link"===i&&"href"===n.name||"object"===i&&"data"===n.name)&&""===n.value&&t.error("[ "+n.name+"] of [ "+i+" ] must set value.",e.line,s+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,n=[],i=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var t=e.tagName.toLowerCase();void 0!==i[t]||e.close||n.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),r=n.length-1;r>=0&&n[r]!==i;r--);if(r>=0){for(var s=[],o=n.length-1;o>r;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),n.length=r}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var i=[],r=n.length-1;r>=0;r--)i.push("");i.length>0&&t.error("Tag must be paired, Missing: [ "+i.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,n=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var i=e.tagName.toLowerCase();void 0!==n[i]&&(e.close||t.warn("The empty tag : [ "+i+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var n=e.tagName;n!==n.toLowerCase()&&t.error("Tagname [ "+n+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file diff --git a/src/core.js b/src/core.js index 897ec32da..22f62fb6b 100644 --- a/src/core.js +++ b/src/core.js @@ -20,23 +20,46 @@ var HTMLHint = (function (undefined) { 'spec-char-escape': true, 'id-unique': true, 'src-not-empty': true, - 'attr-no-duplication': true // added: 2014-6-14 + 'attr-no-duplication': true }; HTMLHint.addRule = function(rule){ HTMLHint.rules[rule.id] = rule; }; - HTMLHint.verify = function(html, ruleset){ + HTMLHint.verify = function(html, newRuleset){ + var id; + var ruleset; + // parse inline ruleset + html = html.replace(/^\s*\s*/i, function(all, strRuleset){ + ruleset = {}; + strRuleset.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g, function(all, key, value){ + if(value === 'false'){ + value = false; + } + else if(value === 'true'){ + value = true; + } + ruleset[key] = value; + }); + return ''; + }); + if(newRuleset !== undefined){ + ruleset = ruleset || {}; + for (id in newRuleset){ + ruleset[id] = newRuleset[id]; + } + } if(ruleset === undefined){ ruleset = HTMLHint.defaultRuleset; } - var parser = new HTMLParser(), - reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset); + + var parser = new HTMLParser(); + var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset); var rules = HTMLHint.rules, rule; - for (var id in ruleset){ + for (id in ruleset){ rule = rules[id]; if (rule !== undefined && ruleset[id] !== false){ rule.init(parser, reporter, ruleset[id]); diff --git a/src/rules/id-class-ad-disabled.js b/src/rules/id-class-ad-disabled.js new file mode 100644 index 000000000..91a46a900 --- /dev/null +++ b/src/rules/id-class-ad-disabled.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014, Yanis Wang + * MIT Licensed + */ +HTMLHint.addRule({ + id: 'id-class-ad-disabled', + description: 'Id and class can not use ad keyword, it will blocked by adblock software.', + init: function(parser, reporter){ + var self = this; + parser.addListener('tagstart', function(event){ + var attrs = event.attrs; + var attr; + var attrName; + var col = event.col + event.tagName.length + 1; + + for(var i=0, l=attrs.length;i + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../index").HTMLHint; + +describe('Core', function(){ + + it('Default ruleset not worked should result in an error', function(){ + var code = '

>

'; + var messages = HTMLHint.verify(code); + expect(messages.length).to.be(9); + }); + + it('Inline ruleset not worked should result in an error', function(){ + var code = '\r\n'; + var messages = HTMLHint.verify(code); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be('img-alt-require'); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(1); + + // overwrite inline ruleset + messages = HTMLHint.verify(code, { + 'img-alt-require': false + }); + expect(messages.length).to.be(0); + }); + +}); \ No newline at end of file diff --git a/test/rules/id-class-ad-disabled.js b/test/rules/id-class-ad-disabled.js new file mode 100644 index 000000000..f8e2b5b24 --- /dev/null +++ b/test/rules/id-class-ad-disabled.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2014, Yanis Wang + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../../index").HTMLHint; + +var ruldId = 'id-class-ad-disabled', + ruleOptions = {}; + +ruleOptions[ruldId] = true; + +describe('Rules: '+ruldId, function(){ + + it('Id use ad keyword should result in an error', function(){ + var code = ''; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].type).to.be('warning'); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = ''; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + }); + + it('Class use ad keyword should result in an error', function(){ + var code = '
test
'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = ''; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be(ruldId); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + }); + + it('Id and class no ad keyword used should not result in an error', function(){ + var code = '
test
'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + + code = '
test
'; + messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + }); + +}); \ No newline at end of file