diff --git a/coverage.html b/coverage.html index c7e944afa..14234960e 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%
339
335
4

htmlhint.js

98%
339
335
4
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var rules = [];
8
91 var HTMLHint = {};
10
111 HTMLHint.version = '@VERSION';
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 };
22
231 HTMLHint.addRule = function(rule){
2416 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2845 if(ruleset === undefined){
291 ruleset = HTMLHint.defaultRuleset;
30 }
3145 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3445 var rule;
3545 for (var id in ruleset){
3650 rule = rules[id];
3750 if (rule !== undefined){
3850 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4245 parser.parse(html);
43
4445 return reporter.messages;
45 };
46
471 return HTMLHint;
48
49})();
50
511if (typeof exports === 'object' && exports){
521 exports.HTMLHint = HTMLHint;
53}
54/**
55 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
56 * MIT Licensed
57 */
581(function(HTMLHint, undefined){
59
601 var Reporter = function(){
6145 var self = this;
6245 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6745 var self = this;
6845 self.lines = lines;
6945 self.ruleset = ruleset;
7045 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7418 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7826 this.report('warning', message, line, col, rule, raw);
79 },
80 //信息
81 info: function(message, line, col, rule, raw){
820 this.report('info', message, line, col, rule, raw);
83 },
84 //报告
85 report: function(type, message, line, col, rule, raw){
8644 var self = this;
8744 self.messages.push({
88 type: type,
89 message: message,
90 raw: raw,
91 evidence: self.lines[line-1],
92 line: line,
93 col: col,
94 rule: rule
95 });
96 }
97 };
98
991 HTMLHint.Reporter = Reporter;
100
101})(HTMLHint);
102/**
103 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
104 * MIT Licensed
105 */
1061var HTMLParser = (function(undefined){
107
1081 var HTMLParser = function(){
10967 var self = this;
11067 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11567 var self = this;
11667 self._listeners = {};
11767 self._mapCdataTags = self.makeMap("script,style");
11867 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12273 var obj = {}, items = str.split(",");
12373 for ( var i = 0; i < items.length; i++ ){
124218 obj[ items[i] ] = true;
125 }
12673 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13267 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13567 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13967 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14067 var lastLineIndex = 0, line = 1;
14167 var arrBlocks = self._arrBlocks;
142
14367 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14967 while((match = regTag.exec(html))){
150130 matchIndex = match.index;
151130 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15225 text = html.substring(lastIndex, matchIndex);
15325 if(tagCDATA){
15410 arrCDATA.push(text);
155 }
156 else{//文本
15715 saveBlock('text', text, lastIndex);
158 }
159 }
160130 lastIndex = regTag.lastIndex;
161
162130 if((tagName = match[1])){
16343 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
16411 text = arrCDATA.join('');
16511 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA,
167 'attrs': attrsCDATA
168 });
16911 tagCDATA = null;
17011 attrsCDATA = null;
17111 arrCDATA = null;
172 }
17343 if(!tagCDATA){
174 //标签结束
17542 saveBlock('tagend', match[0], matchIndex, {
176 'tagName': tagName
177 });
17842 continue;
179 }
180 }
181
18288 if(tagCDATA){
1831 arrCDATA.push(match[0]);
184 }
185 else{
18687 if((tagName = match[4])){//标签开始
18774 arrAttrs = [];
18874 var attrs = match[5],
189 attrMatch,
190 attrMatchCount = 0;
19174 while((attrMatch = regAttr.exec(attrs))){
19261 var name = attrMatch[1],
193 quote = attrMatch[2] ? attrMatch[2] :
194 attrMatch[4] ? attrMatch[4] : '',
195 value = attrMatch[3] ? attrMatch[3] :
196 attrMatch[5] ? attrMatch[5] :
197 attrMatch[6] ? attrMatch[6] : '';
19861 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
19961 attrMatchCount += attrMatch[0].length;
200 }
20174 if(attrMatchCount === attrs.length){
20273 saveBlock('tagstart', match[0], matchIndex, {
203 'tagName': tagName,
204 'attrs': arrAttrs,
205 'close': match[6]
206 });
20773 if(mapCdataTags[tagName]){
20811 tagCDATA = tagName;
20911 attrsCDATA = arrAttrs.concat();
21011 arrCDATA = [];
21111 lastCDATAIndex = lastIndex;
212 }
213 }
214 else{//如果出现漏匹配,则把当前内容匹配为text
2151 saveBlock('text', match[0], matchIndex);
216 }
217 }
21813 else if(match[2] || match[3]){//注释标签
21913 saveBlock('comment', match[0], matchIndex, {
220 'content': match[2] || match[3],
221 'long': match[2]?true:false
222 });
223 }
224 }
225 }
226
22767 if(html.length > lastIndex){
228 //结尾文本
2299 text = html.substring(lastIndex, html.length);
2309 saveBlock('text', text, lastIndex);
231 }
232
23367 self.fire('end', {
234 pos: lastIndex,
235 line: line,
236 col: lastIndex - lastLineIndex + 1
237 });
238
239 //存储区块
24067 function saveBlock(type, raw, pos, data){
241164 var col = pos - lastLineIndex + 1;
242164 if(data === undefined){
24325 data = {};
244 }
245164 data.raw = raw;
246164 data.pos = pos;
247164 data.line = line;
248164 data.col = col;
249164 arrBlocks.push(data);
250164 self.fire(type, data);
251164 var lineMatch;
252164 while((lineMatch = regLine.exec(raw))){
25318 line ++;
25418 lastLineIndex = pos + regLine.lastIndex;
255 }
256 }
257
258 },
259
260 // add event
261 addListener: function(types, listener){
26285 var _listeners = this._listeners;
26385 var arrTypes = types.split(/[,\s]/), type;
26485 for(var i=0, l = arrTypes.length;i<l;i++){
26588 type = arrTypes[i];
26688 if (_listeners[type] === undefined){
26784 _listeners[type] = [];
268 }
26988 _listeners[type].push(listener);
270 }
271 },
272
273 // fire event
274 fire: function(type, data){
275298 if (data === undefined){
2760 data = {};
277 }
278298 data.type = type;
279298 var self = this,
280 listeners = [],
281 listenersType = self._listeners[type],
282 listenersAll = self._listeners['all'];
283298 if (listenersType !== undefined){
28475 listeners = listeners.concat(listenersType);
285 }
286298 if (listenersAll !== undefined){
287112 listeners = listeners.concat(listenersAll);
288 }
289298 for (var i = 0, l = listeners.length; i < l; i++){
290183 listeners[i].call(self, data);
291 }
292 },
293
294 // remove event
295 removeListener: function(type, listener){
29613 var listenersType = this._listeners[type];
29713 if(listenersType !== undefined){
29811 for (var i = 0, l = listenersType.length; i < l; i++){
2998 if (listenersType[i] === listener){
3008 listenersType.splice(i, 1);
3018 break;
302 }
303 }
304 }
305 },
306
307 //fix pos if event.raw have \n
308 fixPos: function(event, index){
3093 var text = event.raw.substr(0, index);
3103 var arrLines = text.split(/\r?\n/),
311 lineCount = arrLines.length - 1,
312 line = event.line, col;
3133 if(lineCount > 0){
3141 line += lineCount;
3151 col = arrLines[lineCount].length + 1;
316 }
317 else{
3182 col = event.col + index;
319 }
3203 return {
321 line: line,
322 col: col
323 };
324 },
325
326 // covert array type of attrs to map
327 getMapAttrs: function(arrAttrs){
3286 var mapAttrs = {},
329 attr;
3306 for(var i=0,l=arrAttrs.length;i<l;i++){
3316 attr = arrAttrs[i];
3326 mapAttrs[attr.name] = attr.value;
333 }
3346 return mapAttrs;
335 }
336 };
337
3381 return HTMLParser;
339
340})();
341
3421if (typeof exports === 'object' && exports){
3431 exports.HTMLParser = HTMLParser;
344}
345/**
346 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
347 * MIT Licensed
348 */
3491HTMLHint.addRule({
350 id: 'attr-lowercase',
351 description: 'Attribute name must be lowercase.',
352 init: function(parser, reporter){
3533 var self = this;
3543 parser.addListener('tagstart', function(event){
3553 var attrs = event.attrs,
356 attr,
357 col = event.col + event.tagName.length + 1;
3583 for(var i=0, l=attrs.length;i<l;i++){
3593 attr = attrs[i];
3603 var attrName = attr.name;
3613 if(attrName !== attrName.toLowerCase()){
3622 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
363 }
364 }
365 });
366 }
367});
368/**
369 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
370 * MIT Licensed
371 */
3721HTMLHint.addRule({
373 id: 'attr-value-double-quotes',
374 description: 'Attribute value must closed by double quotes.',
375 init: function(parser, reporter){
3764 var self = this;
3774 parser.addListener('tagstart', function(event){
3784 var attrs = event.attrs,
379 attr,
380 col = event.col + event.tagName.length + 1;
3814 for(var i=0, l=attrs.length;i<l;i++){
3826 attr = attrs[i];
3836 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3842 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
385 }
386 }
387 });
388 }
389});
390/**
391 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
392 * MIT Licensed
393 */
3941HTMLHint.addRule({
395 id: 'attr-value-not-empty',
396 description: 'Attribute must set value.',
397 init: function(parser, reporter){
3983 var self = this;
3993 parser.addListener('tagstart', function(event){
4003 var attrs = event.attrs,
401 attr,
402 col = event.col + event.tagName.length + 1;
4033 for(var i=0, l=attrs.length;i<l;i++){
4043 attr = attrs[i];
4053 if(attr.quote === '' && attr.value === ''){
4061 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
407 }
408 }
409 });
410 }
411});
412/**
413 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
414 * MIT Licensed
415 */
4161HTMLHint.addRule({
417 id: 'csslint',
418 description: 'Scan css with csslint.',
419 init: function(parser, reporter, options){
4201 var self = this;
4211 parser.addListener('cdata', function(event){
4221 if(event.tagName.toLowerCase() === 'style'){
423
4241 var cssVerify;
425
4261 if(typeof exports === 'object' && require){
4271 cssVerify = require("csslint").CSSLint.verify;
428 }
429 else{
4300 cssVerify = CSSLint.verify;
431 }
432
4331 if(options !== undefined){
4341 var styleLine = event.line - 1,
435 styleCol = event.col - 1;
4361 try{
4371 var messages = cssVerify(event.raw, options).messages;
4381 messages.forEach(function(error){
4392 var line = error.line;
4402 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
441 });
442 }
443 catch(e){}
444 }
445
446 }
447 });
448 }
449});
450/**
451 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
452 * MIT Licensed
453 */
4541HTMLHint.addRule({
455 id: 'doctype-first',
456 description: 'Doctype must be first.',
457 init: function(parser, reporter){
4583 var self = this;
4593 var allEvent = function(event){
4606 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4613 return;
462 }
4633 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4642 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
465 }
4663 parser.removeListener('all', allEvent);
467 };
4683 parser.addListener('all', allEvent);
469 }
470});
471/**
472 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
473 * MIT Licensed
474 */
4751HTMLHint.addRule({
476 id: 'doctype-html5',
477 description: 'Doctype must be html5.',
478 init: function(parser, reporter){
4792 var self = this;
4802 function onComment(event){
4819 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4821 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
483 }
484 }
4852 function onTagStart(){
4862 parser.removeListener('comment', onComment);
4872 parser.removeListener('tagstart', onTagStart);
488 }
4892 parser.addListener('all', onComment);
4902 parser.addListener('tagstart', onTagStart);
491 }
492});
493/**
494 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
495 * MIT Licensed
496 */
4971HTMLHint.addRule({
498 id: 'head-script-disabled',
499 description: 'The script tag can not be used in head.',
500 init: function(parser, reporter){
5013 var self = this;
5023 function onTagStart(event){
5035 if(event.tagName.toLowerCase() === 'script'){
5042 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
505 }
506 }
5073 function onTagEnd(event){
5087 if(event.tagName.toLowerCase() === 'head'){
5093 parser.removeListener('tagstart', onTagStart);
5103 parser.removeListener('tagstart', onTagEnd);
511 }
512 }
5133 parser.addListener('tagstart', onTagStart);
5143 parser.addListener('tagend', onTagEnd);
515 }
516});
517/**
518 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
519 * MIT Licensed
520 */
5211HTMLHint.addRule({
522 id: 'id-class-value',
523 description: 'Id and class value must meet some rules.',
524 init: function(parser, reporter, options){
5258 var self = this;
5268 var arrRules = {
527 'underline': {
528 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
529 'message': 'Id and class value must lower case and split by underline.'
530 },
531 'dash': {
532 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
533 'message': 'Id and class value must lower case and split by dash.'
534 },
535 'hump': {
536 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
537 'message': 'Id and class value must meet hump style.'
538 }
539 }, rule;
5408 if(typeof options === 'string'){
5416 rule = arrRules[options];
542 }
543 else{
5442 rule = options;
545 }
5468 if(rule && rule.regId){
5478 var regId = rule.regId,
548 message = rule.message;
5498 parser.addListener('tagstart', function(event){
5508 var attrs = event.attrs,
551 attr,
552 col = event.col + event.tagName.length + 1;
5538 for(var i=0, l1=attrs.length;i<l1;i++){
55416 attr = attrs[i];
55516 if(attr.name.toLowerCase() === 'id'){
5568 if(regId.test(attr.value) === false){
5574 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
558 }
559 }
56016 if(attr.name.toLowerCase() === 'class'){
5618 var arrClass = attr.value.split(/\s+/g), classValue;
5628 for(var j=0, l2=arrClass.length;j<l2;j++){
5638 classValue = arrClass[j];
5648 if(classValue && regId.test(classValue) === false){
5654 reporter.warn(message, event.line, col + attr.index, self, classValue);
566 }
567 }
568 }
569 }
570 });
571 }
572 }
573});
574/**
575 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
576 * MIT Licensed
577 */
5781HTMLHint.addRule({
579 id: 'id-unique',
580 description: 'Id must be unique.',
581 init: function(parser, reporter){
5822 var self = this;
5832 var mapIdCount = {};
5842 parser.addListener('tagstart', function(event){
5854 var attrs = event.attrs,
586 attr,
587 id,
588 col = event.col + event.tagName.length + 1;
5894 for(var i=0, l=attrs.length;i<l;i++){
5904 attr = attrs[i];
5914 if(attr.name.toLowerCase() === 'id'){
5924 id = attr.value;
5934 if(id){
5944 if(mapIdCount[id] === undefined){
5953 mapIdCount[id] = 1;
596 }
597 else{
5981 mapIdCount[id] ++;
599 }
6004 if(mapIdCount[id] > 1){
6011 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
602 }
603 }
6044 break;
605 }
606 }
607 });
608 }
609});
610/**
611 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
612 * MIT Licensed
613 */
6141HTMLHint.addRule({
615 id: 'img-alt-require',
616 description: 'Alt of img tag must be set value.',
617 init: function(parser, reporter){
6183 var self = this;
6193 parser.addListener('tagstart', function(event){
6203 if(event.tagName.toLowerCase() === 'img'){
6213 var attrs = event.attrs;
6223 var haveAlt = false;
6233 for(var i=0, l=attrs.length;i<l;i++){
6248 if(attrs[i].name.toLowerCase() === 'alt'){
6252 haveAlt = true;
6262 break;
627 }
628 }
6293 if(haveAlt === false){
6301 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
631 }
632 }
633 });
634 }
635});
636/**
637 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
638 * MIT Licensed
639 */
6401HTMLHint.addRule({
641 id: 'jshint',
642 description: 'Scan script with jshint.',
643 init: function(parser, reporter, options){
6444 var self = this;
6454 parser.addListener('cdata', function(event){
6464 if(event.tagName.toLowerCase() === 'script'){
647
6484 var mapAttrs = parser.getMapAttrs(event.attrs),
649 type = mapAttrs.type;
650
651 // Only scan internal javascript
6524 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6532 return;
654 }
655
6562 var jsVerify;
657
6582 if(typeof exports === 'object' && require){
6592 jsVerify = require('jshint').JSHINT;
660 }
661 else{
6620 jsVerify = JSHINT;
663 }
664
6652 if(options !== undefined){
6662 var styleLine = event.line - 1,
667 styleCol = event.col - 1;
6682 var code = event.raw.replace(/\t/g,' ');
6692 try{
6702 var status = jsVerify(code, options);
6712 if(status === false){
6722 jsVerify.errors.forEach(function(error){
6738 var line = error.line;
6748 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
675 });
676 }
677 }
678 catch(e){}
679 }
680
681 }
682 });
683 }
684});
685/**
686 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
687 * MIT Licensed
688 */
6891HTMLHint.addRule({
690 id: 'spec-char-escape',
691 description: 'Special characters must be escaped.',
692 init: function(parser, reporter){
6933 var self = this;
6943 parser.addListener('text', function(event){
6953 var raw = event.raw,
696 reSpecChar = /[<>]/g,
697 match;
6983 while((match = reSpecChar.exec(raw))){
6993 var fixedPos = parser.fixPos(event, match.index);
7003 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
701 }
702 });
703 }
704});
705/**
706 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
707 * MIT Licensed
708 */
7091HTMLHint.addRule({
710 id: 'style-disabled',
711 description: 'Style tag can not be use.',
712 init: function(parser, reporter){
7132 var self = this;
7142 parser.addListener('tagstart', function(event){
7154 if(event.tagName.toLowerCase() === 'style'){
7161 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
717 }
718 });
719 }
720});
721/**
722 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
723 * MIT Licensed
724 */
7251HTMLHint.addRule({
726 id: 'tag-pair',
727 description: 'Tag must be paired.',
728 init: function(parser, reporter){
7294 var self = this;
7304 var stack=[],
731 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7324 parser.addListener('tagstart', function(event){
7335 var tagName = event.tagName.toLowerCase();
7345 if (mapEmptyTags[tagName] === undefined && !event.close){
7355 stack.push(tagName);
736 }
737 });
7384 parser.addListener('tagend', function(event){
7393 var tagName = event.tagName.toLowerCase();
740 //向上寻找匹配的开始标签
7413 for(var pos = stack.length-1;pos >= 0; pos--){
7423 if(stack[pos] === tagName){
7432 break;
744 }
745 }
7463 if(pos >= 0){
7472 var arrTags = [];
7482 for(var i=stack.length-1;i>pos;i--){
7491 arrTags.push('</'+stack[i]+'>');
750 }
7512 if(arrTags.length > 0){
7521 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
753 }
7542 stack.length=pos;
755 }
756 else{
7571 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
758 }
759 });
7604 parser.addListener('end', function(event){
7614 var arrTags = [];
7624 for(var i=stack.length-1;i>=0;i--){
7632 arrTags.push('</'+stack[i]+'>');
764 }
7654 if(arrTags.length > 0){
7662 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
767 }
768 });
769 }
770});
771/**
772 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
773 * MIT Licensed
774 */
7751HTMLHint.addRule({
776 id: 'tag-self-close',
777 description: 'The empty tag must closed by self.',
778 init: function(parser, reporter){
7792 var self = this;
7802 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7812 parser.addListener('tagstart', function(event){
7824 var tagName = event.tagName.toLowerCase();
7834 if(mapEmptyTags[tagName] !== undefined){
7844 if(!event.close){
7852 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
786 }
787 }
788 });
789 }
790});
791/**
792 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
793 * MIT Licensed
794 */
7951HTMLHint.addRule({
796 id: 'tagname-lowercase',
797 description: 'Tagname must be lowercase.',
798 init: function(parser, reporter){
7993 var self = this;
8003 parser.addListener('tagstart,tagend', function(event){
8019 var tagName = event.tagName;
8029 if(tagName !== tagName.toLowerCase()){
8034 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
804 }
805 });
806 }
807});
\ No newline at end of file +

Coverage

98%
339
335
4

htmlhint.js

98%
339
335
4
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var rules = [];
8
91 var HTMLHint = {};
10
111 HTMLHint.version = '@VERSION';
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 };
22
231 HTMLHint.addRule = function(rule){
2416 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2845 if(ruleset === undefined){
291 ruleset = HTMLHint.defaultRuleset;
30 }
3145 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3445 var rule;
3545 for (var id in ruleset){
3650 rule = rules[id];
3750 if (rule !== undefined){
3850 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4245 parser.parse(html);
43
4445 return reporter.messages;
45 };
46
471 return HTMLHint;
48
49})();
50
511if (typeof exports === 'object' && exports){
521 exports.HTMLHint = HTMLHint;
53}
54/**
55 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
56 * MIT Licensed
57 */
581(function(HTMLHint, undefined){
59
601 var Reporter = function(){
6145 var self = this;
6245 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6745 var self = this;
6845 self.lines = lines;
6945 self.ruleset = ruleset;
7045 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7418 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7826 this.report('warning', message, line, col, rule, raw);
79 },
80 //信息
81 info: function(message, line, col, rule, raw){
820 this.report('info', message, line, col, rule, raw);
83 },
84 //报告
85 report: function(type, message, line, col, rule, raw){
8644 var self = this;
8744 self.messages.push({
88 type: type,
89 message: message,
90 raw: raw,
91 evidence: self.lines[line-1],
92 line: line,
93 col: col,
94 rule: {
95 id: rule.id,
96 description: rule.description,
97 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
98 }
99 });
100 }
101 };
102
1031 HTMLHint.Reporter = Reporter;
104
105})(HTMLHint);
106/**
107 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
108 * MIT Licensed
109 */
1101var HTMLParser = (function(undefined){
111
1121 var HTMLParser = function(){
11367 var self = this;
11467 self._init.apply(self,arguments);
115 };
116
1171 HTMLParser.prototype = {
118 _init: function(){
11967 var self = this;
12067 self._listeners = {};
12167 self._mapCdataTags = self.makeMap("script,style");
12267 self._arrBlocks = [];
123 },
124
125 makeMap: function(str){
12673 var obj = {}, items = str.split(",");
12773 for ( var i = 0; i < items.length; i++ ){
128218 obj[ items[i] ] = true;
129 }
13073 return obj;
131 },
132
133 // parse html code
134 parse: function(html){
135
13667 var self = this,
137 mapCdataTags = self._mapCdataTags;
138
13967 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
140 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
141 regLine = /\r?\n/g;
142
14367 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14467 var lastLineIndex = 0, line = 1;
14567 var arrBlocks = self._arrBlocks;
146
14767 self.fire('start', {
148 pos: 0,
149 line: 1,
150 col: 1
151 });
152
15367 while((match = regTag.exec(html))){
154130 matchIndex = match.index;
155130 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15625 text = html.substring(lastIndex, matchIndex);
15725 if(tagCDATA){
15810 arrCDATA.push(text);
159 }
160 else{//文本
16115 saveBlock('text', text, lastIndex);
162 }
163 }
164130 lastIndex = regTag.lastIndex;
165
166130 if((tagName = match[1])){
16743 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
16811 text = arrCDATA.join('');
16911 saveBlock('cdata', text, lastCDATAIndex, {
170 'tagName': tagCDATA,
171 'attrs': attrsCDATA
172 });
17311 tagCDATA = null;
17411 attrsCDATA = null;
17511 arrCDATA = null;
176 }
17743 if(!tagCDATA){
178 //标签结束
17942 saveBlock('tagend', match[0], matchIndex, {
180 'tagName': tagName
181 });
18242 continue;
183 }
184 }
185
18688 if(tagCDATA){
1871 arrCDATA.push(match[0]);
188 }
189 else{
19087 if((tagName = match[4])){//标签开始
19174 arrAttrs = [];
19274 var attrs = match[5],
193 attrMatch,
194 attrMatchCount = 0;
19574 while((attrMatch = regAttr.exec(attrs))){
19661 var name = attrMatch[1],
197 quote = attrMatch[2] ? attrMatch[2] :
198 attrMatch[4] ? attrMatch[4] : '',
199 value = attrMatch[3] ? attrMatch[3] :
200 attrMatch[5] ? attrMatch[5] :
201 attrMatch[6] ? attrMatch[6] : '';
20261 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20361 attrMatchCount += attrMatch[0].length;
204 }
20574 if(attrMatchCount === attrs.length){
20673 saveBlock('tagstart', match[0], matchIndex, {
207 'tagName': tagName,
208 'attrs': arrAttrs,
209 'close': match[6]
210 });
21173 if(mapCdataTags[tagName]){
21211 tagCDATA = tagName;
21311 attrsCDATA = arrAttrs.concat();
21411 arrCDATA = [];
21511 lastCDATAIndex = lastIndex;
216 }
217 }
218 else{//如果出现漏匹配,则把当前内容匹配为text
2191 saveBlock('text', match[0], matchIndex);
220 }
221 }
22213 else if(match[2] || match[3]){//注释标签
22313 saveBlock('comment', match[0], matchIndex, {
224 'content': match[2] || match[3],
225 'long': match[2]?true:false
226 });
227 }
228 }
229 }
230
23167 if(html.length > lastIndex){
232 //结尾文本
2339 text = html.substring(lastIndex, html.length);
2349 saveBlock('text', text, lastIndex);
235 }
236
23767 self.fire('end', {
238 pos: lastIndex,
239 line: line,
240 col: lastIndex - lastLineIndex + 1
241 });
242
243 //存储区块
24467 function saveBlock(type, raw, pos, data){
245164 var col = pos - lastLineIndex + 1;
246164 if(data === undefined){
24725 data = {};
248 }
249164 data.raw = raw;
250164 data.pos = pos;
251164 data.line = line;
252164 data.col = col;
253164 arrBlocks.push(data);
254164 self.fire(type, data);
255164 var lineMatch;
256164 while((lineMatch = regLine.exec(raw))){
25718 line ++;
25818 lastLineIndex = pos + regLine.lastIndex;
259 }
260 }
261
262 },
263
264 // add event
265 addListener: function(types, listener){
26685 var _listeners = this._listeners;
26785 var arrTypes = types.split(/[,\s]/), type;
26885 for(var i=0, l = arrTypes.length;i<l;i++){
26988 type = arrTypes[i];
27088 if (_listeners[type] === undefined){
27184 _listeners[type] = [];
272 }
27388 _listeners[type].push(listener);
274 }
275 },
276
277 // fire event
278 fire: function(type, data){
279298 if (data === undefined){
2800 data = {};
281 }
282298 data.type = type;
283298 var self = this,
284 listeners = [],
285 listenersType = self._listeners[type],
286 listenersAll = self._listeners['all'];
287298 if (listenersType !== undefined){
28875 listeners = listeners.concat(listenersType);
289 }
290298 if (listenersAll !== undefined){
291112 listeners = listeners.concat(listenersAll);
292 }
293298 for (var i = 0, l = listeners.length; i < l; i++){
294183 listeners[i].call(self, data);
295 }
296 },
297
298 // remove event
299 removeListener: function(type, listener){
30013 var listenersType = this._listeners[type];
30113 if(listenersType !== undefined){
30211 for (var i = 0, l = listenersType.length; i < l; i++){
3038 if (listenersType[i] === listener){
3048 listenersType.splice(i, 1);
3058 break;
306 }
307 }
308 }
309 },
310
311 //fix pos if event.raw have \n
312 fixPos: function(event, index){
3133 var text = event.raw.substr(0, index);
3143 var arrLines = text.split(/\r?\n/),
315 lineCount = arrLines.length - 1,
316 line = event.line, col;
3173 if(lineCount > 0){
3181 line += lineCount;
3191 col = arrLines[lineCount].length + 1;
320 }
321 else{
3222 col = event.col + index;
323 }
3243 return {
325 line: line,
326 col: col
327 };
328 },
329
330 // covert array type of attrs to map
331 getMapAttrs: function(arrAttrs){
3326 var mapAttrs = {},
333 attr;
3346 for(var i=0,l=arrAttrs.length;i<l;i++){
3356 attr = arrAttrs[i];
3366 mapAttrs[attr.name] = attr.value;
337 }
3386 return mapAttrs;
339 }
340 };
341
3421 return HTMLParser;
343
344})();
345
3461if (typeof exports === 'object' && exports){
3471 exports.HTMLParser = HTMLParser;
348}
349/**
350 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
351 * MIT Licensed
352 */
3531HTMLHint.addRule({
354 id: 'attr-lowercase',
355 description: 'Attribute name must be lowercase.',
356 init: function(parser, reporter){
3573 var self = this;
3583 parser.addListener('tagstart', function(event){
3593 var attrs = event.attrs,
360 attr,
361 col = event.col + event.tagName.length + 1;
3623 for(var i=0, l=attrs.length;i<l;i++){
3633 attr = attrs[i];
3643 var attrName = attr.name;
3653 if(attrName !== attrName.toLowerCase()){
3662 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
367 }
368 }
369 });
370 }
371});
372/**
373 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
374 * MIT Licensed
375 */
3761HTMLHint.addRule({
377 id: 'attr-value-double-quotes',
378 description: 'Attribute value must closed by double quotes.',
379 init: function(parser, reporter){
3804 var self = this;
3814 parser.addListener('tagstart', function(event){
3824 var attrs = event.attrs,
383 attr,
384 col = event.col + event.tagName.length + 1;
3854 for(var i=0, l=attrs.length;i<l;i++){
3866 attr = attrs[i];
3876 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3882 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
389 }
390 }
391 });
392 }
393});
394/**
395 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
396 * MIT Licensed
397 */
3981HTMLHint.addRule({
399 id: 'attr-value-not-empty',
400 description: 'Attribute must set value.',
401 init: function(parser, reporter){
4023 var self = this;
4033 parser.addListener('tagstart', function(event){
4043 var attrs = event.attrs,
405 attr,
406 col = event.col + event.tagName.length + 1;
4073 for(var i=0, l=attrs.length;i<l;i++){
4083 attr = attrs[i];
4093 if(attr.quote === '' && attr.value === ''){
4101 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
411 }
412 }
413 });
414 }
415});
416/**
417 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
418 * MIT Licensed
419 */
4201HTMLHint.addRule({
421 id: 'csslint',
422 description: 'Scan css with csslint.',
423 init: function(parser, reporter, options){
4241 var self = this;
4251 parser.addListener('cdata', function(event){
4261 if(event.tagName.toLowerCase() === 'style'){
427
4281 var cssVerify;
429
4301 if(typeof exports === 'object' && require){
4311 cssVerify = require("csslint").CSSLint.verify;
432 }
433 else{
4340 cssVerify = CSSLint.verify;
435 }
436
4371 if(options !== undefined){
4381 var styleLine = event.line - 1,
439 styleCol = event.col - 1;
4401 try{
4411 var messages = cssVerify(event.raw, options).messages;
4421 messages.forEach(function(error){
4432 var line = error.line;
4442 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
445 });
446 }
447 catch(e){}
448 }
449
450 }
451 });
452 }
453});
454/**
455 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
456 * MIT Licensed
457 */
4581HTMLHint.addRule({
459 id: 'doctype-first',
460 description: 'Doctype must be first.',
461 init: function(parser, reporter){
4623 var self = this;
4633 var allEvent = function(event){
4646 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4653 return;
466 }
4673 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4682 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
469 }
4703 parser.removeListener('all', allEvent);
471 };
4723 parser.addListener('all', allEvent);
473 }
474});
475/**
476 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
477 * MIT Licensed
478 */
4791HTMLHint.addRule({
480 id: 'doctype-html5',
481 description: 'Doctype must be html5.',
482 init: function(parser, reporter){
4832 var self = this;
4842 function onComment(event){
4859 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4861 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
487 }
488 }
4892 function onTagStart(){
4902 parser.removeListener('comment', onComment);
4912 parser.removeListener('tagstart', onTagStart);
492 }
4932 parser.addListener('all', onComment);
4942 parser.addListener('tagstart', onTagStart);
495 }
496});
497/**
498 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
499 * MIT Licensed
500 */
5011HTMLHint.addRule({
502 id: 'head-script-disabled',
503 description: 'The script tag can not be used in head.',
504 init: function(parser, reporter){
5053 var self = this;
5063 function onTagStart(event){
5075 if(event.tagName.toLowerCase() === 'script'){
5082 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
509 }
510 }
5113 function onTagEnd(event){
5127 if(event.tagName.toLowerCase() === 'head'){
5133 parser.removeListener('tagstart', onTagStart);
5143 parser.removeListener('tagstart', onTagEnd);
515 }
516 }
5173 parser.addListener('tagstart', onTagStart);
5183 parser.addListener('tagend', onTagEnd);
519 }
520});
521/**
522 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
523 * MIT Licensed
524 */
5251HTMLHint.addRule({
526 id: 'id-class-value',
527 description: 'Id and class value must meet some rules.',
528 init: function(parser, reporter, options){
5298 var self = this;
5308 var arrRules = {
531 'underline': {
532 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
533 'message': 'Id and class value must lower case and split by underline.'
534 },
535 'dash': {
536 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
537 'message': 'Id and class value must lower case and split by dash.'
538 },
539 'hump': {
540 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
541 'message': 'Id and class value must meet hump style.'
542 }
543 }, rule;
5448 if(typeof options === 'string'){
5456 rule = arrRules[options];
546 }
547 else{
5482 rule = options;
549 }
5508 if(rule && rule.regId){
5518 var regId = rule.regId,
552 message = rule.message;
5538 parser.addListener('tagstart', function(event){
5548 var attrs = event.attrs,
555 attr,
556 col = event.col + event.tagName.length + 1;
5578 for(var i=0, l1=attrs.length;i<l1;i++){
55816 attr = attrs[i];
55916 if(attr.name.toLowerCase() === 'id'){
5608 if(regId.test(attr.value) === false){
5614 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
562 }
563 }
56416 if(attr.name.toLowerCase() === 'class'){
5658 var arrClass = attr.value.split(/\s+/g), classValue;
5668 for(var j=0, l2=arrClass.length;j<l2;j++){
5678 classValue = arrClass[j];
5688 if(classValue && regId.test(classValue) === false){
5694 reporter.warn(message, event.line, col + attr.index, self, classValue);
570 }
571 }
572 }
573 }
574 });
575 }
576 }
577});
578/**
579 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
580 * MIT Licensed
581 */
5821HTMLHint.addRule({
583 id: 'id-unique',
584 description: 'Id must be unique.',
585 init: function(parser, reporter){
5862 var self = this;
5872 var mapIdCount = {};
5882 parser.addListener('tagstart', function(event){
5894 var attrs = event.attrs,
590 attr,
591 id,
592 col = event.col + event.tagName.length + 1;
5934 for(var i=0, l=attrs.length;i<l;i++){
5944 attr = attrs[i];
5954 if(attr.name.toLowerCase() === 'id'){
5964 id = attr.value;
5974 if(id){
5984 if(mapIdCount[id] === undefined){
5993 mapIdCount[id] = 1;
600 }
601 else{
6021 mapIdCount[id] ++;
603 }
6044 if(mapIdCount[id] > 1){
6051 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
606 }
607 }
6084 break;
609 }
610 }
611 });
612 }
613});
614/**
615 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
616 * MIT Licensed
617 */
6181HTMLHint.addRule({
619 id: 'img-alt-require',
620 description: 'Alt of img tag must be set value.',
621 init: function(parser, reporter){
6223 var self = this;
6233 parser.addListener('tagstart', function(event){
6243 if(event.tagName.toLowerCase() === 'img'){
6253 var attrs = event.attrs;
6263 var haveAlt = false;
6273 for(var i=0, l=attrs.length;i<l;i++){
6288 if(attrs[i].name.toLowerCase() === 'alt'){
6292 haveAlt = true;
6302 break;
631 }
632 }
6333 if(haveAlt === false){
6341 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
635 }
636 }
637 });
638 }
639});
640/**
641 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
642 * MIT Licensed
643 */
6441HTMLHint.addRule({
645 id: 'jshint',
646 description: 'Scan script with jshint.',
647 init: function(parser, reporter, options){
6484 var self = this;
6494 parser.addListener('cdata', function(event){
6504 if(event.tagName.toLowerCase() === 'script'){
651
6524 var mapAttrs = parser.getMapAttrs(event.attrs),
653 type = mapAttrs.type;
654
655 // Only scan internal javascript
6564 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6572 return;
658 }
659
6602 var jsVerify;
661
6622 if(typeof exports === 'object' && require){
6632 jsVerify = require('jshint').JSHINT;
664 }
665 else{
6660 jsVerify = JSHINT;
667 }
668
6692 if(options !== undefined){
6702 var styleLine = event.line - 1,
671 styleCol = event.col - 1;
6722 var code = event.raw.replace(/\t/g,' ');
6732 try{
6742 var status = jsVerify(code, options);
6752 if(status === false){
6762 jsVerify.errors.forEach(function(error){
6778 var line = error.line;
6788 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
679 });
680 }
681 }
682 catch(e){}
683 }
684
685 }
686 });
687 }
688});
689/**
690 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
691 * MIT Licensed
692 */
6931HTMLHint.addRule({
694 id: 'spec-char-escape',
695 description: 'Special characters must be escaped.',
696 init: function(parser, reporter){
6973 var self = this;
6983 parser.addListener('text', function(event){
6993 var raw = event.raw,
700 reSpecChar = /[<>]/g,
701 match;
7023 while((match = reSpecChar.exec(raw))){
7033 var fixedPos = parser.fixPos(event, match.index);
7043 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
705 }
706 });
707 }
708});
709/**
710 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
711 * MIT Licensed
712 */
7131HTMLHint.addRule({
714 id: 'style-disabled',
715 description: 'Style tag can not be use.',
716 init: function(parser, reporter){
7172 var self = this;
7182 parser.addListener('tagstart', function(event){
7194 if(event.tagName.toLowerCase() === 'style'){
7201 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
721 }
722 });
723 }
724});
725/**
726 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
727 * MIT Licensed
728 */
7291HTMLHint.addRule({
730 id: 'tag-pair',
731 description: 'Tag must be paired.',
732 init: function(parser, reporter){
7334 var self = this;
7344 var stack=[],
735 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7364 parser.addListener('tagstart', function(event){
7375 var tagName = event.tagName.toLowerCase();
7385 if (mapEmptyTags[tagName] === undefined && !event.close){
7395 stack.push(tagName);
740 }
741 });
7424 parser.addListener('tagend', function(event){
7433 var tagName = event.tagName.toLowerCase();
744 //向上寻找匹配的开始标签
7453 for(var pos = stack.length-1;pos >= 0; pos--){
7463 if(stack[pos] === tagName){
7472 break;
748 }
749 }
7503 if(pos >= 0){
7512 var arrTags = [];
7522 for(var i=stack.length-1;i>pos;i--){
7531 arrTags.push('</'+stack[i]+'>');
754 }
7552 if(arrTags.length > 0){
7561 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
757 }
7582 stack.length=pos;
759 }
760 else{
7611 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
762 }
763 });
7644 parser.addListener('end', function(event){
7654 var arrTags = [];
7664 for(var i=stack.length-1;i>=0;i--){
7672 arrTags.push('</'+stack[i]+'>');
768 }
7694 if(arrTags.length > 0){
7702 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
771 }
772 });
773 }
774});
775/**
776 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
777 * MIT Licensed
778 */
7791HTMLHint.addRule({
780 id: 'tag-self-close',
781 description: 'The empty tag must closed by self.',
782 init: function(parser, reporter){
7832 var self = this;
7842 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7852 parser.addListener('tagstart', function(event){
7864 var tagName = event.tagName.toLowerCase();
7874 if(mapEmptyTags[tagName] !== undefined){
7884 if(!event.close){
7892 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
790 }
791 }
792 });
793 }
794});
795/**
796 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
797 * MIT Licensed
798 */
7991HTMLHint.addRule({
800 id: 'tagname-lowercase',
801 description: 'Tagname must be lowercase.',
802 init: function(parser, reporter){
8033 var self = this;
8043 parser.addListener('tagstart,tagend', function(event){
8059 var tagName = event.tagName;
8069 if(tagName !== tagName.toLowerCase()){
8074 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
808 }
809 });
810 }
811});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index ab53d4dcd..f951ca20f 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=[],a={};return a.version="0.9.2",a.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0},a.addRule=function(e){t[e.id]=e},a.verify=function(r,n){n===e&&(n=a.defaultRuleset);var i,s=new HTMLParser,o=new a.Reporter(r.split(/\r?\n/),n);for(var l in n)i=t[l],i!==e&&i.init(s,o,n[l]);return s.parse(r),o.messages},a}();"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,r,n){this.report("error",e,t,a,r,n)},warn:function(e,t,a,r,n){this.report("warning",e,t,a,r,n)},info:function(e,t,a,r,n){this.report("info",e,t,a,r,n)},report:function(e,t,a,r,n,i){var s=this;s.messages.push({type:e,message:t,raw:i,evidence:s.lines[a-1],line:a,col:r,rule:n})}},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(","),r=0;a.length>r;r++)t[a[r]]=!0;return t},parse:function(t){function a(t,a,r,n){var i=r-w+1;n===e&&(n={}),n.raw=a,n.pos=r,n.line=L,n.col=i,b.push(n),u.fire(t,n);for(var s;s=m.exec(a);)L++,w=r+m.lastIndex}var r,n,i,s,o,l,d,c,u=this,f=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\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});r=g.exec(t);)if(n=r.index,n>v&&(c=t.substring(v,n),o?d.push(c):a("text",c,v)),v=g.lastIndex,!(i=r[1])||(o&&i===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(r[0]);else if(i=r[4]){s=[];for(var H,y=r[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",N=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:N,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",r[0],n,{tagName:i,attrs:s,close:r[6]}),f[i]&&(o=i,l=s.concat(),d=[],h=v)):a("text",r[0],n)}else(r[2]||r[3])&&a("comment",r[0],n,{content:r[2]||r[3],"long":r[2]?!0:!1});else a("tagend",r[0],n,{tagName:i});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 r,n=this._listeners,i=t.split(/[,\s]/),s=0,o=i.length;o>s;s++)r=i[s],n[r]===e&&(n[r]=[]),n[r].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var r=this,n=[],i=r._listeners[t],s=r._listeners.all;i!==e&&(n=n.concat(i)),s!==e&&(n=n.concat(s));for(var o=0,l=n.length;l>o;o++)n[o].call(r,a)},removeListener:function(t,a){var r=this._listeners[t];if(r!==e)for(var n=0,i=r.length;i>n;n++)if(r[n]===a){r.splice(n,1);break}},fixPos:function(e,t){var a,r=e.raw.substr(0,t),n=r.split(/\r?\n/),i=n.length-1,s=e.line;return i>0?(s+=i,a=n[i].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},r=0,n=e.length;n>r;r++)t=e[r],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 r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++){r=n[s];var l=r.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,i+r.index,a,r.raw)}})}}),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 r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++)r=n[s],'"'===r.quote||""===r.value&&""!==(r.value===r.quote)||t.error("The value of attribute [ "+r.name+" ] must closed by double quotes.",e.line,i+r.index,a,r.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 r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++)r=n[s],""===r.quote&&""===r.value&&t.warn("The attribute [ "+r.name+" ] must set value.",e.line,i+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var n;if(n="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var i=e.line-1,s=e.col-1;try{var o=n(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,i+a,(1===a?s:0)+e.col,r,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,r=function(n){"start"===n.type||"text"===n.type&&/^\s*$/.test(n.raw)||(("comment"!==n.type&&n.long===!1||/^DOCTYPE\s+/i.test(n.content)===!1)&&t.error("Doctype must be first.",n.line,n.col,a,n.raw),e.removeListener("all",r))};e.addListener("all",r)}}),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,n,e.raw)}function r(){e.removeListener("comment",a),e.removeListener("tagstart",r)}var n=this;e.addListener("all",a),e.addListener("tagstart",r)}}),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,n,e.raw)}function r(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",r))}var n=this;e.addListener("tagstart",a),e.addListener("tagend",r)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var r,n=this,i={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(r="string"==typeof a?i[a]:a,r&&r.regId){var s=r.regId,o=r.message;e.addListener("tagstart",function(e){for(var a,r=e.attrs,i=e.col+e.tagName.length+1,l=0,d=r.length;d>l;l++)if(a=r[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,i+a.index,n,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,i+a.index,n,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,r={};e.addListener("tagstart",function(e){for(var n,i,s=e.attrs,o=e.col+e.tagName.length+1,l=0,d=s.length;d>l;l++)if(n=s[l],"id"===n.name.toLowerCase()){i=n.value,i&&(void 0===r[i]?r[i]=1:r[i]++,r[i]>1&&t.error("Id redefinition of [ "+i+" ].",e.line,o+n.index,a,n.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 r=e.attrs,n=!1,i=0,s=r.length;s>i;i++)if("alt"===r[i].name.toLowerCase()){n=!0;break}n===!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 r=this;e.addListener("cdata",function(n){if("script"===n.tagName.toLowerCase()){var i=e.getMapAttrs(n.attrs),s=i.type;if(void 0!==i.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=n.line-1,d=n.col-1,c=n.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,r,e.evidence)})}catch(f){}}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(r){for(var n,i=r.raw,s=/[<>]/g;n=s.exec(i);){var o=e.fixPos(r,n.index);t.error("Special characters must be escaped : [ "+n[0]+" ].",o.line,o.col,a,r.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,r=[],n=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!==n[t]||e.close||r.push(t)}),e.addListener("tagend",function(e){for(var n=e.tagName.toLowerCase(),i=r.length-1;i>=0&&r[i]!==n;i--);if(i>=0){for(var s=[],o=r.length-1;o>i;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),r.length=i}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 n=[],i=r.length-1;i>=0;i--)n.push("");n.length>0&&t.error("Tag must be paired, Missing: [ "+n.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,r=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var n=e.tagName.toLowerCase();void 0!==r[n]&&(e.close||t.warn("The empty tag : [ "+n+" ] 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 r=e.tagName;r!==r.toLowerCase()&&t.error("Tagname [ "+r+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file +var HTMLHint=function(e){var t=[],a={};return a.version="0.9.2",a.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0},a.addRule=function(e){t[e.id]=e},a.verify=function(r,i){i===e&&(i=a.defaultRuleset);var n,s=new HTMLParser,o=new a.Reporter(r.split(/\r?\n/),i);for(var l in i)n=t[l],n!==e&&n.init(s,o,i[l]);return s.parse(r),o.messages},a}();"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,r,i){this.report("error",e,t,a,r,i)},warn:function(e,t,a,r,i){this.report("warning",e,t,a,r,i)},info:function(e,t,a,r,i){this.report("info",e,t,a,r,i)},report:function(e,t,a,r,i,n){var s=this;s.messages.push({type:e,message:t,raw:n,evidence:s.lines[a-1],line:a,col:r,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(","),r=0;a.length>r;r++)t[a[r]]=!0;return t},parse:function(t){function a(t,a,r,i){var n=r-w+1;i===e&&(i={}),i.raw=a,i.pos=r,i.line=L,i.col=n,b.push(i),u.fire(t,i);for(var s;s=m.exec(a);)L++,w=r+m.lastIndex}var r,i,n,s,o,l,d,c,u=this,g=u._mapCdataTags,f=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\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});r=f.exec(t);)if(i=r.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=f.lastIndex,!(n=r[1])||(o&&n===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(r[0]);else if(n=r[4]){s=[];for(var H,y=r[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",N=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:N,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",r[0],i,{tagName:n,attrs:s,close:r[6]}),g[n]&&(o=n,l=s.concat(),d=[],h=v)):a("text",r[0],i)}else(r[2]||r[3])&&a("comment",r[0],i,{content:r[2]||r[3],"long":r[2]?!0:!1});else a("tagend",r[0],i,{tagName:n});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 r,i=this._listeners,n=t.split(/[,\s]/),s=0,o=n.length;o>s;s++)r=n[s],i[r]===e&&(i[r]=[]),i[r].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var r=this,i=[],n=r._listeners[t],s=r._listeners.all;n!==e&&(i=i.concat(n)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(r,a)},removeListener:function(t,a){var r=this._listeners[t];if(r!==e)for(var i=0,n=r.length;n>i;i++)if(r[i]===a){r.splice(i,1);break}},fixPos:function(e,t){var a,r=e.raw.substr(0,t),i=r.split(/\r?\n/),n=i.length-1,s=e.line;return n>0?(s+=n,a=i[n].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},r=0,i=e.length;i>r;r++)t=e[r],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 r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){r=i[s];var l=r.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,n+r.index,a,r.raw)}})}}),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 r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],'"'===r.quote||""===r.value&&""!==(r.value===r.quote)||t.error("The value of attribute [ "+r.name+" ] must closed by double quotes.",e.line,n+r.index,a,r.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 r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],""===r.quote&&""===r.value&&t.warn("The attribute [ "+r.name+" ] must set value.",e.line,n+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var r=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 n=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.message,n+a,(1===a?s:0)+e.col,r,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,r=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",r))};e.addListener("all",r)}}),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 r(){e.removeListener("comment",a),e.removeListener("tagstart",r)}var i=this;e.addListener("all",a),e.addListener("tagstart",r)}}),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 r(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",r))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",r)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var r,i=this,n={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(r="string"==typeof a?n[a]:a,r&&r.regId){var s=r.regId,o=r.message;e.addListener("tagstart",function(e){for(var a,r=e.attrs,n=e.col+e.tagName.length+1,l=0,d=r.length;d>l;l++)if(a=r[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,n+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)c=u[g],c&&s.test(c)===!1&&t.warn(o,e.line,n+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,r={};e.addListener("tagstart",function(e){for(var i,n,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()){n=i.value,n&&(void 0===r[n]?r[n]=1:r[n]++,r[n]>1&&t.error("Id redefinition of [ "+n+" ].",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 r=e.attrs,i=!1,n=0,s=r.length;s>n;n++)if("alt"===r[n].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 r=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var n=e.getMapAttrs(i.attrs),s=n.type;if(void 0!==n.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,r,e.evidence)})}catch(g){}}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(r){for(var i,n=r.raw,s=/[<>]/g;i=s.exec(n);){var o=e.fixPos(r,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,r.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,r=[],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||r.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),n=r.length-1;n>=0&&r[n]!==i;n--);if(n>=0){for(var s=[],o=r.length-1;o>n;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),r.length=n}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=[],n=r.length-1;n>=0;n--)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,r=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!==r[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 r=e.tagName;r!==r.toLowerCase()&&t.error("Tagname [ "+r+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file diff --git a/src/reporter.js b/src/reporter.js index 4d0ffb841..49ee3f7d7 100644 --- a/src/reporter.js +++ b/src/reporter.js @@ -38,7 +38,11 @@ evidence: self.lines[line-1], line: line, col: col, - rule: rule + rule: { + id: rule.id, + description: rule.description, + link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id + } }); } };