From 6bd3eae317601bb6b12ce2dbb41bde44250b2b5e Mon Sep 17 00:00:00 2001 From: Yanis Wang Date: Tue, 2 Apr 2013 15:26:00 +0800 Subject: [PATCH] add rule: id-unique --- coverage.html | 2 +- lib/htmlhint.js | 2 +- src/rules/id-unique.js | 36 ++++++++++++++++++++++++++++++++++++ test/rules/id-unique.spec.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/rules/id-unique.js create mode 100644 test/rules/id-unique.spec.js diff --git a/coverage.html b/coverage.html index a0100f39a..ac7c76340 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%
323
319
4

htmlhint.js

98%
323
319
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){
2415 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2843 if(ruleset === undefined){
291 ruleset = HTMLHint.defaultRuleset;
30 }
3143 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3443 var rule;
3543 for (var id in ruleset){
3648 rule = rules[id];
3748 if (rule !== undefined){
3848 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4243 parser.parse(html);
43
4443 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(){
6143 var self = this;
6243 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6743 var self = this;
6843 self.lines = lines;
6943 self.ruleset = ruleset;
7043 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7417 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){
8643 var self = this;
8743 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(){
10965 var self = this;
11065 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11565 var self = this;
11665 self._listeners = {};
11765 self._mapCdataTags = self.makeMap("script,style");
11865 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12271 var obj = {}, items = str.split(",");
12371 for ( var i = 0; i < items.length; i++ ){
124214 obj[ items[i] ] = true;
125 }
12671 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13265 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13565 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13965 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14065 var lastLineIndex = 0, line = 1;
14165 var arrBlocks = self._arrBlocks;
142
14365 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14965 while((match = regTag.exec(html))){
150122 matchIndex = match.index;
151122 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 }
160122 lastIndex = regTag.lastIndex;
161
162122 if((tagName = match[1])){
16339 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 }
17339 if(!tagCDATA){
174 //标签结束
17538 saveBlock('tagend', match[0], matchIndex, {
176 'tagName': tagName
177 });
17838 continue;
179 }
180 }
181
18284 if(tagCDATA){
1831 arrCDATA.push(match[0]);
184 }
185 else{
18683 if((tagName = match[4])){//标签开始
18770 arrAttrs = [];
18870 var attrs = match[5],
189 attrMatch,
190 attrMatchCount = 0;
19170 while((attrMatch = regAttr.exec(attrs))){
19257 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] : '';
19857 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
19957 attrMatchCount += attrMatch[0].length;
200 }
20170 if(attrMatchCount === attrs.length){
20269 saveBlock('tagstart', match[0], matchIndex, {
203 'tagName': tagName,
204 'attrs': arrAttrs,
205 'close': match[6]
206 });
20769 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
22765 if(html.length > lastIndex){
228 //结尾文本
2299 text = html.substring(lastIndex, html.length);
2309 saveBlock('text', text, lastIndex);
231 }
232
23365 self.fire('end', {
234 pos: lastIndex,
235 line: line,
236 col: lastIndex - lastLineIndex + 1
237 });
238
239 //存储区块
24065 function saveBlock(type, raw, pos, data){
241156 var col = pos - lastLineIndex + 1;
242156 if(data === undefined){
24325 data = {};
244 }
245156 data.raw = raw;
246156 data.pos = pos;
247156 data.line = line;
248156 data.col = col;
249156 arrBlocks.push(data);
250156 self.fire(type, data);
251156 var lineMatch;
252156 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){
26283 var _listeners = this._listeners;
26383 var arrTypes = types.split(/[,\s]/), type;
26483 for(var i=0, l = arrTypes.length;i<l;i++){
26586 type = arrTypes[i];
26686 if (_listeners[type] === undefined){
26782 _listeners[type] = [];
268 }
26986 _listeners[type].push(listener);
270 }
271 },
272
273 // fire event
274 fire: function(type, data){
275286 if (data === undefined){
2760 data = {};
277 }
278286 data.type = type;
279286 var self = this,
280 listeners = [],
281 listenersType = self._listeners[type],
282 listenersAll = self._listeners['all'];
283286 if (listenersType !== undefined){
28471 listeners = listeners.concat(listenersType);
285 }
286286 if (listenersAll !== undefined){
287112 listeners = listeners.concat(listenersAll);
288 }
289286 for (var i = 0, l = listeners.length; i < l; i++){
290179 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: 'img-alt-require',
580 description: 'Alt of img tag must be set value.',
581 init: function(parser, reporter){
5823 var self = this;
5833 parser.addListener('tagstart', function(event){
5843 if(event.tagName.toLowerCase() === 'img'){
5853 var attrs = event.attrs;
5863 var haveAlt = false;
5873 for(var i=0, l=attrs.length;i<l;i++){
5888 if(attrs[i].name.toLowerCase() === 'alt'){
5892 haveAlt = true;
5902 break;
591 }
592 }
5933 if(haveAlt === false){
5941 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
595 }
596 }
597 });
598 }
599});
600/**
601 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
602 * MIT Licensed
603 */
6041HTMLHint.addRule({
605 id: 'jshint',
606 description: 'Scan script with jshint.',
607 init: function(parser, reporter, options){
6084 var self = this;
6094 parser.addListener('cdata', function(event){
6104 if(event.tagName.toLowerCase() === 'script'){
611
6124 var mapAttrs = parser.getMapAttrs(event.attrs),
613 type = mapAttrs.type;
614
615 // Only scan internal javascript
6164 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6172 return;
618 }
619
6202 var jsVerify;
621
6222 if(typeof exports === 'object' && require){
6232 jsVerify = require('jshint').JSHINT;
624 }
625 else{
6260 jsVerify = JSHINT;
627 }
628
6292 if(options !== undefined){
6302 var styleLine = event.line - 1,
631 styleCol = event.col - 1;
6322 var code = event.raw.replace(/\t/g,' ');
6332 try{
6342 var status = jsVerify(code, options);
6352 if(status === false){
6362 jsVerify.errors.forEach(function(error){
6378 var line = error.line;
6388 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
639 });
640 }
641 }
642 catch(e){}
643 }
644
645 }
646 });
647 }
648});
649/**
650 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
651 * MIT Licensed
652 */
6531HTMLHint.addRule({
654 id: 'spec-char-escape',
655 description: 'Special characters must be escaped.',
656 init: function(parser, reporter){
6573 var self = this;
6583 parser.addListener('text', function(event){
6593 var raw = event.raw,
660 reSpecChar = /[<>]/g,
661 match;
6623 while((match = reSpecChar.exec(raw))){
6633 var fixedPos = parser.fixPos(event, match.index);
6643 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
665 }
666 });
667 }
668});
669/**
670 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
671 * MIT Licensed
672 */
6731HTMLHint.addRule({
674 id: 'style-disabled',
675 description: 'Style tag can not be use.',
676 init: function(parser, reporter){
6772 var self = this;
6782 parser.addListener('tagstart', function(event){
6794 if(event.tagName.toLowerCase() === 'style'){
6801 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
681 }
682 });
683 }
684});
685/**
686 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
687 * MIT Licensed
688 */
6891HTMLHint.addRule({
690 id: 'tag-pair',
691 description: 'Tag must be paired.',
692 init: function(parser, reporter){
6934 var self = this;
6944 var stack=[],
695 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6964 parser.addListener('tagstart', function(event){
6975 var tagName = event.tagName.toLowerCase();
6985 if (mapEmptyTags[tagName] === undefined && !event.close){
6995 stack.push(tagName);
700 }
701 });
7024 parser.addListener('tagend', function(event){
7033 var tagName = event.tagName.toLowerCase();
704 //向上寻找匹配的开始标签
7053 for(var pos = stack.length-1;pos >= 0; pos--){
7063 if(stack[pos] === tagName){
7072 break;
708 }
709 }
7103 if(pos >= 0){
7112 var arrTags = [];
7122 for(var i=stack.length-1;i>pos;i--){
7131 arrTags.push('</'+stack[i]+'>');
714 }
7152 if(arrTags.length > 0){
7161 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
717 }
7182 stack.length=pos;
719 }
720 else{
7211 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
722 }
723 });
7244 parser.addListener('end', function(event){
7254 var arrTags = [];
7264 for(var i=stack.length-1;i>=0;i--){
7272 arrTags.push('</'+stack[i]+'>');
728 }
7294 if(arrTags.length > 0){
7302 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
731 }
732 });
733 }
734});
735/**
736 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
737 * MIT Licensed
738 */
7391HTMLHint.addRule({
740 id: 'tag-self-close',
741 description: 'The empty tag must closed by self.',
742 init: function(parser, reporter){
7432 var self = this;
7442 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7452 parser.addListener('tagstart', function(event){
7464 var tagName = event.tagName.toLowerCase();
7474 if(mapEmptyTags[tagName] !== undefined){
7484 if(!event.close){
7492 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
750 }
751 }
752 });
753 }
754});
755/**
756 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
757 * MIT Licensed
758 */
7591HTMLHint.addRule({
760 id: 'tagname-lowercase',
761 description: 'Tagname must be lowercase.',
762 init: function(parser, reporter){
7633 var self = this;
7643 parser.addListener('tagstart,tagend', function(event){
7659 var tagName = event.tagName;
7669 if(tagName !== tagName.toLowerCase()){
7674 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
768 }
769 });
770 }
771});
\ 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){
7417 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7827 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.warn('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 diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 6581da452..68e82fbc7 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,c,d,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(n=r.index,n>v&&(d=t.substring(v,n),o?c.push(d):a("text",d,v)),v=f.lastIndex,!(i=r[1])||(o&&i===o&&(d=c.join(""),a("cdata",d,h,{tagName:o,attrs:l}),o=null,l=null,c=null),o))if(o)c.push(r[0]);else if(i=r[4]){s=[];for(var y,H=r[5],T=0;y=p.exec(H);){var x=y[1],M=y[2]?y[2]:y[4]?y[4]:"",N=y[3]?y[3]:y[5]?y[5]:y[6]?y[6]:"";s.push({name:x,value:N,quote:M,index:y.index,raw:y[0]}),T+=y[0].length}T===H.length?(a("tagstart",r[0],n,{tagName:i,attrs:s,close:r[6]}),g[i]&&(o=i,l=s.concat(),c=[],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&&(d=t.substring(v,t.length),a("text",d,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,c=r.length;c>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 d,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)d=u[g],d&&s.test(d)===!1&&t.warn(o,e.line,i+a.index,n,d)})}}}),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,c=n.col-1,d=n.raw.replace(/\t/g," ");try{var u=o(d,a);u===!1&&o.errors.forEach(function(e){var a=e.line;t.warn(e.reason,l+a,(1===a?c: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 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,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.warn("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 diff --git a/src/rules/id-unique.js b/src/rules/id-unique.js new file mode 100644 index 000000000..a879eca83 --- /dev/null +++ b/src/rules/id-unique.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2013, Yanis Wang + * MIT Licensed + */ +HTMLHint.addRule({ + id: 'id-unique', + description: 'Id must be unique.', + init: function(parser, reporter){ + var self = this; + var mapIdCount = {}; + parser.addListener('tagstart', function(event){ + var attrs = event.attrs, + attr, + id, + col = event.col + event.tagName.length + 1; + for(var i=0, l=attrs.length;i 1){ + reporter.warn('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw); + } + } + break; + } + } + }); + } +}); \ No newline at end of file diff --git a/test/rules/id-unique.spec.js b/test/rules/id-unique.spec.js new file mode 100644 index 000000000..64cdd45e6 --- /dev/null +++ b/test/rules/id-unique.spec.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2013, Yanis Wang + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../../index").HTMLHint; + +describe('Rules: id-unique', function(){ + + it('Id redefine should result in an error', function(){ + var code = '
'; + var messages = HTMLHint.verify(code, {'id-unique': true}); + expect(messages.length).to.be(1); + expect(messages[0].rule.id).to.be('id-unique'); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(26); + expect(messages[0].type).to.be('warning'); + }); + + it('Id no redefine should not result in an error', function(){ + var code = '
'; + var messages = HTMLHint.verify(code, {'id-unique': true}); + expect(messages.length).to.be(0); + }); + +}); \ No newline at end of file