Coverage

98%
347
342
5

htmlhint.js

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