diff --git a/coverage.html b/coverage.html index 226b31620..8aae09db4 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%
304
299
5

htmlhint.js

98%
304
299
5
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){
2837 if(ruleset === undefined){
290 ruleset = HTMLHint.defaultRuleset;
30 }
3137 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3437 var rule;
3537 for (var id in ruleset){
3637 rule = rules[id];
3737 if (rule !== undefined){
3837 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4237 parser.parse(html);
43
4437 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(){
6137 var self = this;
6237 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6737 var self = this;
6837 self.lines = lines;
6937 self.ruleset = ruleset;
7037 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7414 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7820 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){
8634 var self = this;
8734 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(){
10959 var self = this;
11059 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11559 var self = this;
11659 self._listeners = {};
11759 self._mapCdataTags = self.makeMap("script,style");
11859 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12264 var obj = {}, items = str.split(",");
12364 for ( var i = 0; i < items.length; i++ ){
124188 obj[ items[i] ] = true;
125 }
12664 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13259 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13559 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13959 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, arrCDATA, lastCDATAIndex = 0, text;
14059 var lastLineIndex = 0, line = 1;
14159 var arrBlocks = self._arrBlocks;
142
14359 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14959 while((match = regTag.exec(html))){
150112 matchIndex = match.index;
151112 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15218 text = html.substring(lastIndex, matchIndex);
15318 if(tagCDATA){
1546 arrCDATA.push(text);
155 }
156 else{//文本
15712 saveBlock('text', text, lastIndex);
158 }
159 }
160112 lastIndex = regTag.lastIndex;
161
162112 if((tagName = match[1])){
16335 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
1648 text = arrCDATA.join('');
1658 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA
167 });
1688 tagCDATA = null;
1698 arrCDATA = null;
170 }
17135 if(!tagCDATA){
172 //标签结束
17335 saveBlock('tagend', match[0], matchIndex, {
174 'tagName': tagName
175 });
17635 continue;
177 }
178 }
179
18077 if(tagCDATA){
1810 arrCDATA.push(match[0]);
182 }
183 else{
18477 if((tagName = match[4])){//标签开始
18564 arrAttrs = [];
18664 var attrs = match[5],
187 attrMatch,
188 attrMatchCount = 0;
18964 while((attrMatch = regAttr.exec(attrs))){
19048 var name = attrMatch[1],
191 quote = attrMatch[2] ? attrMatch[2] :
192 attrMatch[4] ? attrMatch[4] : '',
193 value = attrMatch[3] ? attrMatch[3] :
194 attrMatch[5] ? attrMatch[5] :
195 attrMatch[6] ? attrMatch[6] : '';
19648 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
19748 attrMatchCount += attrMatch[0].length;
198 }
19964 if(attrMatchCount === attrs.length){
20063 saveBlock('tagstart', match[0], matchIndex, {
201 'tagName': tagName,
202 'attrs': arrAttrs,
203 'close': match[6]
204 });
20563 if(mapCdataTags[tagName]){
2068 tagCDATA = tagName;
2078 arrCDATA = [];
2088 lastCDATAIndex = lastIndex;
209 }
210 }
211 else{//如果出现漏匹配,则把当前内容匹配为text
2121 saveBlock('text', match[0], matchIndex);
213 }
214 }
21513 else if(match[2] || match[3]){//注释标签
21613 saveBlock('comment', match[0], matchIndex, {
217 'content': match[2] || match[3],
218 'long': match[2]?true:false
219 });
220 }
221 }
222 }
223
22459 if(html.length > lastIndex){
225 //结尾文本
2266 text = html.substring(lastIndex, html.length);
2276 saveBlock('text', text, lastIndex);
228 }
229
23059 self.fire('end', {
231 pos: lastIndex,
232 line: line,
233 col: lastIndex - lastLineIndex + 1
234 });
235
236 //存储区块
23759 function saveBlock(type, raw, pos, data){
238138 var col = pos - lastLineIndex + 1;
239138 if(data === undefined){
24019 data = {};
241 }
242138 data.raw = raw;
243138 data.pos = pos;
244138 data.line = line;
245138 data.col = col;
246138 arrBlocks.push(data);
247138 self.fire(type, data);
248138 var lineMatch;
249138 while((lineMatch = regLine.exec(raw))){
2509 line ++;
2519 lastLineIndex = pos + regLine.lastIndex;
252 }
253 }
254
255 },
256
257 // add event
258 addListener: function(types, listener){
25970 var _listeners = this._listeners;
26070 var arrTypes = types.split(/[,\s]/), type;
26170 for(var i=0, l = arrTypes.length;i<l;i++){
26272 type = arrTypes[i];
26372 if (_listeners[type] === undefined){
26472 _listeners[type] = [];
265 }
26672 _listeners[type].push(listener);
267 }
268 },
269
270 // fire event
271 fire: function(type, data){
272256 if (data === undefined){
2730 data = {};
274 }
275256 data.type = type;
276256 var self = this,
277 listeners = [],
278 listenersType = self._listeners[type],
279 listenersAll = self._listeners['all'];
280256 if (listenersType !== undefined){
28164 listeners = listeners.concat(listenersType);
282 }
283256 if (listenersAll !== undefined){
284109 listeners = listeners.concat(listenersAll);
285 }
286256 for (var i = 0, l = listeners.length; i < l; i++){
287167 listeners[i].call(self, data);
288 }
289 },
290
291 // remove event
292 removeListener: function(type, listener){
29312 var listenersType = this._listeners[type];
29412 if(listenersType !== undefined){
29510 for (var i = 0, l = listenersType.length; i < l; i++){
2967 if (listenersType[i] === listener){
2977 listenersType.splice(i, 1);
2987 break;
299 }
300 }
301 }
302 },
303
304 //fix pos if event.raw have \n
305 fixPos: function(event, index){
3063 var text = event.raw.substr(0, index);
3073 var arrLines = text.split(/\r?\n/),
308 lineCount = arrLines.length - 1,
309 line = event.line, col;
3103 if(lineCount > 0){
3111 line += lineCount;
3121 col = arrLines[lineCount].length + 1;
313 }
314 else{
3152 col = event.col + index;
316 }
3173 return {
318 line: line,
319 col: col
320 };
321
322 }
323 };
324
3251 return HTMLParser;
326
327})();
328
3291if (typeof exports === 'object' && exports){
3301 exports.HTMLParser = HTMLParser;
331}
332/**
333 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
334 * MIT Licensed
335 */
3361HTMLHint.addRule({
337 id: 'attr-lowercase',
338 description: 'Attribute name must be lowercase.',
339 init: function(parser, reporter){
3402 var self = this;
3412 parser.addListener('tagstart', function(event){
3422 var attrs = event.attrs,
343 attr,
344 col = event.col + event.tagName.length + 1;
3452 for(var i=0, l=attrs.length;i<l;i++){
3462 attr = attrs[i];
3472 var attrName = attr.name;
3482 if(attrName !== attrName.toLowerCase()){
3491 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
350 }
351 }
352 });
353 }
354});
355/**
356 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
357 * MIT Licensed
358 */
3591HTMLHint.addRule({
360 id: 'attr-value-double-quotes',
361 description: 'Attribute value must closed by double quotes.',
362 init: function(parser, reporter){
3633 var self = this;
3643 parser.addListener('tagstart', function(event){
3653 var attrs = event.attrs,
366 attr,
367 col = event.col + event.tagName.length + 1;
3683 for(var i=0, l=attrs.length;i<l;i++){
3695 attr = attrs[i];
3705 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3712 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
372 }
373 }
374 });
375 }
376});
377/**
378 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
379 * MIT Licensed
380 */
3811HTMLHint.addRule({
382 id: 'attr-value-not-empty',
383 description: 'Attribute must set value.',
384 init: function(parser, reporter){
3853 var self = this;
3863 parser.addListener('tagstart', function(event){
3873 var attrs = event.attrs,
388 attr,
389 col = event.col + event.tagName.length + 1;
3903 for(var i=0, l=attrs.length;i<l;i++){
3913 attr = attrs[i];
3923 if(attr.quote === '' && attr.value === ''){
3931 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
394 }
395 }
396 });
397 }
398});
399/**
400 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
401 * MIT Licensed
402 */
4031HTMLHint.addRule({
404 id: 'csslint',
405 description: 'Scan css with csslint.',
406 init: function(parser, reporter, options){
4071 var self = this;
4081 parser.addListener('cdata', function(event){
4091 if(event.tagName.toLowerCase() === 'style'){
410
4111 var cssVerify = options.verify,
412 cssOptions = options.options;
413
4141 if(cssVerify !== undefined && cssOptions !== undefined){
4151 var styleLine = event.line - 1,
416 styleCol = event.col - 1;
4171 var messages = cssVerify(event.raw, cssOptions).messages;
4181 messages.forEach(function(error){
4192 var line = error.line;
4202 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
421 });
422 }
423
424 }
425 });
426 }
427});
428/**
429 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
430 * MIT Licensed
431 */
4321HTMLHint.addRule({
433 id: 'doctype-first',
434 description: 'Doctype must be first.',
435 init: function(parser, reporter){
4362 var self = this;
4372 var allEvent = function(event){
4384 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4392 return;
440 }
4412 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4421 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
443 }
4442 parser.removeListener('all', allEvent);
445 };
4462 parser.addListener('all', allEvent);
447 }
448});
449/**
450 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
451 * MIT Licensed
452 */
4531HTMLHint.addRule({
454 id: 'doctype-html5',
455 description: 'Doctype must be html5.',
456 init: function(parser, reporter){
4572 var self = this;
4582 function onComment(event){
4599 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4601 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
461 }
462 }
4632 function onTagStart(){
4642 parser.removeListener('comment', onComment);
4652 parser.removeListener('tagstart', onTagStart);
466 }
4672 parser.addListener('all', onComment);
4682 parser.addListener('tagstart', onTagStart);
469 }
470});
471/**
472 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
473 * MIT Licensed
474 */
4751HTMLHint.addRule({
476 id: 'head-script-disabled',
477 description: 'The script tag can not be used in head.',
478 init: function(parser, reporter){
4793 var self = this;
4803 function onTagStart(event){
4815 if(event.tagName.toLowerCase() === 'script'){
4822 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
483 }
484 }
4853 function onTagEnd(event){
4867 if(event.tagName.toLowerCase() === 'head'){
4873 parser.removeListener('tagstart', onTagStart);
4883 parser.removeListener('tagstart', onTagEnd);
489 }
490 }
4913 parser.addListener('tagstart', onTagStart);
4923 parser.addListener('tagend', onTagEnd);
493 }
494});
495/**
496 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
497 * MIT Licensed
498 */
4991HTMLHint.addRule({
500 id: 'id-class-value',
501 description: 'Id and class value must meet some rules.',
502 init: function(parser, reporter, options){
5036 var self = this;
5046 var arrRules = {
505 'underline': {
506 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
507 'message': 'Id and class value must lower case and split by underline.'
508 },
509 'dash': {
510 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
511 'message': 'Id and class value must lower case and split by dash.'
512 },
513 'hump': {
514 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
515 'message': 'Id and class value must meet hump style.'
516 }
517 }, rule;
5186 if(typeof options === 'string'){
5196 rule = arrRules[options];
520 }
521 else{
5220 rule = options;
523 }
5246 if(rule && rule.regId){
5256 var regId = rule.regId,
526 message = rule.message;
5276 parser.addListener('tagstart', function(event){
5286 var attrs = event.attrs,
529 attr,
530 col = event.col + event.tagName.length + 1;
5316 for(var i=0, l1=attrs.length;i<l1;i++){
53212 attr = attrs[i];
53312 if(attr.name.toLowerCase() === 'id'){
5346 if(regId.test(attr.value) === false){
5353 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
536 }
537 }
53812 if(attr.name.toLowerCase() === 'class'){
5396 var arrClass = attr.value.split(/\s+/g), classValue;
5406 for(var j=0, l2=arrClass.length;j<l2;j++){
5416 classValue = arrClass[j];
5426 if(classValue && regId.test(classValue) === false){
5433 reporter.warn(message, event.line, col + attr.index, self, classValue);
544 }
545 }
546 }
547 }
548 });
549 }
550 }
551});
552/**
553 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
554 * MIT Licensed
555 */
5561HTMLHint.addRule({
557 id: 'img-alt-require',
558 description: 'Alt of img tag must be set value.',
559 init: function(parser, reporter){
5603 var self = this;
5613 parser.addListener('tagstart', function(event){
5623 if(event.tagName.toLowerCase() === 'img'){
5633 var attrs = event.attrs;
5643 var haveAlt = false;
5653 for(var i=0, l=attrs.length;i<l;i++){
5668 if(attrs[i].name.toLowerCase() === 'alt'){
5672 haveAlt = true;
5682 break;
569 }
570 }
5713 if(haveAlt === false){
5721 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
573 }
574 }
575 });
576 }
577});
578/**
579 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
580 * MIT Licensed
581 */
5821HTMLHint.addRule({
583 id: 'jshint',
584 description: 'Scan script with jshint.',
585 init: function(parser, reporter, options){
5861 var self = this;
5871 parser.addListener('cdata', function(event){
5881 if(event.tagName.toLowerCase() === 'script'){
589
5901 var jsVerify = options.verify,
591 jsOptions = options.options;
592
5931 if(jsVerify !== undefined && jsOptions !== undefined){
5941 var styleLine = event.line - 1,
595 styleCol = event.col - 1;
5961 var status = jsVerify(event.raw, jsOptions);
5971 if(status === false){
5981 jsVerify.errors.forEach(function(error){
5994 var line = error.line;
6004 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
601 });
602 }
603 }
604
605 }
606 });
607 }
608});
609/**
610 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
611 * MIT Licensed
612 */
6131HTMLHint.addRule({
614 id: 'spec-char-escape',
615 description: 'Special characters must be escaped.',
616 init: function(parser, reporter){
6172 var self = this;
6182 parser.addListener('text', function(event){
6193 var raw = event.raw,
620 reSpecChar = /[<>]/g,
621 match;
6223 while((match = reSpecChar.exec(raw))){
6233 var fixedPos = parser.fixPos(event, match.index);
6243 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
625 }
626 });
627 }
628});
629/**
630 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
631 * MIT Licensed
632 */
6331HTMLHint.addRule({
634 id: 'style-disabled',
635 description: 'Style tag can not be use.',
636 init: function(parser, reporter){
6372 var self = this;
6382 parser.addListener('tagstart', function(event){
6394 if(event.tagName.toLowerCase() === 'style'){
6401 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
641 }
642 });
643 }
644});
645/**
646 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
647 * MIT Licensed
648 */
6491HTMLHint.addRule({
650 id: 'tag-pair',
651 description: 'Tag must be paired.',
652 init: function(parser, reporter){
6533 var self = this;
6543 var stack=[],
655 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6563 parser.addListener('tagstart', function(event){
6574 var tagName = event.tagName.toLowerCase();
6584 if (mapEmptyTags[tagName] === undefined && !event.close){
6594 stack.push(tagName);
660 }
661 });
6623 parser.addListener('tagend', function(event){
6633 var tagName = event.tagName.toLowerCase();
664 //向上寻找匹配的开始标签
6653 for(var pos = stack.length-1;pos >= 0; pos--){
6663 if(stack[pos] === tagName){
6672 break;
668 }
669 }
6703 if(pos >= 0){
6712 var arrTags = [];
6722 for(var i=stack.length-1;i>pos;i--){
6731 arrTags.push('</'+stack[i]+'>');
674 }
6752 if(arrTags.length > 0){
6761 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
677 }
6782 stack.length=pos;
679 }
680 else{
6811 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
682 }
683 });
6843 parser.addListener('end', function(event){
6853 var arrTags = [];
6863 for(var i=stack.length-1;i>=0;i--){
6871 arrTags.push('</'+stack[i]+'>');
688 }
6893 if(arrTags.length > 0){
6901 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
691 }
692 });
693 }
694});
695/**
696 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
697 * MIT Licensed
698 */
6991HTMLHint.addRule({
700 id: 'tag-self-close',
701 description: 'The empty tag must closed by self.',
702 init: function(parser, reporter){
7032 var self = this;
7042 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7052 parser.addListener('tagstart', function(event){
7064 var tagName = event.tagName.toLowerCase();
7074 if(mapEmptyTags[tagName] !== undefined){
7084 if(!event.close){
7092 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
710 }
711 }
712 });
713 }
714});
715/**
716 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
717 * MIT Licensed
718 */
7191HTMLHint.addRule({
720 id: 'tagname-lowercase',
721 description: 'Tagname must be lowercase.',
722 init: function(parser, reporter){
7232 var self = this;
7242 parser.addListener('tagstart,tagend', function(event){
7258 var tagName = event.tagName;
7268 if(tagName !== tagName.toLowerCase()){
7274 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
728 }
729 });
730 }
731});
\ No newline at end of file +

Coverage

98%
305
300
5

htmlhint.js

98%
305
300
5
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){
2837 if(ruleset === undefined){
290 ruleset = HTMLHint.defaultRuleset;
30 }
3137 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3437 var rule;
3537 for (var id in ruleset){
3637 rule = rules[id];
3737 if (rule !== undefined){
3837 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4237 parser.parse(html);
43
4437 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(){
6137 var self = this;
6237 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6737 var self = this;
6837 self.lines = lines;
6937 self.ruleset = ruleset;
7037 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7414 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7820 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){
8634 var self = this;
8734 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(){
10959 var self = this;
11059 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11559 var self = this;
11659 self._listeners = {};
11759 self._mapCdataTags = self.makeMap("script,style");
11859 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12264 var obj = {}, items = str.split(",");
12364 for ( var i = 0; i < items.length; i++ ){
124188 obj[ items[i] ] = true;
125 }
12664 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13259 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13559 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13959 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, arrCDATA, lastCDATAIndex = 0, text;
14059 var lastLineIndex = 0, line = 1;
14159 var arrBlocks = self._arrBlocks;
142
14359 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14959 while((match = regTag.exec(html))){
150112 matchIndex = match.index;
151112 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15218 text = html.substring(lastIndex, matchIndex);
15318 if(tagCDATA){
1546 arrCDATA.push(text);
155 }
156 else{//文本
15712 saveBlock('text', text, lastIndex);
158 }
159 }
160112 lastIndex = regTag.lastIndex;
161
162112 if((tagName = match[1])){
16335 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
1648 text = arrCDATA.join('');
1658 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA
167 });
1688 tagCDATA = null;
1698 arrCDATA = null;
170 }
17135 if(!tagCDATA){
172 //标签结束
17335 saveBlock('tagend', match[0], matchIndex, {
174 'tagName': tagName
175 });
17635 continue;
177 }
178 }
179
18077 if(tagCDATA){
1810 arrCDATA.push(match[0]);
182 }
183 else{
18477 if((tagName = match[4])){//标签开始
18564 arrAttrs = [];
18664 var attrs = match[5],
187 attrMatch,
188 attrMatchCount = 0;
18964 while((attrMatch = regAttr.exec(attrs))){
19048 var name = attrMatch[1],
191 quote = attrMatch[2] ? attrMatch[2] :
192 attrMatch[4] ? attrMatch[4] : '',
193 value = attrMatch[3] ? attrMatch[3] :
194 attrMatch[5] ? attrMatch[5] :
195 attrMatch[6] ? attrMatch[6] : '';
19648 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
19748 attrMatchCount += attrMatch[0].length;
198 }
19964 if(attrMatchCount === attrs.length){
20063 saveBlock('tagstart', match[0], matchIndex, {
201 'tagName': tagName,
202 'attrs': arrAttrs,
203 'close': match[6]
204 });
20563 if(mapCdataTags[tagName]){
2068 tagCDATA = tagName;
2078 arrCDATA = [];
2088 lastCDATAIndex = lastIndex;
209 }
210 }
211 else{//如果出现漏匹配,则把当前内容匹配为text
2121 saveBlock('text', match[0], matchIndex);
213 }
214 }
21513 else if(match[2] || match[3]){//注释标签
21613 saveBlock('comment', match[0], matchIndex, {
217 'content': match[2] || match[3],
218 'long': match[2]?true:false
219 });
220 }
221 }
222 }
223
22459 if(html.length > lastIndex){
225 //结尾文本
2266 text = html.substring(lastIndex, html.length);
2276 saveBlock('text', text, lastIndex);
228 }
229
23059 self.fire('end', {
231 pos: lastIndex,
232 line: line,
233 col: lastIndex - lastLineIndex + 1
234 });
235
236 //存储区块
23759 function saveBlock(type, raw, pos, data){
238138 var col = pos - lastLineIndex + 1;
239138 if(data === undefined){
24019 data = {};
241 }
242138 data.raw = raw;
243138 data.pos = pos;
244138 data.line = line;
245138 data.col = col;
246138 arrBlocks.push(data);
247138 self.fire(type, data);
248138 var lineMatch;
249138 while((lineMatch = regLine.exec(raw))){
2509 line ++;
2519 lastLineIndex = pos + regLine.lastIndex;
252 }
253 }
254
255 },
256
257 // add event
258 addListener: function(types, listener){
25970 var _listeners = this._listeners;
26070 var arrTypes = types.split(/[,\s]/), type;
26170 for(var i=0, l = arrTypes.length;i<l;i++){
26272 type = arrTypes[i];
26372 if (_listeners[type] === undefined){
26472 _listeners[type] = [];
265 }
26672 _listeners[type].push(listener);
267 }
268 },
269
270 // fire event
271 fire: function(type, data){
272256 if (data === undefined){
2730 data = {};
274 }
275256 data.type = type;
276256 var self = this,
277 listeners = [],
278 listenersType = self._listeners[type],
279 listenersAll = self._listeners['all'];
280256 if (listenersType !== undefined){
28164 listeners = listeners.concat(listenersType);
282 }
283256 if (listenersAll !== undefined){
284109 listeners = listeners.concat(listenersAll);
285 }
286256 for (var i = 0, l = listeners.length; i < l; i++){
287167 listeners[i].call(self, data);
288 }
289 },
290
291 // remove event
292 removeListener: function(type, listener){
29312 var listenersType = this._listeners[type];
29412 if(listenersType !== undefined){
29510 for (var i = 0, l = listenersType.length; i < l; i++){
2967 if (listenersType[i] === listener){
2977 listenersType.splice(i, 1);
2987 break;
299 }
300 }
301 }
302 },
303
304 //fix pos if event.raw have \n
305 fixPos: function(event, index){
3063 var text = event.raw.substr(0, index);
3073 var arrLines = text.split(/\r?\n/),
308 lineCount = arrLines.length - 1,
309 line = event.line, col;
3103 if(lineCount > 0){
3111 line += lineCount;
3121 col = arrLines[lineCount].length + 1;
313 }
314 else{
3152 col = event.col + index;
316 }
3173 return {
318 line: line,
319 col: col
320 };
321
322 }
323 };
324
3251 return HTMLParser;
326
327})();
328
3291if (typeof exports === 'object' && exports){
3301 exports.HTMLParser = HTMLParser;
331}
332/**
333 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
334 * MIT Licensed
335 */
3361HTMLHint.addRule({
337 id: 'attr-lowercase',
338 description: 'Attribute name must be lowercase.',
339 init: function(parser, reporter){
3402 var self = this;
3412 parser.addListener('tagstart', function(event){
3422 var attrs = event.attrs,
343 attr,
344 col = event.col + event.tagName.length + 1;
3452 for(var i=0, l=attrs.length;i<l;i++){
3462 attr = attrs[i];
3472 var attrName = attr.name;
3482 if(attrName !== attrName.toLowerCase()){
3491 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
350 }
351 }
352 });
353 }
354});
355/**
356 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
357 * MIT Licensed
358 */
3591HTMLHint.addRule({
360 id: 'attr-value-double-quotes',
361 description: 'Attribute value must closed by double quotes.',
362 init: function(parser, reporter){
3633 var self = this;
3643 parser.addListener('tagstart', function(event){
3653 var attrs = event.attrs,
366 attr,
367 col = event.col + event.tagName.length + 1;
3683 for(var i=0, l=attrs.length;i<l;i++){
3695 attr = attrs[i];
3705 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3712 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
372 }
373 }
374 });
375 }
376});
377/**
378 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
379 * MIT Licensed
380 */
3811HTMLHint.addRule({
382 id: 'attr-value-not-empty',
383 description: 'Attribute must set value.',
384 init: function(parser, reporter){
3853 var self = this;
3863 parser.addListener('tagstart', function(event){
3873 var attrs = event.attrs,
388 attr,
389 col = event.col + event.tagName.length + 1;
3903 for(var i=0, l=attrs.length;i<l;i++){
3913 attr = attrs[i];
3923 if(attr.quote === '' && attr.value === ''){
3931 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
394 }
395 }
396 });
397 }
398});
399/**
400 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
401 * MIT Licensed
402 */
4031HTMLHint.addRule({
404 id: 'csslint',
405 description: 'Scan css with csslint.',
406 init: function(parser, reporter, options){
4071 var self = this;
4081 parser.addListener('cdata', function(event){
4091 if(event.tagName.toLowerCase() === 'style'){
410
4111 var cssVerify = options.verify,
412 cssOptions = options.options;
413
4141 if(cssVerify !== undefined && cssOptions !== undefined){
4151 var styleLine = event.line - 1,
416 styleCol = event.col - 1;
4171 var messages = cssVerify(event.raw, cssOptions).messages;
4181 messages.forEach(function(error){
4192 var line = error.line;
4202 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
421 });
422 }
423
424 }
425 });
426 }
427});
428/**
429 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
430 * MIT Licensed
431 */
4321HTMLHint.addRule({
433 id: 'doctype-first',
434 description: 'Doctype must be first.',
435 init: function(parser, reporter){
4362 var self = this;
4372 var allEvent = function(event){
4384 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4392 return;
440 }
4412 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4421 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
443 }
4442 parser.removeListener('all', allEvent);
445 };
4462 parser.addListener('all', allEvent);
447 }
448});
449/**
450 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
451 * MIT Licensed
452 */
4531HTMLHint.addRule({
454 id: 'doctype-html5',
455 description: 'Doctype must be html5.',
456 init: function(parser, reporter){
4572 var self = this;
4582 function onComment(event){
4599 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4601 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
461 }
462 }
4632 function onTagStart(){
4642 parser.removeListener('comment', onComment);
4652 parser.removeListener('tagstart', onTagStart);
466 }
4672 parser.addListener('all', onComment);
4682 parser.addListener('tagstart', onTagStart);
469 }
470});
471/**
472 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
473 * MIT Licensed
474 */
4751HTMLHint.addRule({
476 id: 'head-script-disabled',
477 description: 'The script tag can not be used in head.',
478 init: function(parser, reporter){
4793 var self = this;
4803 function onTagStart(event){
4815 if(event.tagName.toLowerCase() === 'script'){
4822 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
483 }
484 }
4853 function onTagEnd(event){
4867 if(event.tagName.toLowerCase() === 'head'){
4873 parser.removeListener('tagstart', onTagStart);
4883 parser.removeListener('tagstart', onTagEnd);
489 }
490 }
4913 parser.addListener('tagstart', onTagStart);
4923 parser.addListener('tagend', onTagEnd);
493 }
494});
495/**
496 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
497 * MIT Licensed
498 */
4991HTMLHint.addRule({
500 id: 'id-class-value',
501 description: 'Id and class value must meet some rules.',
502 init: function(parser, reporter, options){
5036 var self = this;
5046 var arrRules = {
505 'underline': {
506 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
507 'message': 'Id and class value must lower case and split by underline.'
508 },
509 'dash': {
510 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
511 'message': 'Id and class value must lower case and split by dash.'
512 },
513 'hump': {
514 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
515 'message': 'Id and class value must meet hump style.'
516 }
517 }, rule;
5186 if(typeof options === 'string'){
5196 rule = arrRules[options];
520 }
521 else{
5220 rule = options;
523 }
5246 if(rule && rule.regId){
5256 var regId = rule.regId,
526 message = rule.message;
5276 parser.addListener('tagstart', function(event){
5286 var attrs = event.attrs,
529 attr,
530 col = event.col + event.tagName.length + 1;
5316 for(var i=0, l1=attrs.length;i<l1;i++){
53212 attr = attrs[i];
53312 if(attr.name.toLowerCase() === 'id'){
5346 if(regId.test(attr.value) === false){
5353 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
536 }
537 }
53812 if(attr.name.toLowerCase() === 'class'){
5396 var arrClass = attr.value.split(/\s+/g), classValue;
5406 for(var j=0, l2=arrClass.length;j<l2;j++){
5416 classValue = arrClass[j];
5426 if(classValue && regId.test(classValue) === false){
5433 reporter.warn(message, event.line, col + attr.index, self, classValue);
544 }
545 }
546 }
547 }
548 });
549 }
550 }
551});
552/**
553 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
554 * MIT Licensed
555 */
5561HTMLHint.addRule({
557 id: 'img-alt-require',
558 description: 'Alt of img tag must be set value.',
559 init: function(parser, reporter){
5603 var self = this;
5613 parser.addListener('tagstart', function(event){
5623 if(event.tagName.toLowerCase() === 'img'){
5633 var attrs = event.attrs;
5643 var haveAlt = false;
5653 for(var i=0, l=attrs.length;i<l;i++){
5668 if(attrs[i].name.toLowerCase() === 'alt'){
5672 haveAlt = true;
5682 break;
569 }
570 }
5713 if(haveAlt === false){
5721 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
573 }
574 }
575 });
576 }
577});
578/**
579 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
580 * MIT Licensed
581 */
5821HTMLHint.addRule({
583 id: 'jshint',
584 description: 'Scan script with jshint.',
585 init: function(parser, reporter, options){
5861 var self = this;
5871 parser.addListener('cdata', function(event){
5881 if(event.tagName.toLowerCase() === 'script'){
589
5901 var jsVerify = options.verify,
591 jsOptions = options.options;
592
5931 if(jsVerify !== undefined && jsOptions !== undefined){
5941 var styleLine = event.line - 1,
595 styleCol = event.col - 1;
5961 var code = event.raw.replace(/\t/g,' ');
5971 var status = jsVerify(code, jsOptions);
5981 if(status === false){
5991 jsVerify.errors.forEach(function(error){
6004 var line = error.line;
6014 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
602 });
603 }
604 }
605
606 }
607 });
608 }
609});
610/**
611 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
612 * MIT Licensed
613 */
6141HTMLHint.addRule({
615 id: 'spec-char-escape',
616 description: 'Special characters must be escaped.',
617 init: function(parser, reporter){
6182 var self = this;
6192 parser.addListener('text', function(event){
6203 var raw = event.raw,
621 reSpecChar = /[<>]/g,
622 match;
6233 while((match = reSpecChar.exec(raw))){
6243 var fixedPos = parser.fixPos(event, match.index);
6253 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
626 }
627 });
628 }
629});
630/**
631 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
632 * MIT Licensed
633 */
6341HTMLHint.addRule({
635 id: 'style-disabled',
636 description: 'Style tag can not be use.',
637 init: function(parser, reporter){
6382 var self = this;
6392 parser.addListener('tagstart', function(event){
6404 if(event.tagName.toLowerCase() === 'style'){
6411 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
642 }
643 });
644 }
645});
646/**
647 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
648 * MIT Licensed
649 */
6501HTMLHint.addRule({
651 id: 'tag-pair',
652 description: 'Tag must be paired.',
653 init: function(parser, reporter){
6543 var self = this;
6553 var stack=[],
656 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6573 parser.addListener('tagstart', function(event){
6584 var tagName = event.tagName.toLowerCase();
6594 if (mapEmptyTags[tagName] === undefined && !event.close){
6604 stack.push(tagName);
661 }
662 });
6633 parser.addListener('tagend', function(event){
6643 var tagName = event.tagName.toLowerCase();
665 //向上寻找匹配的开始标签
6663 for(var pos = stack.length-1;pos >= 0; pos--){
6673 if(stack[pos] === tagName){
6682 break;
669 }
670 }
6713 if(pos >= 0){
6722 var arrTags = [];
6732 for(var i=stack.length-1;i>pos;i--){
6741 arrTags.push('</'+stack[i]+'>');
675 }
6762 if(arrTags.length > 0){
6771 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
678 }
6792 stack.length=pos;
680 }
681 else{
6821 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
683 }
684 });
6853 parser.addListener('end', function(event){
6863 var arrTags = [];
6873 for(var i=stack.length-1;i>=0;i--){
6881 arrTags.push('</'+stack[i]+'>');
689 }
6903 if(arrTags.length > 0){
6911 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
692 }
693 });
694 }
695});
696/**
697 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
698 * MIT Licensed
699 */
7001HTMLHint.addRule({
701 id: 'tag-self-close',
702 description: 'The empty tag must closed by self.',
703 init: function(parser, reporter){
7042 var self = this;
7052 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7062 parser.addListener('tagstart', function(event){
7074 var tagName = event.tagName.toLowerCase();
7084 if(mapEmptyTags[tagName] !== undefined){
7094 if(!event.close){
7102 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
711 }
712 }
713 });
714 }
715});
716/**
717 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
718 * MIT Licensed
719 */
7201HTMLHint.addRule({
721 id: 'tagname-lowercase',
722 description: 'Tagname must be lowercase.',
723 init: function(parser, reporter){
7242 var self = this;
7252 parser.addListener('tagstart,tagend', function(event){
7268 var tagName = event.tagName;
7278 if(tagName !== tagName.toLowerCase()){
7284 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
729 }
730 });
731 }
732});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 7df856f6b..4d3f9ca37 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(n,r){r===e&&(r=a.defaultRuleset);var i,s=new HTMLParser,o=new a.Reporter(n.split(/\r?\n/),r);for(var l in r)i=t[l],i!==e&&i.init(s,o,r[l]);return s.parse(n),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,n,r){this.report("error",e,t,a,n,r)},warn:function(e,t,a,n,r){this.report("warning",e,t,a,n,r)},info:function(e,t,a,n,r){this.report("info",e,t,a,n,r)},report:function(e,t,a,n,r,i){var s=this;s.messages.push({type:e,message:t,raw:i,evidence:s.lines[a-1],line:a,col:n,rule:r})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,r){var i=n-h+1;r===e&&(r={}),r.raw=a,r.pos=n,r.line=w,r.col=i,L.push(r),c.fire(t,r);for(var s;s=m.exec(a);)w++,h=n+m.lastIndex}var n,r,i,s,o,l,d,c=this,u=c._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,f=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,m=/\r?\n/g,p=0,v=0,h=0,w=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(r=n.index,r>p&&(d=t.substring(p,r),o?l.push(d):a("text",d,p)),p=g.lastIndex,!(i=n[1])||(o&&i===o&&(d=l.join(""),a("cdata",d,v,{tagName:o}),o=null,l=null),o))if(o)l.push(n[0]);else if(i=n[4]){s=[];for(var b,H=n[5],y=0;b=f.exec(H);){var T=b[1],x=b[2]?b[2]:b[4]?b[4]:"",M=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:T,value:M,quote:x,index:b.index,raw:b[0]}),y+=b[0].length}y===H.length?(a("tagstart",n[0],r,{tagName:i,attrs:s,close:n[6]}),u[i]&&(o=i,l=[],v=p)):a("text",n[0],r)}else(n[2]||n[3])&&a("comment",n[0],r,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],r,{tagName:i});t.length>p&&(d=t.substring(p,t.length),a("text",d,p)),c.fire("end",{pos:p,line:w,col:p-h+1})},addListener:function(t,a){for(var n,r=this._listeners,i=t.split(/[,\s]/),s=0,o=i.length;o>s;s++)n=i[s],r[n]===e&&(r[n]=[]),r[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,r=[],i=n._listeners[t],s=n._listeners.all;i!==e&&(r=r.concat(i)),s!==e&&(r=r.concat(s));for(var o=0,l=r.length;l>o;o++)r[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var r=0,i=n.length;i>r;r++)if(n[r]===a){n.splice(r,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),r=n.split(/\r?\n/),i=r.length-1,s=e.line;return i>0?(s+=i,a=r[i].length+1):a=e.col+t,{line:s,col:a}}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++){n=r[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,i+n.index,a,n.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 n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],'"'===n.quote||""===n.value&&""!==(n.value===n.quote)||t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=r(e.raw,i).messages;l.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,s+a,(1===a?o:0)+e.col,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=function(r){"start"===r.type||"text"===r.type&&/^\s*$/.test(r.raw)||(("comment"!==r.type&&r.long===!1||/^DOCTYPE\s+/i.test(r.content)===!1)&&t.error("Doctype must be first.",r.line,r.col,a,r.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,r,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var r=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,r,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var r=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,r=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(n="string"==typeof a?i[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,i=e.col+e.tagName.length+1,l=0,d=n.length;d>l;l++)if(a=n[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,i+a.index,r,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,i+a.index,r,c)})}}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var n=e.attrs,r=!1,i=0,s=n.length;s>i;i++)if("alt"===n[i].name.toLowerCase()){r=!0;break}r===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=r(e.raw,i);l===!1&&r.errors.forEach(function(e){var a=e.line;t.warn(e.reason,s+a,(1===a?o:0)+e.character,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var r,i=n.raw,s=/[<>]/g;r=s.exec(i);){var o=e.fixPos(n,r.index);t.error("Special characters must be escaped : [ "+r[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,n=[],r=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!==r[t]||e.close||n.push(t)}),e.addListener("tagend",function(e){for(var r=e.tagName.toLowerCase(),i=n.length-1;i>=0&&n[i]!==r;i--);if(i>=0){for(var s=[],o=n.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),n.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 r=[],i=n.length-1;i>=0;i--)r.push("");r.length>0&&t.error("Tag must be paired, Missing: [ "+r.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,n=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var r=e.tagName.toLowerCase();void 0!==n[r]&&(e.close||t.warn("The empty tag : [ "+r+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var n=e.tagName;n!==n.toLowerCase()&&t.error("Tagname [ "+n+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file +var HTMLHint=function(e){var t=[],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(n,r){r===e&&(r=a.defaultRuleset);var i,s=new HTMLParser,o=new a.Reporter(n.split(/\r?\n/),r);for(var l in r)i=t[l],i!==e&&i.init(s,o,r[l]);return s.parse(n),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,n,r){this.report("error",e,t,a,n,r)},warn:function(e,t,a,n,r){this.report("warning",e,t,a,n,r)},info:function(e,t,a,n,r){this.report("info",e,t,a,n,r)},report:function(e,t,a,n,r,i){var s=this;s.messages.push({type:e,message:t,raw:i,evidence:s.lines[a-1],line:a,col:n,rule:r})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,r){var i=n-h+1;r===e&&(r={}),r.raw=a,r.pos=n,r.line=w,r.col=i,L.push(r),c.fire(t,r);for(var s;s=p.exec(a);)w++,h=n+p.lastIndex}var n,r,i,s,o,l,d,c=this,u=c._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,f=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,p=/\r?\n/g,m=0,v=0,h=0,w=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(r=n.index,r>m&&(d=t.substring(m,r),o?l.push(d):a("text",d,m)),m=g.lastIndex,!(i=n[1])||(o&&i===o&&(d=l.join(""),a("cdata",d,v,{tagName:o}),o=null,l=null),o))if(o)l.push(n[0]);else if(i=n[4]){s=[];for(var b,H=n[5],y=0;b=f.exec(H);){var T=b[1],x=b[2]?b[2]:b[4]?b[4]:"",M=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:T,value:M,quote:x,index:b.index,raw:b[0]}),y+=b[0].length}y===H.length?(a("tagstart",n[0],r,{tagName:i,attrs:s,close:n[6]}),u[i]&&(o=i,l=[],v=m)):a("text",n[0],r)}else(n[2]||n[3])&&a("comment",n[0],r,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],r,{tagName:i});t.length>m&&(d=t.substring(m,t.length),a("text",d,m)),c.fire("end",{pos:m,line:w,col:m-h+1})},addListener:function(t,a){for(var n,r=this._listeners,i=t.split(/[,\s]/),s=0,o=i.length;o>s;s++)n=i[s],r[n]===e&&(r[n]=[]),r[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,r=[],i=n._listeners[t],s=n._listeners.all;i!==e&&(r=r.concat(i)),s!==e&&(r=r.concat(s));for(var o=0,l=r.length;l>o;o++)r[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var r=0,i=n.length;i>r;r++)if(n[r]===a){n.splice(r,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),r=n.split(/\r?\n/),i=r.length-1,s=e.line;return i>0?(s+=i,a=r[i].length+1):a=e.col+t,{line:s,col:a}}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++){n=r[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,i+n.index,a,n.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 n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],'"'===n.quote||""===n.value&&""!==(n.value===n.quote)||t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=r(e.raw,i).messages;l.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,s+a,(1===a?o:0)+e.col,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=function(r){"start"===r.type||"text"===r.type&&/^\s*$/.test(r.raw)||(("comment"!==r.type&&r.long===!1||/^DOCTYPE\s+/i.test(r.content)===!1)&&t.error("Doctype must be first.",r.line,r.col,a,r.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,r,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var r=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,r,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var r=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,r=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(n="string"==typeof a?i[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,i=e.col+e.tagName.length+1,l=0,d=n.length;d>l;l++)if(a=n[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,i+a.index,r,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,i+a.index,r,c)})}}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var n=e.attrs,r=!1,i=0,s=n.length;s>i;i++)if("alt"===n[i].name.toLowerCase()){r=!0;break}r===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=e.raw.replace(/\t/g," "),d=r(l,i);d===!1&&r.errors.forEach(function(e){var a=e.line;t.warn(e.reason,s+a,(1===a?o:0)+e.character,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var r,i=n.raw,s=/[<>]/g;r=s.exec(i);){var o=e.fixPos(n,r.index);t.error("Special characters must be escaped : [ "+r[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,n=[],r=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!==r[t]||e.close||n.push(t)}),e.addListener("tagend",function(e){for(var r=e.tagName.toLowerCase(),i=n.length-1;i>=0&&n[i]!==r;i--);if(i>=0){for(var s=[],o=n.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),n.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 r=[],i=n.length-1;i>=0;i--)r.push("");r.length>0&&t.error("Tag must be paired, Missing: [ "+r.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,n=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var r=e.tagName.toLowerCase();void 0!==n[r]&&(e.close||t.warn("The empty tag : [ "+r+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var n=e.tagName;n!==n.toLowerCase()&&t.error("Tagname [ "+n+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file diff --git a/src/rules/jshint.js b/src/rules/jshint.js index 51191f88b..a62396f1e 100644 --- a/src/rules/jshint.js +++ b/src/rules/jshint.js @@ -16,7 +16,8 @@ HTMLHint.addRule({ if(jsVerify !== undefined && jsOptions !== undefined){ var styleLine = event.line - 1, styleCol = event.col - 1; - var status = jsVerify(event.raw, jsOptions); + var code = event.raw.replace(/\t/g,' '); + var status = jsVerify(code, jsOptions); if(status === false){ jsVerify.errors.forEach(function(error){ var line = error.line;