diff --git a/.jshintrc b/.jshintrc index 95e34101e..52a05cb90 100644 --- a/.jshintrc +++ b/.jshintrc @@ -14,6 +14,7 @@ "browser": true, "wsh": true, + "-W099": true, "predef": [ "HTMLHint", diff --git a/CHANGE.md b/CHANGE.md index e31cc1a1e..a12e4a959 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -1,6 +1,12 @@ HTMLHint change log ==================== +## ver 0.9.6 (2014-6-14) + +1. add rule: attr-no-duplication +2. add rule: space-tab-mixed-disabled +2. add default rule: attr-no-duplication + ## ver 0.9.4 (2013-9-27) 1. add rule: src-not-empty diff --git a/Gruntfile.js b/Gruntfile.js index 247174bca..a418507c9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -69,8 +69,7 @@ module.exports = function(grunt) { replace: { htmlhint: { files: { - 'lib/htmlhint.js':'lib/htmlhint.js', - 'bin/htmlhint':'src/cli.js' + 'lib/htmlhint.js':'lib/htmlhint.js' }, options: { prefix: '@', diff --git a/LICENSE.md b/LICENSE.md index 2e1fd2890..0c4a92f02 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ The MIT License ================ -Copyright (c) 2013 Yanis Wang \ +Copyright (c) 2014 Yanis Wang \ 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 8d3ef543c..d92ffb360 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,9 @@ TODO ================== -1. reporter support -2. w3c rule \ No newline at end of file +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 81610b8cf..a2612513a 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%
347
342
5

htmlhint.js

98%
347
342
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 };
24
251 HTMLHint.addRule = function(rule){
2617 HTMLHint.rules[rule.id] = rule;
27 };
28
291 HTMLHint.verify = function(html, ruleset){
3047 if(ruleset === undefined){
311 ruleset = HTMLHint.defaultRuleset;
32 }
3347 var parser = new HTMLParser(),
34 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
35
3647 var rules = HTMLHint.rules,
37 rule;
3847 for (var id in ruleset){
3954 rule = rules[id];
4054 if (rule !== undefined){
4154 rule.init(parser, reporter, ruleset[id]);
42 }
43 }
44
4547 parser.parse(html);
46
4747 return reporter.messages;
48 };
49
501 return HTMLHint;
51
52})();
53
541if (typeof exports === 'object' && exports){
551 exports.HTMLHint = HTMLHint;
56}
57/**
58 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
59 * MIT Licensed
60 */
611(function(HTMLHint, undefined){
62
631 var Reporter = function(){
6447 var self = this;
6547 self._init.apply(self,arguments);
66 };
67
681 Reporter.prototype = {
69 _init: function(lines, ruleset){
7047 var self = this;
7147 self.lines = lines;
7247 self.ruleset = ruleset;
7347 self.messages = [];
74 },
75 //错误
76 error: function(message, line, col, rule, raw){
7733 this.report('error', message, line, col, rule, raw);
78 },
79 //警告
80 warn: function(message, line, col, rule, raw){
8126 this.report('warning', message, line, col, rule, raw);
82 },
83 //信息
84 info: function(message, line, col, rule, raw){
850 this.report('info', message, line, col, rule, raw);
86 },
87 //报告
88 report: function(type, message, line, col, rule, raw){
8959 var self = this;
9059 self.messages.push({
91 type: type,
92 message: message,
93 raw: raw,
94 evidence: self.lines[line-1],
95 line: line,
96 col: col,
97 rule: {
98 id: rule.id,
99 description: rule.description,
100 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
101 }
102 });
103 }
104 };
105
1061 HTMLHint.Reporter = Reporter;
107
108})(HTMLHint);
109/**
110 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
111 * MIT Licensed
112 */
1131var HTMLParser = (function(undefined){
114
1151 var HTMLParser = function(){
11672 var self = this;
11772 self._init.apply(self,arguments);
118 };
119
1201 HTMLParser.prototype = {
121 _init: function(){
12272 var self = this;
12372 self._listeners = {};
12472 self._mapCdataTags = self.makeMap("script,style");
12572 self._arrBlocks = [];
126 },
127
128 makeMap: function(str){
12978 var obj = {}, items = str.split(",");
13078 for ( var i = 0; i < items.length; i++ ){
131228 obj[ items[i] ] = true;
132 }
13378 return obj;
134 },
135
136 // parse html code
137 parse: function(html){
138
13972 var self = this,
140 mapCdataTags = self._mapCdataTags;
141
14272 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
143 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
144 regLine = /\r?\n/g;
145
14672 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14772 var lastLineIndex = 0, line = 1;
14872 var arrBlocks = self._arrBlocks;
149
15072 self.fire('start', {
151 pos: 0,
152 line: 1,
153 col: 1
154 });
155
15672 while((match = regTag.exec(html))){
157161 matchIndex = match.index;
158161 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15926 text = html.substring(lastIndex, matchIndex);
16026 if(tagCDATA){
16110 arrCDATA.push(text);
162 }
163 else{//文本
16416 saveBlock('text', text, lastIndex);
165 }
166 }
167161 lastIndex = regTag.lastIndex;
168
169161 if((tagName = match[1])){
17047 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
17115 text = arrCDATA.join('');
17215 saveBlock('cdata', text, lastCDATAIndex, {
173 'tagName': tagCDATA,
174 'attrs': attrsCDATA
175 });
17615 tagCDATA = null;
17715 attrsCDATA = null;
17815 arrCDATA = null;
179 }
18047 if(!tagCDATA){
181 //标签结束
18246 saveBlock('tagend', match[0], matchIndex, {
183 'tagName': tagName
184 });
18546 continue;
186 }
187 }
188
189115 if(tagCDATA){
1901 arrCDATA.push(match[0]);
191 }
192 else{
193114 if((tagName = match[4])){//标签开始
194101 arrAttrs = [];
195101 var attrs = match[5],
196 attrMatch,
197 attrMatchCount = 0;
198101 while((attrMatch = regAttr.exec(attrs))){
19989 var name = attrMatch[1],
200 quote = attrMatch[2] ? attrMatch[2] :
201 attrMatch[4] ? attrMatch[4] : '',
202 value = attrMatch[3] ? attrMatch[3] :
203 attrMatch[5] ? attrMatch[5] :
204 attrMatch[6] ? attrMatch[6] : '';
20589 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20689 attrMatchCount += attrMatch[0].length;
207 }
208101 if(attrMatchCount === attrs.length){
209101 saveBlock('tagstart', match[0], matchIndex, {
210 'tagName': tagName,
211 'attrs': arrAttrs,
212 'close': match[6]
213 });
214101 if(mapCdataTags[tagName]){
21515 tagCDATA = tagName;
21615 attrsCDATA = arrAttrs.concat();
21715 arrCDATA = [];
21815 lastCDATAIndex = lastIndex;
219 }
220 }
221 else{//如果出现漏匹配,则把当前内容匹配为text
2220 saveBlock('text', match[0], matchIndex);
223 }
224 }
22513 else if(match[2] || match[3]){//注释标签
22613 saveBlock('comment', match[0], matchIndex, {
227 'content': match[2] || match[3],
228 'long': match[2]?true:false
229 });
230 }
231 }
232 }
233
23472 if(html.length > lastIndex){
235 //结尾文本
23613 text = html.substring(lastIndex, html.length);
23713 saveBlock('text', text, lastIndex);
238 }
239
24072 self.fire('end', {
241 pos: lastIndex,
242 line: line,
243 col: lastIndex - lastLineIndex + 1
244 });
245
246 //存储区块
24772 function saveBlock(type, raw, pos, data){
248204 var col = pos - lastLineIndex + 1;
249204 if(data === undefined){
25029 data = {};
251 }
252204 data.raw = raw;
253204 data.pos = pos;
254204 data.line = line;
255204 data.col = col;
256204 arrBlocks.push(data);
257204 self.fire(type, data);
258204 var lineMatch;
259204 while((lineMatch = regLine.exec(raw))){
26018 line ++;
26118 lastLineIndex = pos + regLine.lastIndex;
262 }
263 }
264
265 },
266
267 // add event
268 addListener: function(types, listener){
26992 var _listeners = this._listeners;
27092 var arrTypes = types.split(/[,\s]/), type;
27192 for(var i=0, l = arrTypes.length;i<l;i++){
27295 type = arrTypes[i];
27395 if (_listeners[type] === undefined){
27489 _listeners[type] = [];
275 }
27695 _listeners[type].push(listener);
277 }
278 },
279
280 // fire event
281 fire: function(type, data){
282348 if (data === undefined){
2830 data = {};
284 }
285348 data.type = type;
286348 var self = this,
287 listeners = [],
288 listenersType = self._listeners[type],
289 listenersAll = self._listeners['all'];
290348 if (listenersType !== undefined){
291102 listeners = listeners.concat(listenersType);
292 }
293348 if (listenersAll !== undefined){
294123 listeners = listeners.concat(listenersAll);
295 }
296348 for (var i = 0, l = listeners.length; i < l; i++){
297223 listeners[i].call(self, data);
298 }
299 },
300
301 // remove event
302 removeListener: function(type, listener){
30313 var listenersType = this._listeners[type];
30413 if(listenersType !== undefined){
30511 for (var i = 0, l = listenersType.length; i < l; i++){
3068 if (listenersType[i] === listener){
3078 listenersType.splice(i, 1);
3088 break;
309 }
310 }
311 }
312 },
313
314 //fix pos if event.raw have \n
315 fixPos: function(event, index){
3163 var text = event.raw.substr(0, index);
3173 var arrLines = text.split(/\r?\n/),
318 lineCount = arrLines.length - 1,
319 line = event.line, col;
3203 if(lineCount > 0){
3211 line += lineCount;
3221 col = arrLines[lineCount].length + 1;
323 }
324 else{
3252 col = event.col + index;
326 }
3273 return {
328 line: line,
329 col: col
330 };
331 },
332
333 // covert array type of attrs to map
334 getMapAttrs: function(arrAttrs){
3356 var mapAttrs = {},
336 attr;
3376 for(var i=0,l=arrAttrs.length;i<l;i++){
3386 attr = arrAttrs[i];
3396 mapAttrs[attr.name] = attr.value;
340 }
3416 return mapAttrs;
342 }
343 };
344
3451 return HTMLParser;
346
347})();
348
3491if (typeof exports === 'object' && exports){
3501 exports.HTMLParser = HTMLParser;
351}
352/**
353 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
354 * MIT Licensed
355 */
3561HTMLHint.addRule({
357 id: 'attr-lowercase',
358 description: 'Attribute name must be lowercase.',
359 init: function(parser, reporter){
3603 var self = this;
3613 parser.addListener('tagstart', function(event){
3623 var attrs = event.attrs,
363 attr,
364 col = event.col + event.tagName.length + 1;
3653 for(var i=0, l=attrs.length;i<l;i++){
3663 attr = attrs[i];
3673 var attrName = attr.name;
3683 if(attrName !== attrName.toLowerCase()){
3692 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
370 }
371 }
372 });
373 }
374});
375/**
376 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
377 * MIT Licensed
378 */
3791HTMLHint.addRule({
380 id: 'attr-value-double-quotes',
381 description: 'Attribute value must closed by double quotes.',
382 init: function(parser, reporter){
3833 var self = this;
3843 parser.addListener('tagstart', function(event){
3853 var attrs = event.attrs,
386 attr,
387 col = event.col + event.tagName.length + 1;
3883 for(var i=0, l=attrs.length;i<l;i++){
3897 attr = attrs[i];
3907 if((attr.value !== '' && attr.quote !== '"') ||
391 (attr.value === '' && attr.quote === "'")){
3923 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
393 }
394 }
395 });
396 }
397});
398/**
399 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
400 * MIT Licensed
401 */
4021HTMLHint.addRule({
403 id: 'attr-value-not-empty',
404 description: 'Attribute must set value.',
405 init: function(parser, reporter){
4063 var self = this;
4073 parser.addListener('tagstart', function(event){
4083 var attrs = event.attrs,
409 attr,
410 col = event.col + event.tagName.length + 1;
4113 for(var i=0, l=attrs.length;i<l;i++){
4123 attr = attrs[i];
4133 if(attr.quote === '' && attr.value === ''){
4141 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
415 }
416 }
417 });
418 }
419});
420/**
421 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
422 * MIT Licensed
423 */
4241HTMLHint.addRule({
425 id: 'csslint',
426 description: 'Scan css with csslint.',
427 init: function(parser, reporter, options){
4281 var self = this;
4291 parser.addListener('cdata', function(event){
4301 if(event.tagName.toLowerCase() === 'style'){
431
4321 var cssVerify;
433
4341 if(typeof exports === 'object' && require){
4351 cssVerify = require("csslint").CSSLint.verify;
436 }
437 else{
4380 cssVerify = CSSLint.verify;
439 }
440
4411 if(options !== undefined){
4421 var styleLine = event.line - 1,
443 styleCol = event.col - 1;
4441 try{
4451 var messages = cssVerify(event.raw, options).messages;
4461 messages.forEach(function(error){
4472 var line = error.line;
4482 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
449 });
450 }
451 catch(e){}
452 }
453
454 }
455 });
456 }
457});
458/**
459 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
460 * MIT Licensed
461 */
4621HTMLHint.addRule({
463 id: 'doctype-first',
464 description: 'Doctype must be first.',
465 init: function(parser, reporter){
4663 var self = this;
4673 var allEvent = function(event){
4686 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4693 return;
470 }
4713 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4722 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
473 }
4743 parser.removeListener('all', allEvent);
475 };
4763 parser.addListener('all', allEvent);
477 }
478});
479/**
480 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
481 * MIT Licensed
482 */
4831HTMLHint.addRule({
484 id: 'doctype-html5',
485 description: 'Doctype must be html5.',
486 init: function(parser, reporter){
4872 var self = this;
4882 function onComment(event){
4899 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4901 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
491 }
492 }
4932 function onTagStart(){
4942 parser.removeListener('comment', onComment);
4952 parser.removeListener('tagstart', onTagStart);
496 }
4972 parser.addListener('all', onComment);
4982 parser.addListener('tagstart', onTagStart);
499 }
500});
501/**
502 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
503 * MIT Licensed
504 */
5051HTMLHint.addRule({
506 id: 'head-script-disabled',
507 description: 'The script tag can not be used in head.',
508 init: function(parser, reporter){
5093 var self = this;
5103 function onTagStart(event){
5115 if(event.tagName.toLowerCase() === 'script'){
5122 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
513 }
514 }
5153 function onTagEnd(event){
5167 if(event.tagName.toLowerCase() === 'head'){
5173 parser.removeListener('tagstart', onTagStart);
5183 parser.removeListener('tagstart', onTagEnd);
519 }
520 }
5213 parser.addListener('tagstart', onTagStart);
5223 parser.addListener('tagend', onTagEnd);
523 }
524});
525/**
526 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
527 * MIT Licensed
528 */
5291HTMLHint.addRule({
530 id: 'id-class-value',
531 description: 'Id and class value must meet some rules.',
532 init: function(parser, reporter, options){
5338 var self = this;
5348 var arrRules = {
535 'underline': {
536 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
537 'message': 'Id and class value must lower case and split by underline.'
538 },
539 'dash': {
540 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
541 'message': 'Id and class value must lower case and split by dash.'
542 },
543 'hump': {
544 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
545 'message': 'Id and class value must meet hump style.'
546 }
547 }, rule;
5488 if(typeof options === 'string'){
5496 rule = arrRules[options];
550 }
551 else{
5522 rule = options;
553 }
5548 if(rule && rule.regId){
5558 var regId = rule.regId,
556 message = rule.message;
5578 parser.addListener('tagstart', function(event){
5588 var attrs = event.attrs,
559 attr,
560 col = event.col + event.tagName.length + 1;
5618 for(var i=0, l1=attrs.length;i<l1;i++){
56216 attr = attrs[i];
56316 if(attr.name.toLowerCase() === 'id'){
5648 if(regId.test(attr.value) === false){
5654 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
566 }
567 }
56816 if(attr.name.toLowerCase() === 'class'){
5698 var arrClass = attr.value.split(/\s+/g), classValue;
5708 for(var j=0, l2=arrClass.length;j<l2;j++){
5718 classValue = arrClass[j];
5728 if(classValue && regId.test(classValue) === false){
5734 reporter.warn(message, event.line, col + attr.index, self, classValue);
574 }
575 }
576 }
577 }
578 });
579 }
580 }
581});
582/**
583 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
584 * MIT Licensed
585 */
5861HTMLHint.addRule({
587 id: 'id-unique',
588 description: 'Id must be unique.',
589 init: function(parser, reporter){
5903 var self = this;
5913 var mapIdCount = {};
5923 parser.addListener('tagstart', function(event){
5935 var attrs = event.attrs,
594 attr,
595 id,
596 col = event.col + event.tagName.length + 1;
5975 for(var i=0, l=attrs.length;i<l;i++){
5985 attr = attrs[i];
5995 if(attr.name.toLowerCase() === 'id'){
6004 id = attr.value;
6014 if(id){
6024 if(mapIdCount[id] === undefined){
6033 mapIdCount[id] = 1;
604 }
605 else{
6061 mapIdCount[id] ++;
607 }
6084 if(mapIdCount[id] > 1){
6091 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
610 }
611 }
6124 break;
613 }
614 }
615 });
616 }
617});
618/**
619 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
620 * MIT Licensed
621 */
6221HTMLHint.addRule({
623 id: 'img-alt-require',
624 description: 'Alt of img tag must be set value.',
625 init: function(parser, reporter){
6263 var self = this;
6273 parser.addListener('tagstart', function(event){
6283 if(event.tagName.toLowerCase() === 'img'){
6293 var attrs = event.attrs;
6303 var haveAlt = false;
6313 for(var i=0, l=attrs.length;i<l;i++){
6328 if(attrs[i].name.toLowerCase() === 'alt'){
6332 haveAlt = true;
6342 break;
635 }
636 }
6373 if(haveAlt === false){
6381 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
639 }
640 }
641 });
642 }
643});
644/**
645 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
646 * MIT Licensed
647 */
6481HTMLHint.addRule({
649 id: 'jshint',
650 description: 'Scan script with jshint.',
651 init: function(parser, reporter, options){
6524 var self = this;
6534 parser.addListener('cdata', function(event){
6544 if(event.tagName.toLowerCase() === 'script'){
655
6564 var mapAttrs = parser.getMapAttrs(event.attrs),
657 type = mapAttrs.type;
658
659 // Only scan internal javascript
6604 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6612 return;
662 }
663
6642 var jsVerify;
665
6662 if(typeof exports === 'object' && require){
6672 jsVerify = require('jshint').JSHINT;
668 }
669 else{
6700 jsVerify = JSHINT;
671 }
672
6732 if(options !== undefined){
6742 var styleLine = event.line - 1,
675 styleCol = event.col - 1;
6762 var code = event.raw.replace(/\t/g,' ');
6772 try{
6782 var status = jsVerify(code, options);
6792 if(status === false){
6802 jsVerify.errors.forEach(function(error){
6818 var line = error.line;
6828 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
683 });
684 }
685 }
686 catch(e){}
687 }
688
689 }
690 });
691 }
692});
693/**
694 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
695 * MIT Licensed
696 */
6971HTMLHint.addRule({
698 id: 'spec-char-escape',
699 description: 'Special characters must be escaped.',
700 init: function(parser, reporter){
7013 var self = this;
7023 parser.addListener('text', function(event){
7033 var raw = event.raw,
704 reSpecChar = /[<>]/g,
705 match;
7063 while((match = reSpecChar.exec(raw))){
7073 var fixedPos = parser.fixPos(event, match.index);
7083 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
709 }
710 });
711 }
712});
713/**
714 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
715 * MIT Licensed
716 */
7171HTMLHint.addRule({
718 id: 'src-not-empty',
719 description: 'Src of img(script,link) must set value.',
720 init: function(parser, reporter){
7214 var self = this;
7224 parser.addListener('tagstart', function(event){
72329 var tagName = event.tagName,
724 attrs = event.attrs,
725 attr,
726 col = event.col + tagName.length + 1;
72729 for(var i=0, l=attrs.length;i<l;i++){
72830 attr = attrs[i];
72930 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
730 (tagName === 'link' && attr.name === 'href') ||
731 (tagName === 'object' && attr.name === 'data')) &&
732 attr.value === ''){
73314 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
734 }
735 }
736 });
737 }
738});
739/**
740 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
741 * MIT Licensed
742 */
7431HTMLHint.addRule({
744 id: 'style-disabled',
745 description: 'Style tag can not be use.',
746 init: function(parser, reporter){
7472 var self = this;
7482 parser.addListener('tagstart', function(event){
7494 if(event.tagName.toLowerCase() === 'style'){
7501 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
751 }
752 });
753 }
754});
755/**
756 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
757 * MIT Licensed
758 */
7591HTMLHint.addRule({
760 id: 'tag-pair',
761 description: 'Tag must be paired.',
762 init: function(parser, reporter){
7634 var self = this;
7644 var stack=[],
765 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7664 parser.addListener('tagstart', function(event){
7675 var tagName = event.tagName.toLowerCase();
7685 if (mapEmptyTags[tagName] === undefined && !event.close){
7695 stack.push(tagName);
770 }
771 });
7724 parser.addListener('tagend', function(event){
7733 var tagName = event.tagName.toLowerCase();
774 //向上寻找匹配的开始标签
7753 for(var pos = stack.length-1;pos >= 0; pos--){
7763 if(stack[pos] === tagName){
7772 break;
778 }
779 }
7803 if(pos >= 0){
7812 var arrTags = [];
7822 for(var i=stack.length-1;i>pos;i--){
7831 arrTags.push('</'+stack[i]+'>');
784 }
7852 if(arrTags.length > 0){
7861 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
787 }
7882 stack.length=pos;
789 }
790 else{
7911 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
792 }
793 });
7944 parser.addListener('end', function(event){
7954 var arrTags = [];
7964 for(var i=stack.length-1;i>=0;i--){
7972 arrTags.push('</'+stack[i]+'>');
798 }
7994 if(arrTags.length > 0){
8002 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
801 }
802 });
803 }
804});
805/**
806 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
807 * MIT Licensed
808 */
8091HTMLHint.addRule({
810 id: 'tag-self-close',
811 description: 'The empty tag must closed by self.',
812 init: function(parser, reporter){
8132 var self = this;
8142 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8152 parser.addListener('tagstart', function(event){
8164 var tagName = event.tagName.toLowerCase();
8174 if(mapEmptyTags[tagName] !== undefined){
8184 if(!event.close){
8192 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
820 }
821 }
822 });
823 }
824});
825/**
826 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
827 * MIT Licensed
828 */
8291HTMLHint.addRule({
830 id: 'tagname-lowercase',
831 description: 'Tagname must be lowercase.',
832 init: function(parser, reporter){
8333 var self = this;
8343 parser.addListener('tagstart,tagend', function(event){
8359 var tagName = event.tagName;
8369 if(tagName !== tagName.toLowerCase()){
8374 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
838 }
839 });
840 }
841});
\ No newline at end of file +

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){
3153 if(ruleset === undefined){
321 ruleset = HTMLHint.defaultRuleset;
33 }
3453 var parser = new HTMLParser(),
35 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
36
3753 var rules = HTMLHint.rules,
38 rule;
3953 for (var id in ruleset){
4061 rule = rules[id];
4161 if (rule !== undefined){
4261 rule.init(parser, reporter, ruleset[id]);
43 }
44 }
45
4653 parser.parse(html);
47
4853 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(){
6553 var self = this;
6653 self._init.apply(self,arguments);
67 };
68
691 Reporter.prototype = {
70 _init: function(lines, ruleset){
7153 var self = this;
7253 self.lines = lines;
7353 self.ruleset = ruleset;
7453 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(){
11778 var self = this;
11878 self._init.apply(self,arguments);
119 };
120
1211 HTMLParser.prototype = {
122 _init: function(){
12378 var self = this;
12478 self._listeners = {};
12578 self._mapCdataTags = self.makeMap("script,style");
12678 self._arrBlocks = [];
127 },
128
129 makeMap: function(str){
13084 var obj = {}, items = str.split(",");
13184 for ( var i = 0; i < items.length; i++ ){
132240 obj[ items[i] ] = true;
133 }
13484 return obj;
135 },
136
137 // parse html code
138 parse: function(html){
139
14078 var self = this,
141 mapCdataTags = self._mapCdataTags;
142
14378 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
14778 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14878 var lastLineIndex = 0, line = 1;
14978 var arrBlocks = self._arrBlocks;
150
15178 self.fire('start', {
152 pos: 0,
153 line: 1,
154 col: 1
155 });
156
15778 while((match = regTag.exec(html))){
158173 matchIndex = match.index;
159173 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 }
168173 lastIndex = regTag.lastIndex;
169
170173 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
190121 if(tagCDATA){
1911 arrCDATA.push(match[0]);
192 }
193 else{
194120 if((tagName = match[4])){//标签开始
195107 arrAttrs = [];
196107 var attrs = match[5],
197 attrMatch,
198 attrMatchCount = 0;
199107 while((attrMatch = regAttr.exec(attrs))){
20096 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] : '';
20696 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20796 attrMatchCount += attrMatch[0].length;
208 }
209107 if(attrMatchCount === attrs.length){
210107 saveBlock('tagstart', match[0], matchIndex, {
211 'tagName': tagName,
212 'attrs': arrAttrs,
213 'close': match[6]
214 });
215107 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
23578 if(html.length > lastIndex){
236 //结尾文本
23713 text = html.substring(lastIndex, html.length);
23813 saveBlock('text', text, lastIndex);
239 }
240
24178 self.fire('end', {
242 pos: lastIndex,
243 line: line,
244 col: lastIndex - lastLineIndex + 1
245 });
246
247 //存储区块
24878 function saveBlock(type, raw, pos, data){
249226 var col = pos - lastLineIndex + 1;
250226 if(data === undefined){
25139 data = {};
252 }
253226 data.raw = raw;
254226 data.pos = pos;
255226 data.line = line;
256226 data.col = col;
257226 arrBlocks.push(data);
258226 self.fire(type, data);
259226 var lineMatch;
260226 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){
283382 if (data === undefined){
2840 data = {};
285 }
286382 data.type = type;
287382 var self = this,
288 listeners = [],
289 listenersType = self._listeners[type],
290 listenersAll = self._listeners['all'];
291382 if (listenersType !== undefined){
292112 listeners = listeners.concat(listenersType);
293 }
294382 if (listenersAll !== undefined){
295123 listeners = listeners.concat(listenersAll);
296 }
297382 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 diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 12f925521..44a189f1b 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -1,8 +1,8 @@ /*! - * HTMLHint v0.9.4 + * HTMLHint v0.9.6 * https://github.com/yaniswang/HTMLHint * * (c) 2013 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(a){var b={};return b.version="0.9.4",b.rules={},b.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},b.addRule=function(a){b.rules[a.id]=a},b.verify=function(c,d){d===a&&(d=b.defaultRuleset);var e,f=new HTMLParser,g=new b.Reporter(c.split(/\r?\n/),d),h=b.rules;for(var i in d)e=h[i],e!==a&&e.init(f,g,d[i]);return f.parse(c),g.messages},b}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(a){var b=function(){var a=this;a._init.apply(a,arguments)};b.prototype={_init:function(a,b){var c=this;c.lines=a,c.ruleset=b,c.messages=[]},error:function(a,b,c,d,e){this.report("error",a,b,c,d,e)},warn:function(a,b,c,d,e){this.report("warning",a,b,c,d,e)},info:function(a,b,c,d,e){this.report("info",a,b,c,d,e)},report:function(a,b,c,d,e,f){var g=this;g.messages.push({type:a,message:b,raw:f,evidence:g.lines[c-1],line:c,col:d,rule:{id:e.id,description:e.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+e.id}})}},a.Reporter=b}(HTMLHint);var HTMLParser=function(a){var b=function(){var a=this;a._init.apply(a,arguments)};return b.prototype={_init:function(){var a=this;a._listeners={},a._mapCdataTags=a.makeMap("script,style"),a._arrBlocks=[]},makeMap:function(a){for(var b={},c=a.split(","),d=0;d]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,o=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,p=/\r?\n/g,q=0,r=0,s=0,t=1,u=l._arrBlocks;for(l.fire("start",{pos:0,line:1,col:1});d=n.exec(b);)if(e=d.index,e>q&&(k=b.substring(q,e),h?j.push(k):c("text",k,q)),q=n.lastIndex,!(f=d[1])||(h&&f===h&&(k=j.join(""),c("cdata",k,r,{tagName:h,attrs:i}),h=null,i=null,j=null),h))if(h)j.push(d[0]);else if(f=d[4]){g=[];for(var v,w=d[5],x=0;v=o.exec(w);){var y=v[1],z=v[2]?v[2]:v[4]?v[4]:"",A=v[3]?v[3]:v[5]?v[5]:v[6]?v[6]:"";g.push({name:y,value:A,quote:z,index:v.index,raw:v[0]}),x+=v[0].length}x===w.length?(c("tagstart",d[0],e,{tagName:f,attrs:g,close:d[6]}),m[f]&&(h=f,i=g.concat(),j=[],r=q)):c("text",d[0],e)}else(d[2]||d[3])&&c("comment",d[0],e,{content:d[2]||d[3],"long":d[2]?!0:!1});else c("tagend",d[0],e,{tagName:f});b.length>q&&(k=b.substring(q,b.length),c("text",k,q)),l.fire("end",{pos:q,line:t,col:q-s+1})},addListener:function(b,c){for(var d,e=this._listeners,f=b.split(/[,\s]/),g=0,h=f.length;h>g;g++)d=f[g],e[d]===a&&(e[d]=[]),e[d].push(c)},fire:function(b,c){c===a&&(c={}),c.type=b;var d=this,e=[],f=d._listeners[b],g=d._listeners.all;f!==a&&(e=e.concat(f)),g!==a&&(e=e.concat(g));for(var h=0,i=e.length;i>h;h++)e[h].call(d,c)},removeListener:function(b,c){var d=this._listeners[b];if(d!==a)for(var e=0,f=d.length;f>e;e++)if(d[e]===c){d.splice(e,1);break}},fixPos:function(a,b){var c,d=a.raw.substr(0,b),e=d.split(/\r?\n/),f=e.length-1,g=a.line;return f>0?(g+=f,c=e[f].length+1):c=a.col+b,{line:g,col:c}},getMapAttrs:function(a){for(var b,c={},d=0,e=a.length;e>d;d++)b=a[d],c[b.name]=b.value;return c}},b}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++){d=e[g];var i=d.name;i!==i.toLowerCase()&&b.error("Attribute name [ "+i+" ] must be lower case.",a.line,f+d.index,c,d.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++)d=e[g],(""!==d.value&&'"'!==d.quote||""===d.value&&"'"===d.quote)&&b.error("The value of attribute [ "+d.name+" ] must closed by double quotes.",a.line,f+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++)d=e[g],""===d.quote&&""===d.value&&b.warn("The attribute [ "+d.name+" ] must set value.",a.line,f+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(a,b,c){var d=this;a.addListener("cdata",function(a){if("style"===a.tagName.toLowerCase()){var e;if(e="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==c){var f=a.line-1,g=a.col-1;try{var h=e(a.raw,c).messages;h.forEach(function(a){var c=a.line;b["warning"===a.type?"warn":"error"]("["+a.rule.id+"] "+a.message,f+c,(1===c?g:0)+a.col,d,a.evidence)})}catch(i){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(a,b){var c=this,d=function(e){"start"===e.type||"text"===e.type&&/^\s*$/.test(e.raw)||(("comment"!==e.type&&e.long===!1||/^DOCTYPE\s+/i.test(e.content)===!1)&&b.error("Doctype must be first.",e.line,e.col,c,e.raw),a.removeListener("all",d))};a.addListener("all",d)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(a,b){function c(a){a.long===!1&&"doctype html"!==a.content.toLowerCase()&&b.warn("Doctype must be html5.",a.line,a.col,e,a.raw)}function d(){a.removeListener("comment",c),a.removeListener("tagstart",d)}var e=this;a.addListener("all",c),a.addListener("tagstart",d)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(a,b){function c(a){"script"===a.tagName.toLowerCase()&&b.warn("The script tag can not be used in head.",a.line,a.col,e,a.raw)}function d(b){"head"===b.tagName.toLowerCase()&&(a.removeListener("tagstart",c),a.removeListener("tagstart",d))}var e=this;a.addListener("tagstart",c),a.addListener("tagend",d)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(a,b,c){var d,e=this,f={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(d="string"==typeof c?f[c]:c,d&&d.regId){var g=d.regId,h=d.message;a.addListener("tagstart",function(a){for(var c,d=a.attrs,f=a.col+a.tagName.length+1,i=0,j=d.length;j>i;i++)if(c=d[i],"id"===c.name.toLowerCase()&&g.test(c.value)===!1&&b.warn(h,a.line,f+c.index,e,c.raw),"class"===c.name.toLowerCase())for(var k,l=c.value.split(/\s+/g),m=0,n=l.length;n>m;m++)k=l[m],k&&g.test(k)===!1&&b.warn(h,a.line,f+c.index,e,k)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(a,b){var c=this,d={};a.addListener("tagstart",function(a){for(var e,f,g=a.attrs,h=a.col+a.tagName.length+1,i=0,j=g.length;j>i;i++)if(e=g[i],"id"===e.name.toLowerCase()){f=e.value,f&&(void 0===d[f]?d[f]=1:d[f]++,d[f]>1&&b.error("Id redefinition of [ "+f+" ].",a.line,h+e.index,c,e.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){if("img"===a.tagName.toLowerCase()){for(var d=a.attrs,e=!1,f=0,g=d.length;g>f;f++)if("alt"===d[f].name.toLowerCase()){e=!0;break}e===!1&&b.warn("Alt of img tag must be set value.",a.line,a.col,c,a.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(a,b,c){var d=this;a.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var f=a.getMapAttrs(e.attrs),g=f.type;if(void 0!==f.src||g&&/^(text\/javascript)$/i.test(g)===!1)return;var h;if(h="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==c){var i=e.line-1,j=e.col-1,k=e.raw.replace(/\t/g," ");try{var l=h(k,c);l===!1&&h.errors.forEach(function(a){var c=a.line;b.warn(a.reason,i+c,(1===c?j:0)+a.character,d,a.evidence)})}catch(m){}}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(a,b){var c=this;a.addListener("text",function(d){for(var e,f=d.raw,g=/[<>]/g;e=g.exec(f);){var h=a.fixPos(d,e.index);b.error("Special characters must be escaped : [ "+e[0]+" ].",h.line,h.col,c,d.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.tagName,f=a.attrs,g=a.col+e.length+1,h=0,i=f.length;i>h;h++)d=f[h],(/^(img|script|embed|bgsound|iframe)$/.test(e)===!0&&"src"===d.name||"link"===e&&"href"===d.name||"object"===e&&"data"===d.name)&&""===d.value&&b.error("[ "+d.name+"] of [ "+e+" ] must set value.",a.line,g+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){"style"===a.tagName.toLowerCase()&&b.warn("Style tag can not be use.",a.line,a.col,c,a.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(a,b){var c=this,d=[],e=a.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");a.addListener("tagstart",function(a){var b=a.tagName.toLowerCase();void 0!==e[b]||a.close||d.push(b)}),a.addListener("tagend",function(a){for(var e=a.tagName.toLowerCase(),f=d.length-1;f>=0&&d[f]!==e;f--);if(f>=0){for(var g=[],h=d.length-1;h>f;h--)g.push("");g.length>0&&b.error("Tag must be paired, Missing: [ "+g.join("")+" ]",a.line,a.col,c,a.raw),d.length=f}else b.error("Tag must be paired, No start tag: [ "+a.raw+" ]",a.line,a.col,c,a.raw)}),a.addListener("end",function(a){for(var e=[],f=d.length-1;f>=0;f--)e.push("");e.length>0&&b.error("Tag must be paired, Missing: [ "+e.join("")+" ]",a.line,a.col,c,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(a,b){var c=this,d=a.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");a.addListener("tagstart",function(a){var e=a.tagName.toLowerCase();void 0!==d[e]&&(a.close||b.warn("The empty tag : [ "+e+" ] must closed by self.",a.line,a.col,c,a.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(a,b){var c=this;a.addListener("tagstart,tagend",function(a){var d=a.tagName;d!==d.toLowerCase()&&b.error("Tagname [ "+d+" ] must be lower case.",a.line,a.col,c,a.raw)})}}); \ No newline at end of file +var HTMLHint=function(a){var b={};return b.version="0.9.6",b.rules={},b.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},b.addRule=function(a){b.rules[a.id]=a},b.verify=function(c,d){d===a&&(d=b.defaultRuleset);var e,f=new HTMLParser,g=new b.Reporter(c.split(/\r?\n/),d),h=b.rules;for(var i in d)e=h[i],e!==a&&e.init(f,g,d[i]);return f.parse(c),g.messages},b}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(a){var b=function(){var a=this;a._init.apply(a,arguments)};b.prototype={_init:function(a,b){var c=this;c.lines=a,c.ruleset=b,c.messages=[]},error:function(a,b,c,d,e){this.report("error",a,b,c,d,e)},warn:function(a,b,c,d,e){this.report("warning",a,b,c,d,e)},info:function(a,b,c,d,e){this.report("info",a,b,c,d,e)},report:function(a,b,c,d,e,f){var g=this;g.messages.push({type:a,message:b,raw:f,evidence:g.lines[c-1],line:c,col:d,rule:{id:e.id,description:e.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+e.id}})}},a.Reporter=b}(HTMLHint);var HTMLParser=function(a){var b=function(){var a=this;a._init.apply(a,arguments)};return b.prototype={_init:function(){var a=this;a._listeners={},a._mapCdataTags=a.makeMap("script,style"),a._arrBlocks=[]},makeMap:function(a){for(var b={},c=a.split(","),d=0;d]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,o=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,p=/\r?\n/g,q=0,r=0,s=0,t=1,u=l._arrBlocks;for(l.fire("start",{pos:0,line:1,col:1});d=n.exec(b);)if(e=d.index,e>q&&(k=b.substring(q,e),h?j.push(k):c("text",k,q)),q=n.lastIndex,!(f=d[1])||(h&&f===h&&(k=j.join(""),c("cdata",k,r,{tagName:h,attrs:i}),h=null,i=null,j=null),h))if(h)j.push(d[0]);else if(f=d[4]){g=[];for(var v,w=d[5],x=0;v=o.exec(w);){var y=v[1],z=v[2]?v[2]:v[4]?v[4]:"",A=v[3]?v[3]:v[5]?v[5]:v[6]?v[6]:"";g.push({name:y,value:A,quote:z,index:v.index,raw:v[0]}),x+=v[0].length}x===w.length?(c("tagstart",d[0],e,{tagName:f,attrs:g,close:d[6]}),m[f]&&(h=f,i=g.concat(),j=[],r=q)):c("text",d[0],e)}else(d[2]||d[3])&&c("comment",d[0],e,{content:d[2]||d[3],"long":d[2]?!0:!1});else c("tagend",d[0],e,{tagName:f});b.length>q&&(k=b.substring(q,b.length),c("text",k,q)),l.fire("end",{pos:q,line:t,col:q-s+1})},addListener:function(b,c){for(var d,e=this._listeners,f=b.split(/[,\s]/),g=0,h=f.length;h>g;g++)d=f[g],e[d]===a&&(e[d]=[]),e[d].push(c)},fire:function(b,c){c===a&&(c={}),c.type=b;var d=this,e=[],f=d._listeners[b],g=d._listeners.all;f!==a&&(e=e.concat(f)),g!==a&&(e=e.concat(g));for(var h=0,i=e.length;i>h;h++)e[h].call(d,c)},removeListener:function(b,c){var d=this._listeners[b];if(d!==a)for(var e=0,f=d.length;f>e;e++)if(d[e]===c){d.splice(e,1);break}},fixPos:function(a,b){var c,d=a.raw.substr(0,b),e=d.split(/\r?\n/),f=e.length-1,g=a.line;return f>0?(g+=f,c=e[f].length+1):c=a.col+b,{line:g,col:c}},getMapAttrs:function(a){for(var b,c={},d=0,e=a.length;e>d;d++)b=a[d],c[b.name]=b.value;return c}},b}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++){d=e[g];var i=d.name;i!==i.toLowerCase()&&b.error("Attribute name [ "+i+" ] must be lower case.",a.line,f+d.index,c,d.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Attribute name can not been duplication.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e,f=a.attrs,g=a.col+a.tagName.length+1,h={},i=0,j=f.length;j>i;i++)d=f[i],e=d.name,h[e]===!0&&b.error("The name of attribute [ "+d.name+" ] been duplication.",a.line,g+d.index,c,d.raw),h[e]=!0})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++)d=e[g],(""!==d.value&&'"'!==d.quote||""===d.value&&"'"===d.quote)&&b.error("The value of attribute [ "+d.name+" ] must closed by double quotes.",a.line,f+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.attrs,f=a.col+a.tagName.length+1,g=0,h=e.length;h>g;g++)d=e[g],""===d.quote&&""===d.value&&b.warn("The attribute [ "+d.name+" ] must set value.",a.line,f+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(a,b,c){var d=this;a.addListener("cdata",function(a){if("style"===a.tagName.toLowerCase()){var e;if(e="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==c){var f=a.line-1,g=a.col-1;try{var h=e(a.raw,c).messages;h.forEach(function(a){var c=a.line;b["warning"===a.type?"warn":"error"]("["+a.rule.id+"] "+a.message,f+c,(1===c?g:0)+a.col,d,a.evidence)})}catch(i){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(a,b){var c=this,d=function(e){"start"===e.type||"text"===e.type&&/^\s*$/.test(e.raw)||(("comment"!==e.type&&e.long===!1||/^DOCTYPE\s+/i.test(e.content)===!1)&&b.error("Doctype must be first.",e.line,e.col,c,e.raw),a.removeListener("all",d))};a.addListener("all",d)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(a,b){function c(a){a.long===!1&&"doctype html"!==a.content.toLowerCase()&&b.warn("Doctype must be html5.",a.line,a.col,e,a.raw)}function d(){a.removeListener("comment",c),a.removeListener("tagstart",d)}var e=this;a.addListener("all",c),a.addListener("tagstart",d)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(a,b){function c(a){"script"===a.tagName.toLowerCase()&&b.warn("The script tag can not be used in head.",a.line,a.col,e,a.raw)}function d(b){"head"===b.tagName.toLowerCase()&&(a.removeListener("tagstart",c),a.removeListener("tagstart",d))}var e=this;a.addListener("tagstart",c),a.addListener("tagend",d)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(a,b,c){var d,e=this,f={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(d="string"==typeof c?f[c]:c,d&&d.regId){var g=d.regId,h=d.message;a.addListener("tagstart",function(a){for(var c,d=a.attrs,f=a.col+a.tagName.length+1,i=0,j=d.length;j>i;i++)if(c=d[i],"id"===c.name.toLowerCase()&&g.test(c.value)===!1&&b.warn(h,a.line,f+c.index,e,c.raw),"class"===c.name.toLowerCase())for(var k,l=c.value.split(/\s+/g),m=0,n=l.length;n>m;m++)k=l[m],k&&g.test(k)===!1&&b.warn(h,a.line,f+c.index,e,k)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(a,b){var c=this,d={};a.addListener("tagstart",function(a){for(var e,f,g=a.attrs,h=a.col+a.tagName.length+1,i=0,j=g.length;j>i;i++)if(e=g[i],"id"===e.name.toLowerCase()){f=e.value,f&&(void 0===d[f]?d[f]=1:d[f]++,d[f]>1&&b.error("Id redefinition of [ "+f+" ].",a.line,h+e.index,c,e.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){if("img"===a.tagName.toLowerCase()){for(var d=a.attrs,e=!1,f=0,g=d.length;g>f;f++)if("alt"===d[f].name.toLowerCase()){e=!0;break}e===!1&&b.warn("Alt of img tag must be set value.",a.line,a.col,c,a.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(a,b,c){var d=this;a.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var f=a.getMapAttrs(e.attrs),g=f.type;if(void 0!==f.src||g&&/^(text\/javascript)$/i.test(g)===!1)return;var h;if(h="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==c){var i=e.line-1,j=e.col-1,k=e.raw.replace(/\t/g," ");try{var l=h(k,c);l===!1&&h.errors.forEach(function(a){var c=a.line;b.warn(a.reason,i+c,(1===c?j:0)+a.character,d,a.evidence)})}catch(m){}}}})}}),HTMLHint.addRule({id:"space-tab-mixed-disabled",description:"Spaces and tabs can not mixed in front of line.",init:function(a,b){var c=this;a.addListener("text",function(a){0===a.pos&&/^( +\t|\t+ )/.test(a.raw)===!0&&b.warn("Mixed spaces and tabs in front of line.",a.line,0,c,a.raw)})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(a,b){var c=this;a.addListener("text",function(d){for(var e,f=d.raw,g=/[<>]/g;e=g.exec(f);){var h=a.fixPos(d,e.index);b.error("Special characters must be escaped : [ "+e[0]+" ].",h.line,h.col,c,d.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){for(var d,e=a.tagName,f=a.attrs,g=a.col+e.length+1,h=0,i=f.length;i>h;h++)d=f[h],(/^(img|script|embed|bgsound|iframe)$/.test(e)===!0&&"src"===d.name||"link"===e&&"href"===d.name||"object"===e&&"data"===d.name)&&""===d.value&&b.error("[ "+d.name+"] of [ "+e+" ] must set value.",a.line,g+d.index,c,d.raw)})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(a,b){var c=this;a.addListener("tagstart",function(a){"style"===a.tagName.toLowerCase()&&b.warn("Style tag can not be use.",a.line,a.col,c,a.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(a,b){var c=this,d=[],e=a.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");a.addListener("tagstart",function(a){var b=a.tagName.toLowerCase();void 0!==e[b]||a.close||d.push(b)}),a.addListener("tagend",function(a){for(var e=a.tagName.toLowerCase(),f=d.length-1;f>=0&&d[f]!==e;f--);if(f>=0){for(var g=[],h=d.length-1;h>f;h--)g.push("");g.length>0&&b.error("Tag must be paired, Missing: [ "+g.join("")+" ]",a.line,a.col,c,a.raw),d.length=f}else b.error("Tag must be paired, No start tag: [ "+a.raw+" ]",a.line,a.col,c,a.raw)}),a.addListener("end",function(a){for(var e=[],f=d.length-1;f>=0;f--)e.push("");e.length>0&&b.error("Tag must be paired, Missing: [ "+e.join("")+" ]",a.line,a.col,c,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(a,b){var c=this,d=a.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");a.addListener("tagstart",function(a){var e=a.tagName.toLowerCase();void 0!==d[e]&&(a.close||b.warn("The empty tag : [ "+e+" ] must closed by self.",a.line,a.col,c,a.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(a,b){var c=this;a.addListener("tagstart,tagend",function(a){var d=a.tagName;d!==d.toLowerCase()&&b.error("Tagname [ "+d+" ] must be lower case.",a.line,a.col,c,a.raw)})}}); \ No newline at end of file diff --git a/package.json b/package.json index 86a9db128..102bc7d3d 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,28 @@ { "name": "htmlhint", - "version": "0.9.5", + "version": "0.9.6", "description": "A Static Code Analysis Tool for HTML", "main": "./index", "dependencies": { - "commander": "~1.1.1", - "colors": "~0.6.0-1", - "jshint": "~1.1.0", - "csslint": "~0.9.10" + "commander": "1.1.1", + "colors": "0.6.0-1", + "jshint": "1.1.0", + "csslint": "0.9.10" }, "devDependencies": { - "grunt-cli": "~0.1.6", - "grunt": "~0.4.1", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-uglify": "~0.2.0", - "grunt-contrib-watch": "~0.3.1", - "grunt-replace": "~0.4.0", - "grunt-contrib-jshint": "~0.3.0", + "grunt-cli": "0.1.6", + "grunt": "0.4.1", + "grunt-contrib-clean": "0.4.0", + "grunt-contrib-concat": "0.1.3", + "grunt-contrib-uglify": "0.2.0", + "grunt-contrib-watch": "0.3.1", + "grunt-replace": "0.4.0", + "grunt-contrib-jshint": "0.3.0", "grunt-mocha-hack": "0.1.0", - "grunt-exec": "~0.4.0", - "mocha": "~1.8.2", - "expect.js": "~0.2.0", - "jscover": "~0.2.4" + "grunt-exec": "0.4.0", + "mocha": "1.8.2", + "expect.js": "0.2.0", + "jscover": "0.2.4" }, "bin": { "htmlhint": "./bin/htmlhint" diff --git a/src/cli.js b/src/cli.js deleted file mode 100644 index 17fc0a99f..000000000 --- a/src/cli.js +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env node - -var program = require('commander'), - fs = require('fs'), - path = require('path'), - HTMLHint = require("../index").HTMLHint; - -require('colors'); - -function map(val) { - var objMap = {}; - val.split(',').forEach(function(item){ - var arrItem = item.split(/\s*=\s*/); - objMap[arrItem[0]] = arrItem[1]?arrItem[1]:true; - }); - return objMap; -} - -program.on('--help', function(){ - console.log(' Examples:'); - console.log(''); - console.log(' htmlhint -l'); - console.log(' htmlhint -r tag-pair,id-class-value=underline test.html'); - console.log(' htmlhint -c .htmlhintrc test.html'); - console.log(''); -}); - -program - .version('@VERSION') - .usage('[options] ') - .option('-l, --list', 'show all of the rules available.') - .option('-c, --config ', 'custom configuration file.') - .option('-r, --rules ', 'set all of the rules available.', map) - .parse(process.argv); - -if(program.list){ - listRules(); - quit(0); -} - -var arrAllFiles = getAllFiles(program.args); - -var ruleset = program.rules; -if(ruleset === undefined){ - ruleset = getConfig(program.config); -} - -quit(processFiles(arrAllFiles, ruleset)); - -function listRules(){ - var rules = HTMLHint.rules, - rule; - console.log('\r\nAll rules:'); - console.log('======================================'); - for (var id in rules){ - rule = rules[id]; - console.log('\r\n'+rule.id+' :'); - console.log(' '+rule.description); - } -} - -function getConfig(configFile){ - if(configFile === undefined){ - configFile = '.htmlhintrc'; - } - if(fs.existsSync(configFile)){ - var config = fs.readFileSync(configFile, 'utf-8'), - ruleset; - try{ - ruleset = JSON.parse(config); - } - catch(e){} - return ruleset; - } -} - -function getAllFiles(arrTargets){ - var arrAllFiles = []; - if(arrTargets.length > 0){ - for(var i=0,l=arrTargets.length;i 0){ - exitcode = 1; - allHintCount += hintCount; - } - }); - if(allHintCount > 0){ - console.log('\r\n%d problems.'.red, allHintCount); - } - else{ - console.log('No problem.'.green); - } - return exitcode; -} - -function hintFile(filepath, ruleset){ - var html = fs.readFileSync(filepath, 'utf-8'); - var messages = HTMLHint.verify(html, ruleset); - if(messages.length > 0){ - console.log(filepath+':'); - messages.forEach(function(hint){ - console.log('\tline %d, col %d: %s', hint.line, hint.col, hint.message[hint.type === 'error'?'red':'yellow']); - }); - console.log(''); - } - return messages.length; -} - -function quit(code){ - if ((!process.stdout.flush || !process.stdout.flush()) && (parseFloat(process.versions.node) < 0.5)) { - process.once("drain", function () { - process.exit(code || 0); - }); - } else { - process.exit(code || 0); - } -} \ No newline at end of file diff --git a/src/core.js b/src/core.js index eb8d4ddaf..e8cec0c62 100644 --- a/src/core.js +++ b/src/core.js @@ -19,7 +19,8 @@ var HTMLHint = (function (undefined) { 'tag-pair': true, 'spec-char-escape': true, 'id-unique': true, - 'src-not-empty': true + 'src-not-empty': true, + 'attr-no-duplication': true // added: 2014-6-14 }; HTMLHint.addRule = function(rule){ diff --git a/src/rules/attr-no-duplication.js b/src/rules/attr-no-duplication.js new file mode 100644 index 000000000..adbff8da6 --- /dev/null +++ b/src/rules/attr-no-duplication.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014, Yanis Wang + * MIT Licensed + */ +HTMLHint.addRule({ + id: 'attr-no-duplication', + description: 'Attribute name can not been duplication.', + 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; + + var mapAttrName = {}; + for(var i=0, l=attrs.length;i + * MIT Licensed + */ +HTMLHint.addRule({ + id: 'space-tab-mixed-disabled', + description: 'Spaces and tabs can not mixed in front of line.', + init: function(parser, reporter){ + var self = this; + parser.addListener('text', function(event){ + if(event.pos === 0 && /^( +\t|\t+ )/.test(event.raw) === true){ + reporter.warn('Mixed spaces and tabs in front of line.', event.line, 0, self, event.raw); + } + }); + } +}); \ No newline at end of file diff --git a/test/rules/attr-no-duplication.js b/test/rules/attr-no-duplication.js new file mode 100644 index 000000000..590aab13d --- /dev/null +++ b/test/rules/attr-no-duplication.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2014, Yanis Wang + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../../index").HTMLHint; + +var ruldId = 'attr-no-duplication', + ruleOptions = {}; + +ruleOptions[ruldId] = true; + +describe('Rules: '+ruldId, function(){ + + it('Attribute name been duplication should result in an error', function(){ + var code = 'bbb'; + 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(12); + }); + + it('Attribute name not been duplication should not result in an error', function(){ + var code = 'bbb'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + }); + +}); \ No newline at end of file diff --git a/test/rules/space-tab-mixed-disabled.js b/test/rules/space-tab-mixed-disabled.js new file mode 100644 index 000000000..b74747014 --- /dev/null +++ b/test/rules/space-tab-mixed-disabled.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014, Yanis Wang + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../../index").HTMLHint; + +var ruldId = 'space-tab-mixed-disabled', + ruleOptions = {}; + +ruleOptions[ruldId] = true; + +describe('Rules: '+ruldId, function(){ + + it('Spaces and tabs mixed in front of line should result in an error', function(){ + // space before tab + var code = ' bbb'; + 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(0); + // tab before space + code = ' bbb'; + 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(0); + }); + + it('Only spaces in front of line should not result in an error', function(){ + var code = ' bbb'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + }); + + it('Only tabs in front of line should not result in an error', function(){ + var code = ' bbb'; + var messages = HTMLHint.verify(code, ruleOptions); + expect(messages.length).to.be(0); + }); + +}); \ No newline at end of file