diff --git a/CHANGE.md b/CHANGE.md index 36822e483..aa6159b9d 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -1,6 +1,16 @@ HTMLHint change log ==================== +## ver 0.9.3 (2013-4-8) + +add: + +1. add ruleid to csslint message + +fix: + +1. fix csslint rule: del undefined of raw + ## ver 0.9.2 (2013-4-6) add: diff --git a/bin/htmlhint b/bin/htmlhint index a6f86aefe..3d37e12bf 100644 --- a/bin/htmlhint +++ b/bin/htmlhint @@ -26,7 +26,7 @@ program.on('--help', function(){ }); program - .version('0.9.2') + .version('0.9.3') .usage('[options] ') .option('-l, --list', 'show all of the rules available.') .option('-c, --config ', 'custom configuration file.') diff --git a/coverage.html b/coverage.html index 77abbdd57..a9bab030f 100644 --- a/coverage.html +++ b/coverage.html @@ -338,4 +338,4 @@ code .string { color: #5890AD } code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

98%
339
335
4

htmlhint.js

98%
339
335
4
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var 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 };
23
241 HTMLHint.addRule = function(rule){
2516 HTMLHint.rules[rule.id] = rule;
26 };
27
281 HTMLHint.verify = function(html, ruleset){
2945 if(ruleset === undefined){
301 ruleset = HTMLHint.defaultRuleset;
31 }
3245 var parser = new HTMLParser(),
33 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
34
3545 var rules = HTMLHint.rules,
36 rule;
3745 for (var id in ruleset){
3851 rule = rules[id];
3951 if (rule !== undefined){
4051 rule.init(parser, reporter, ruleset[id]);
41 }
42 }
43
4445 parser.parse(html);
45
4645 return reporter.messages;
47 };
48
491 return HTMLHint;
50
51})();
52
531if (typeof exports === 'object' && exports){
541 exports.HTMLHint = HTMLHint;
55}
56/**
57 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
58 * MIT Licensed
59 */
601(function(HTMLHint, undefined){
61
621 var Reporter = function(){
6345 var self = this;
6445 self._init.apply(self,arguments);
65 };
66
671 Reporter.prototype = {
68 _init: function(lines, ruleset){
6945 var self = this;
7045 self.lines = lines;
7145 self.ruleset = ruleset;
7245 self.messages = [];
73 },
74 //错误
75 error: function(message, line, col, rule, raw){
7618 this.report('error', message, line, col, rule, raw);
77 },
78 //警告
79 warn: function(message, line, col, rule, raw){
8026 this.report('warning', message, line, col, rule, raw);
81 },
82 //信息
83 info: function(message, line, col, rule, raw){
840 this.report('info', message, line, col, rule, raw);
85 },
86 //报告
87 report: function(type, message, line, col, rule, raw){
8844 var self = this;
8944 self.messages.push({
90 type: type,
91 message: message,
92 raw: raw,
93 evidence: self.lines[line-1],
94 line: line,
95 col: col,
96 rule: {
97 id: rule.id,
98 description: rule.description,
99 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
100 }
101 });
102 }
103 };
104
1051 HTMLHint.Reporter = Reporter;
106
107})(HTMLHint);
108/**
109 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
110 * MIT Licensed
111 */
1121var HTMLParser = (function(undefined){
113
1141 var HTMLParser = function(){
11567 var self = this;
11667 self._init.apply(self,arguments);
117 };
118
1191 HTMLParser.prototype = {
120 _init: function(){
12167 var self = this;
12267 self._listeners = {};
12367 self._mapCdataTags = self.makeMap("script,style");
12467 self._arrBlocks = [];
125 },
126
127 makeMap: function(str){
12873 var obj = {}, items = str.split(",");
12973 for ( var i = 0; i < items.length; i++ ){
130218 obj[ items[i] ] = true;
131 }
13273 return obj;
133 },
134
135 // parse html code
136 parse: function(html){
137
13867 var self = this,
139 mapCdataTags = self._mapCdataTags;
140
14167 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
142 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
143 regLine = /\r?\n/g;
144
14567 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14667 var lastLineIndex = 0, line = 1;
14767 var arrBlocks = self._arrBlocks;
148
14967 self.fire('start', {
150 pos: 0,
151 line: 1,
152 col: 1
153 });
154
15567 while((match = regTag.exec(html))){
156130 matchIndex = match.index;
157130 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15825 text = html.substring(lastIndex, matchIndex);
15925 if(tagCDATA){
16010 arrCDATA.push(text);
161 }
162 else{//文本
16315 saveBlock('text', text, lastIndex);
164 }
165 }
166130 lastIndex = regTag.lastIndex;
167
168130 if((tagName = match[1])){
16943 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
17011 text = arrCDATA.join('');
17111 saveBlock('cdata', text, lastCDATAIndex, {
172 'tagName': tagCDATA,
173 'attrs': attrsCDATA
174 });
17511 tagCDATA = null;
17611 attrsCDATA = null;
17711 arrCDATA = null;
178 }
17943 if(!tagCDATA){
180 //标签结束
18142 saveBlock('tagend', match[0], matchIndex, {
182 'tagName': tagName
183 });
18442 continue;
185 }
186 }
187
18888 if(tagCDATA){
1891 arrCDATA.push(match[0]);
190 }
191 else{
19287 if((tagName = match[4])){//标签开始
19374 arrAttrs = [];
19474 var attrs = match[5],
195 attrMatch,
196 attrMatchCount = 0;
19774 while((attrMatch = regAttr.exec(attrs))){
19861 var name = attrMatch[1],
199 quote = attrMatch[2] ? attrMatch[2] :
200 attrMatch[4] ? attrMatch[4] : '',
201 value = attrMatch[3] ? attrMatch[3] :
202 attrMatch[5] ? attrMatch[5] :
203 attrMatch[6] ? attrMatch[6] : '';
20461 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20561 attrMatchCount += attrMatch[0].length;
206 }
20774 if(attrMatchCount === attrs.length){
20873 saveBlock('tagstart', match[0], matchIndex, {
209 'tagName': tagName,
210 'attrs': arrAttrs,
211 'close': match[6]
212 });
21373 if(mapCdataTags[tagName]){
21411 tagCDATA = tagName;
21511 attrsCDATA = arrAttrs.concat();
21611 arrCDATA = [];
21711 lastCDATAIndex = lastIndex;
218 }
219 }
220 else{//如果出现漏匹配,则把当前内容匹配为text
2211 saveBlock('text', match[0], matchIndex);
222 }
223 }
22413 else if(match[2] || match[3]){//注释标签
22513 saveBlock('comment', match[0], matchIndex, {
226 'content': match[2] || match[3],
227 'long': match[2]?true:false
228 });
229 }
230 }
231 }
232
23367 if(html.length > lastIndex){
234 //结尾文本
2359 text = html.substring(lastIndex, html.length);
2369 saveBlock('text', text, lastIndex);
237 }
238
23967 self.fire('end', {
240 pos: lastIndex,
241 line: line,
242 col: lastIndex - lastLineIndex + 1
243 });
244
245 //存储区块
24667 function saveBlock(type, raw, pos, data){
247164 var col = pos - lastLineIndex + 1;
248164 if(data === undefined){
24925 data = {};
250 }
251164 data.raw = raw;
252164 data.pos = pos;
253164 data.line = line;
254164 data.col = col;
255164 arrBlocks.push(data);
256164 self.fire(type, data);
257164 var lineMatch;
258164 while((lineMatch = regLine.exec(raw))){
25918 line ++;
26018 lastLineIndex = pos + regLine.lastIndex;
261 }
262 }
263
264 },
265
266 // add event
267 addListener: function(types, listener){
26886 var _listeners = this._listeners;
26986 var arrTypes = types.split(/[,\s]/), type;
27086 for(var i=0, l = arrTypes.length;i<l;i++){
27189 type = arrTypes[i];
27289 if (_listeners[type] === undefined){
27384 _listeners[type] = [];
274 }
27589 _listeners[type].push(listener);
276 }
277 },
278
279 // fire event
280 fire: function(type, data){
281298 if (data === undefined){
2820 data = {};
283 }
284298 data.type = type;
285298 var self = this,
286 listeners = [],
287 listenersType = self._listeners[type],
288 listenersAll = self._listeners['all'];
289298 if (listenersType !== undefined){
29075 listeners = listeners.concat(listenersType);
291 }
292298 if (listenersAll !== undefined){
293112 listeners = listeners.concat(listenersAll);
294 }
295298 for (var i = 0, l = listeners.length; i < l; i++){
296184 listeners[i].call(self, data);
297 }
298 },
299
300 // remove event
301 removeListener: function(type, listener){
30213 var listenersType = this._listeners[type];
30313 if(listenersType !== undefined){
30411 for (var i = 0, l = listenersType.length; i < l; i++){
3058 if (listenersType[i] === listener){
3068 listenersType.splice(i, 1);
3078 break;
308 }
309 }
310 }
311 },
312
313 //fix pos if event.raw have \n
314 fixPos: function(event, index){
3153 var text = event.raw.substr(0, index);
3163 var arrLines = text.split(/\r?\n/),
317 lineCount = arrLines.length - 1,
318 line = event.line, col;
3193 if(lineCount > 0){
3201 line += lineCount;
3211 col = arrLines[lineCount].length + 1;
322 }
323 else{
3242 col = event.col + index;
325 }
3263 return {
327 line: line,
328 col: col
329 };
330 },
331
332 // covert array type of attrs to map
333 getMapAttrs: function(arrAttrs){
3346 var mapAttrs = {},
335 attr;
3366 for(var i=0,l=arrAttrs.length;i<l;i++){
3376 attr = arrAttrs[i];
3386 mapAttrs[attr.name] = attr.value;
339 }
3406 return mapAttrs;
341 }
342 };
343
3441 return HTMLParser;
345
346})();
347
3481if (typeof exports === 'object' && exports){
3491 exports.HTMLParser = HTMLParser;
350}
351/**
352 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
353 * MIT Licensed
354 */
3551HTMLHint.addRule({
356 id: 'attr-lowercase',
357 description: 'Attribute name must be lowercase.',
358 init: function(parser, reporter){
3593 var self = this;
3603 parser.addListener('tagstart', function(event){
3613 var attrs = event.attrs,
362 attr,
363 col = event.col + event.tagName.length + 1;
3643 for(var i=0, l=attrs.length;i<l;i++){
3653 attr = attrs[i];
3663 var attrName = attr.name;
3673 if(attrName !== attrName.toLowerCase()){
3682 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
369 }
370 }
371 });
372 }
373});
374/**
375 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
376 * MIT Licensed
377 */
3781HTMLHint.addRule({
379 id: 'attr-value-double-quotes',
380 description: 'Attribute value must closed by double quotes.',
381 init: function(parser, reporter){
3824 var self = this;
3834 parser.addListener('tagstart', function(event){
3844 var attrs = event.attrs,
385 attr,
386 col = event.col + event.tagName.length + 1;
3874 for(var i=0, l=attrs.length;i<l;i++){
3886 attr = attrs[i];
3896 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3902 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
391 }
392 }
393 });
394 }
395});
396/**
397 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
398 * MIT Licensed
399 */
4001HTMLHint.addRule({
401 id: 'attr-value-not-empty',
402 description: 'Attribute must set value.',
403 init: function(parser, reporter){
4043 var self = this;
4053 parser.addListener('tagstart', function(event){
4063 var attrs = event.attrs,
407 attr,
408 col = event.col + event.tagName.length + 1;
4093 for(var i=0, l=attrs.length;i<l;i++){
4103 attr = attrs[i];
4113 if(attr.quote === '' && attr.value === ''){
4121 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
413 }
414 }
415 });
416 }
417});
418/**
419 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
420 * MIT Licensed
421 */
4221HTMLHint.addRule({
423 id: 'csslint',
424 description: 'Scan css with csslint.',
425 init: function(parser, reporter, options){
4261 var self = this;
4271 parser.addListener('cdata', function(event){
4281 if(event.tagName.toLowerCase() === 'style'){
429
4301 var cssVerify;
431
4321 if(typeof exports === 'object' && require){
4331 cssVerify = require("csslint").CSSLint.verify;
434 }
435 else{
4360 cssVerify = CSSLint.verify;
437 }
438
4391 if(options !== undefined){
4401 var styleLine = event.line - 1,
441 styleCol = event.col - 1;
4421 try{
4431 var messages = cssVerify(event.raw, options).messages;
4441 messages.forEach(function(error){
4452 var line = error.line;
4462 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
447 });
448 }
449 catch(e){}
450 }
451
452 }
453 });
454 }
455});
456/**
457 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
458 * MIT Licensed
459 */
4601HTMLHint.addRule({
461 id: 'doctype-first',
462 description: 'Doctype must be first.',
463 init: function(parser, reporter){
4643 var self = this;
4653 var allEvent = function(event){
4666 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4673 return;
468 }
4693 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4702 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
471 }
4723 parser.removeListener('all', allEvent);
473 };
4743 parser.addListener('all', allEvent);
475 }
476});
477/**
478 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
479 * MIT Licensed
480 */
4811HTMLHint.addRule({
482 id: 'doctype-html5',
483 description: 'Doctype must be html5.',
484 init: function(parser, reporter){
4852 var self = this;
4862 function onComment(event){
4879 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4881 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
489 }
490 }
4912 function onTagStart(){
4922 parser.removeListener('comment', onComment);
4932 parser.removeListener('tagstart', onTagStart);
494 }
4952 parser.addListener('all', onComment);
4962 parser.addListener('tagstart', onTagStart);
497 }
498});
499/**
500 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
501 * MIT Licensed
502 */
5031HTMLHint.addRule({
504 id: 'head-script-disabled',
505 description: 'The script tag can not be used in head.',
506 init: function(parser, reporter){
5073 var self = this;
5083 function onTagStart(event){
5095 if(event.tagName.toLowerCase() === 'script'){
5102 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
511 }
512 }
5133 function onTagEnd(event){
5147 if(event.tagName.toLowerCase() === 'head'){
5153 parser.removeListener('tagstart', onTagStart);
5163 parser.removeListener('tagstart', onTagEnd);
517 }
518 }
5193 parser.addListener('tagstart', onTagStart);
5203 parser.addListener('tagend', onTagEnd);
521 }
522});
523/**
524 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
525 * MIT Licensed
526 */
5271HTMLHint.addRule({
528 id: 'id-class-value',
529 description: 'Id and class value must meet some rules.',
530 init: function(parser, reporter, options){
5318 var self = this;
5328 var arrRules = {
533 'underline': {
534 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
535 'message': 'Id and class value must lower case and split by underline.'
536 },
537 'dash': {
538 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
539 'message': 'Id and class value must lower case and split by dash.'
540 },
541 'hump': {
542 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
543 'message': 'Id and class value must meet hump style.'
544 }
545 }, rule;
5468 if(typeof options === 'string'){
5476 rule = arrRules[options];
548 }
549 else{
5502 rule = options;
551 }
5528 if(rule && rule.regId){
5538 var regId = rule.regId,
554 message = rule.message;
5558 parser.addListener('tagstart', function(event){
5568 var attrs = event.attrs,
557 attr,
558 col = event.col + event.tagName.length + 1;
5598 for(var i=0, l1=attrs.length;i<l1;i++){
56016 attr = attrs[i];
56116 if(attr.name.toLowerCase() === 'id'){
5628 if(regId.test(attr.value) === false){
5634 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
564 }
565 }
56616 if(attr.name.toLowerCase() === 'class'){
5678 var arrClass = attr.value.split(/\s+/g), classValue;
5688 for(var j=0, l2=arrClass.length;j<l2;j++){
5698 classValue = arrClass[j];
5708 if(classValue && regId.test(classValue) === false){
5714 reporter.warn(message, event.line, col + attr.index, self, classValue);
572 }
573 }
574 }
575 }
576 });
577 }
578 }
579});
580/**
581 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
582 * MIT Licensed
583 */
5841HTMLHint.addRule({
585 id: 'id-unique',
586 description: 'Id must be unique.',
587 init: function(parser, reporter){
5883 var self = this;
5893 var mapIdCount = {};
5903 parser.addListener('tagstart', function(event){
5915 var attrs = event.attrs,
592 attr,
593 id,
594 col = event.col + event.tagName.length + 1;
5955 for(var i=0, l=attrs.length;i<l;i++){
5965 attr = attrs[i];
5975 if(attr.name.toLowerCase() === 'id'){
5984 id = attr.value;
5994 if(id){
6004 if(mapIdCount[id] === undefined){
6013 mapIdCount[id] = 1;
602 }
603 else{
6041 mapIdCount[id] ++;
605 }
6064 if(mapIdCount[id] > 1){
6071 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
608 }
609 }
6104 break;
611 }
612 }
613 });
614 }
615});
616/**
617 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
618 * MIT Licensed
619 */
6201HTMLHint.addRule({
621 id: 'img-alt-require',
622 description: 'Alt of img tag must be set value.',
623 init: function(parser, reporter){
6243 var self = this;
6253 parser.addListener('tagstart', function(event){
6263 if(event.tagName.toLowerCase() === 'img'){
6273 var attrs = event.attrs;
6283 var haveAlt = false;
6293 for(var i=0, l=attrs.length;i<l;i++){
6308 if(attrs[i].name.toLowerCase() === 'alt'){
6312 haveAlt = true;
6322 break;
633 }
634 }
6353 if(haveAlt === false){
6361 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
637 }
638 }
639 });
640 }
641});
642/**
643 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
644 * MIT Licensed
645 */
6461HTMLHint.addRule({
647 id: 'jshint',
648 description: 'Scan script with jshint.',
649 init: function(parser, reporter, options){
6504 var self = this;
6514 parser.addListener('cdata', function(event){
6524 if(event.tagName.toLowerCase() === 'script'){
653
6544 var mapAttrs = parser.getMapAttrs(event.attrs),
655 type = mapAttrs.type;
656
657 // Only scan internal javascript
6584 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6592 return;
660 }
661
6622 var jsVerify;
663
6642 if(typeof exports === 'object' && require){
6652 jsVerify = require('jshint').JSHINT;
666 }
667 else{
6680 jsVerify = JSHINT;
669 }
670
6712 if(options !== undefined){
6722 var styleLine = event.line - 1,
673 styleCol = event.col - 1;
6742 var code = event.raw.replace(/\t/g,' ');
6752 try{
6762 var status = jsVerify(code, options);
6772 if(status === false){
6782 jsVerify.errors.forEach(function(error){
6798 var line = error.line;
6808 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
681 });
682 }
683 }
684 catch(e){}
685 }
686
687 }
688 });
689 }
690});
691/**
692 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
693 * MIT Licensed
694 */
6951HTMLHint.addRule({
696 id: 'spec-char-escape',
697 description: 'Special characters must be escaped.',
698 init: function(parser, reporter){
6993 var self = this;
7003 parser.addListener('text', function(event){
7013 var raw = event.raw,
702 reSpecChar = /[<>]/g,
703 match;
7043 while((match = reSpecChar.exec(raw))){
7053 var fixedPos = parser.fixPos(event, match.index);
7063 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
707 }
708 });
709 }
710});
711/**
712 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
713 * MIT Licensed
714 */
7151HTMLHint.addRule({
716 id: 'style-disabled',
717 description: 'Style tag can not be use.',
718 init: function(parser, reporter){
7192 var self = this;
7202 parser.addListener('tagstart', function(event){
7214 if(event.tagName.toLowerCase() === 'style'){
7221 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
723 }
724 });
725 }
726});
727/**
728 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
729 * MIT Licensed
730 */
7311HTMLHint.addRule({
732 id: 'tag-pair',
733 description: 'Tag must be paired.',
734 init: function(parser, reporter){
7354 var self = this;
7364 var stack=[],
737 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7384 parser.addListener('tagstart', function(event){
7395 var tagName = event.tagName.toLowerCase();
7405 if (mapEmptyTags[tagName] === undefined && !event.close){
7415 stack.push(tagName);
742 }
743 });
7444 parser.addListener('tagend', function(event){
7453 var tagName = event.tagName.toLowerCase();
746 //向上寻找匹配的开始标签
7473 for(var pos = stack.length-1;pos >= 0; pos--){
7483 if(stack[pos] === tagName){
7492 break;
750 }
751 }
7523 if(pos >= 0){
7532 var arrTags = [];
7542 for(var i=stack.length-1;i>pos;i--){
7551 arrTags.push('</'+stack[i]+'>');
756 }
7572 if(arrTags.length > 0){
7581 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
759 }
7602 stack.length=pos;
761 }
762 else{
7631 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
764 }
765 });
7664 parser.addListener('end', function(event){
7674 var arrTags = [];
7684 for(var i=stack.length-1;i>=0;i--){
7692 arrTags.push('</'+stack[i]+'>');
770 }
7714 if(arrTags.length > 0){
7722 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
773 }
774 });
775 }
776});
777/**
778 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
779 * MIT Licensed
780 */
7811HTMLHint.addRule({
782 id: 'tag-self-close',
783 description: 'The empty tag must closed by self.',
784 init: function(parser, reporter){
7852 var self = this;
7862 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7872 parser.addListener('tagstart', function(event){
7884 var tagName = event.tagName.toLowerCase();
7894 if(mapEmptyTags[tagName] !== undefined){
7904 if(!event.close){
7912 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
792 }
793 }
794 });
795 }
796});
797/**
798 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
799 * MIT Licensed
800 */
8011HTMLHint.addRule({
802 id: 'tagname-lowercase',
803 description: 'Tagname must be lowercase.',
804 init: function(parser, reporter){
8053 var self = this;
8063 parser.addListener('tagstart,tagend', function(event){
8079 var tagName = event.tagName;
8089 if(tagName !== tagName.toLowerCase()){
8094 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
810 }
811 });
812 }
813});
\ 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 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 };
23
241 HTMLHint.addRule = function(rule){
2516 HTMLHint.rules[rule.id] = rule;
26 };
27
281 HTMLHint.verify = function(html, ruleset){
2945 if(ruleset === undefined){
301 ruleset = HTMLHint.defaultRuleset;
31 }
3245 var parser = new HTMLParser(),
33 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
34
3545 var rules = HTMLHint.rules,
36 rule;
3745 for (var id in ruleset){
3851 rule = rules[id];
3951 if (rule !== undefined){
4051 rule.init(parser, reporter, ruleset[id]);
41 }
42 }
43
4445 parser.parse(html);
45
4645 return reporter.messages;
47 };
48
491 return HTMLHint;
50
51})();
52
531if (typeof exports === 'object' && exports){
541 exports.HTMLHint = HTMLHint;
55}
56/**
57 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
58 * MIT Licensed
59 */
601(function(HTMLHint, undefined){
61
621 var Reporter = function(){
6345 var self = this;
6445 self._init.apply(self,arguments);
65 };
66
671 Reporter.prototype = {
68 _init: function(lines, ruleset){
6945 var self = this;
7045 self.lines = lines;
7145 self.ruleset = ruleset;
7245 self.messages = [];
73 },
74 //错误
75 error: function(message, line, col, rule, raw){
7618 this.report('error', message, line, col, rule, raw);
77 },
78 //警告
79 warn: function(message, line, col, rule, raw){
8026 this.report('warning', message, line, col, rule, raw);
81 },
82 //信息
83 info: function(message, line, col, rule, raw){
840 this.report('info', message, line, col, rule, raw);
85 },
86 //报告
87 report: function(type, message, line, col, rule, raw){
8844 var self = this;
8944 self.messages.push({
90 type: type,
91 message: message,
92 raw: raw,
93 evidence: self.lines[line-1],
94 line: line,
95 col: col,
96 rule: {
97 id: rule.id,
98 description: rule.description,
99 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
100 }
101 });
102 }
103 };
104
1051 HTMLHint.Reporter = Reporter;
106
107})(HTMLHint);
108/**
109 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
110 * MIT Licensed
111 */
1121var HTMLParser = (function(undefined){
113
1141 var HTMLParser = function(){
11567 var self = this;
11667 self._init.apply(self,arguments);
117 };
118
1191 HTMLParser.prototype = {
120 _init: function(){
12167 var self = this;
12267 self._listeners = {};
12367 self._mapCdataTags = self.makeMap("script,style");
12467 self._arrBlocks = [];
125 },
126
127 makeMap: function(str){
12873 var obj = {}, items = str.split(",");
12973 for ( var i = 0; i < items.length; i++ ){
130218 obj[ items[i] ] = true;
131 }
13273 return obj;
133 },
134
135 // parse html code
136 parse: function(html){
137
13867 var self = this,
139 mapCdataTags = self._mapCdataTags;
140
14167 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
142 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
143 regLine = /\r?\n/g;
144
14567 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
14667 var lastLineIndex = 0, line = 1;
14767 var arrBlocks = self._arrBlocks;
148
14967 self.fire('start', {
150 pos: 0,
151 line: 1,
152 col: 1
153 });
154
15567 while((match = regTag.exec(html))){
156130 matchIndex = match.index;
157130 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15825 text = html.substring(lastIndex, matchIndex);
15925 if(tagCDATA){
16010 arrCDATA.push(text);
161 }
162 else{//文本
16315 saveBlock('text', text, lastIndex);
164 }
165 }
166130 lastIndex = regTag.lastIndex;
167
168130 if((tagName = match[1])){
16943 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
17011 text = arrCDATA.join('');
17111 saveBlock('cdata', text, lastCDATAIndex, {
172 'tagName': tagCDATA,
173 'attrs': attrsCDATA
174 });
17511 tagCDATA = null;
17611 attrsCDATA = null;
17711 arrCDATA = null;
178 }
17943 if(!tagCDATA){
180 //标签结束
18142 saveBlock('tagend', match[0], matchIndex, {
182 'tagName': tagName
183 });
18442 continue;
185 }
186 }
187
18888 if(tagCDATA){
1891 arrCDATA.push(match[0]);
190 }
191 else{
19287 if((tagName = match[4])){//标签开始
19374 arrAttrs = [];
19474 var attrs = match[5],
195 attrMatch,
196 attrMatchCount = 0;
19774 while((attrMatch = regAttr.exec(attrs))){
19861 var name = attrMatch[1],
199 quote = attrMatch[2] ? attrMatch[2] :
200 attrMatch[4] ? attrMatch[4] : '',
201 value = attrMatch[3] ? attrMatch[3] :
202 attrMatch[5] ? attrMatch[5] :
203 attrMatch[6] ? attrMatch[6] : '';
20461 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
20561 attrMatchCount += attrMatch[0].length;
206 }
20774 if(attrMatchCount === attrs.length){
20873 saveBlock('tagstart', match[0], matchIndex, {
209 'tagName': tagName,
210 'attrs': arrAttrs,
211 'close': match[6]
212 });
21373 if(mapCdataTags[tagName]){
21411 tagCDATA = tagName;
21511 attrsCDATA = arrAttrs.concat();
21611 arrCDATA = [];
21711 lastCDATAIndex = lastIndex;
218 }
219 }
220 else{//如果出现漏匹配,则把当前内容匹配为text
2211 saveBlock('text', match[0], matchIndex);
222 }
223 }
22413 else if(match[2] || match[3]){//注释标签
22513 saveBlock('comment', match[0], matchIndex, {
226 'content': match[2] || match[3],
227 'long': match[2]?true:false
228 });
229 }
230 }
231 }
232
23367 if(html.length > lastIndex){
234 //结尾文本
2359 text = html.substring(lastIndex, html.length);
2369 saveBlock('text', text, lastIndex);
237 }
238
23967 self.fire('end', {
240 pos: lastIndex,
241 line: line,
242 col: lastIndex - lastLineIndex + 1
243 });
244
245 //存储区块
24667 function saveBlock(type, raw, pos, data){
247164 var col = pos - lastLineIndex + 1;
248164 if(data === undefined){
24925 data = {};
250 }
251164 data.raw = raw;
252164 data.pos = pos;
253164 data.line = line;
254164 data.col = col;
255164 arrBlocks.push(data);
256164 self.fire(type, data);
257164 var lineMatch;
258164 while((lineMatch = regLine.exec(raw))){
25918 line ++;
26018 lastLineIndex = pos + regLine.lastIndex;
261 }
262 }
263
264 },
265
266 // add event
267 addListener: function(types, listener){
26886 var _listeners = this._listeners;
26986 var arrTypes = types.split(/[,\s]/), type;
27086 for(var i=0, l = arrTypes.length;i<l;i++){
27189 type = arrTypes[i];
27289 if (_listeners[type] === undefined){
27384 _listeners[type] = [];
274 }
27589 _listeners[type].push(listener);
276 }
277 },
278
279 // fire event
280 fire: function(type, data){
281298 if (data === undefined){
2820 data = {};
283 }
284298 data.type = type;
285298 var self = this,
286 listeners = [],
287 listenersType = self._listeners[type],
288 listenersAll = self._listeners['all'];
289298 if (listenersType !== undefined){
29075 listeners = listeners.concat(listenersType);
291 }
292298 if (listenersAll !== undefined){
293112 listeners = listeners.concat(listenersAll);
294 }
295298 for (var i = 0, l = listeners.length; i < l; i++){
296184 listeners[i].call(self, data);
297 }
298 },
299
300 // remove event
301 removeListener: function(type, listener){
30213 var listenersType = this._listeners[type];
30313 if(listenersType !== undefined){
30411 for (var i = 0, l = listenersType.length; i < l; i++){
3058 if (listenersType[i] === listener){
3068 listenersType.splice(i, 1);
3078 break;
308 }
309 }
310 }
311 },
312
313 //fix pos if event.raw have \n
314 fixPos: function(event, index){
3153 var text = event.raw.substr(0, index);
3163 var arrLines = text.split(/\r?\n/),
317 lineCount = arrLines.length - 1,
318 line = event.line, col;
3193 if(lineCount > 0){
3201 line += lineCount;
3211 col = arrLines[lineCount].length + 1;
322 }
323 else{
3242 col = event.col + index;
325 }
3263 return {
327 line: line,
328 col: col
329 };
330 },
331
332 // covert array type of attrs to map
333 getMapAttrs: function(arrAttrs){
3346 var mapAttrs = {},
335 attr;
3366 for(var i=0,l=arrAttrs.length;i<l;i++){
3376 attr = arrAttrs[i];
3386 mapAttrs[attr.name] = attr.value;
339 }
3406 return mapAttrs;
341 }
342 };
343
3441 return HTMLParser;
345
346})();
347
3481if (typeof exports === 'object' && exports){
3491 exports.HTMLParser = HTMLParser;
350}
351/**
352 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
353 * MIT Licensed
354 */
3551HTMLHint.addRule({
356 id: 'attr-lowercase',
357 description: 'Attribute name must be lowercase.',
358 init: function(parser, reporter){
3593 var self = this;
3603 parser.addListener('tagstart', function(event){
3613 var attrs = event.attrs,
362 attr,
363 col = event.col + event.tagName.length + 1;
3643 for(var i=0, l=attrs.length;i<l;i++){
3653 attr = attrs[i];
3663 var attrName = attr.name;
3673 if(attrName !== attrName.toLowerCase()){
3682 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
369 }
370 }
371 });
372 }
373});
374/**
375 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
376 * MIT Licensed
377 */
3781HTMLHint.addRule({
379 id: 'attr-value-double-quotes',
380 description: 'Attribute value must closed by double quotes.',
381 init: function(parser, reporter){
3824 var self = this;
3834 parser.addListener('tagstart', function(event){
3844 var attrs = event.attrs,
385 attr,
386 col = event.col + event.tagName.length + 1;
3874 for(var i=0, l=attrs.length;i<l;i++){
3886 attr = attrs[i];
3896 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3902 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
391 }
392 }
393 });
394 }
395});
396/**
397 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
398 * MIT Licensed
399 */
4001HTMLHint.addRule({
401 id: 'attr-value-not-empty',
402 description: 'Attribute must set value.',
403 init: function(parser, reporter){
4043 var self = this;
4053 parser.addListener('tagstart', function(event){
4063 var attrs = event.attrs,
407 attr,
408 col = event.col + event.tagName.length + 1;
4093 for(var i=0, l=attrs.length;i<l;i++){
4103 attr = attrs[i];
4113 if(attr.quote === '' && attr.value === ''){
4121 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
413 }
414 }
415 });
416 }
417});
418/**
419 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
420 * MIT Licensed
421 */
4221HTMLHint.addRule({
423 id: 'csslint',
424 description: 'Scan css with csslint.',
425 init: function(parser, reporter, options){
4261 var self = this;
4271 parser.addListener('cdata', function(event){
4281 if(event.tagName.toLowerCase() === 'style'){
429
4301 var cssVerify;
431
4321 if(typeof exports === 'object' && require){
4331 cssVerify = require("csslint").CSSLint.verify;
434 }
435 else{
4360 cssVerify = CSSLint.verify;
437 }
438
4391 if(options !== undefined){
4401 var styleLine = event.line - 1,
441 styleCol = event.col - 1;
4421 try{
4431 var messages = cssVerify(event.raw, options).messages;
4441 messages.forEach(function(error){
4452 var line = error.line;
4462 reporter[error.type==='warning'?'warn':'error']('['+error.rule+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, '');
447 });
448 }
449 catch(e){}
450 }
451
452 }
453 });
454 }
455});
456/**
457 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
458 * MIT Licensed
459 */
4601HTMLHint.addRule({
461 id: 'doctype-first',
462 description: 'Doctype must be first.',
463 init: function(parser, reporter){
4643 var self = this;
4653 var allEvent = function(event){
4666 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4673 return;
468 }
4693 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4702 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
471 }
4723 parser.removeListener('all', allEvent);
473 };
4743 parser.addListener('all', allEvent);
475 }
476});
477/**
478 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
479 * MIT Licensed
480 */
4811HTMLHint.addRule({
482 id: 'doctype-html5',
483 description: 'Doctype must be html5.',
484 init: function(parser, reporter){
4852 var self = this;
4862 function onComment(event){
4879 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4881 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
489 }
490 }
4912 function onTagStart(){
4922 parser.removeListener('comment', onComment);
4932 parser.removeListener('tagstart', onTagStart);
494 }
4952 parser.addListener('all', onComment);
4962 parser.addListener('tagstart', onTagStart);
497 }
498});
499/**
500 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
501 * MIT Licensed
502 */
5031HTMLHint.addRule({
504 id: 'head-script-disabled',
505 description: 'The script tag can not be used in head.',
506 init: function(parser, reporter){
5073 var self = this;
5083 function onTagStart(event){
5095 if(event.tagName.toLowerCase() === 'script'){
5102 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
511 }
512 }
5133 function onTagEnd(event){
5147 if(event.tagName.toLowerCase() === 'head'){
5153 parser.removeListener('tagstart', onTagStart);
5163 parser.removeListener('tagstart', onTagEnd);
517 }
518 }
5193 parser.addListener('tagstart', onTagStart);
5203 parser.addListener('tagend', onTagEnd);
521 }
522});
523/**
524 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
525 * MIT Licensed
526 */
5271HTMLHint.addRule({
528 id: 'id-class-value',
529 description: 'Id and class value must meet some rules.',
530 init: function(parser, reporter, options){
5318 var self = this;
5328 var arrRules = {
533 'underline': {
534 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
535 'message': 'Id and class value must lower case and split by underline.'
536 },
537 'dash': {
538 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
539 'message': 'Id and class value must lower case and split by dash.'
540 },
541 'hump': {
542 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
543 'message': 'Id and class value must meet hump style.'
544 }
545 }, rule;
5468 if(typeof options === 'string'){
5476 rule = arrRules[options];
548 }
549 else{
5502 rule = options;
551 }
5528 if(rule && rule.regId){
5538 var regId = rule.regId,
554 message = rule.message;
5558 parser.addListener('tagstart', function(event){
5568 var attrs = event.attrs,
557 attr,
558 col = event.col + event.tagName.length + 1;
5598 for(var i=0, l1=attrs.length;i<l1;i++){
56016 attr = attrs[i];
56116 if(attr.name.toLowerCase() === 'id'){
5628 if(regId.test(attr.value) === false){
5634 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
564 }
565 }
56616 if(attr.name.toLowerCase() === 'class'){
5678 var arrClass = attr.value.split(/\s+/g), classValue;
5688 for(var j=0, l2=arrClass.length;j<l2;j++){
5698 classValue = arrClass[j];
5708 if(classValue && regId.test(classValue) === false){
5714 reporter.warn(message, event.line, col + attr.index, self, classValue);
572 }
573 }
574 }
575 }
576 });
577 }
578 }
579});
580/**
581 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
582 * MIT Licensed
583 */
5841HTMLHint.addRule({
585 id: 'id-unique',
586 description: 'Id must be unique.',
587 init: function(parser, reporter){
5883 var self = this;
5893 var mapIdCount = {};
5903 parser.addListener('tagstart', function(event){
5915 var attrs = event.attrs,
592 attr,
593 id,
594 col = event.col + event.tagName.length + 1;
5955 for(var i=0, l=attrs.length;i<l;i++){
5965 attr = attrs[i];
5975 if(attr.name.toLowerCase() === 'id'){
5984 id = attr.value;
5994 if(id){
6004 if(mapIdCount[id] === undefined){
6013 mapIdCount[id] = 1;
602 }
603 else{
6041 mapIdCount[id] ++;
605 }
6064 if(mapIdCount[id] > 1){
6071 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
608 }
609 }
6104 break;
611 }
612 }
613 });
614 }
615});
616/**
617 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
618 * MIT Licensed
619 */
6201HTMLHint.addRule({
621 id: 'img-alt-require',
622 description: 'Alt of img tag must be set value.',
623 init: function(parser, reporter){
6243 var self = this;
6253 parser.addListener('tagstart', function(event){
6263 if(event.tagName.toLowerCase() === 'img'){
6273 var attrs = event.attrs;
6283 var haveAlt = false;
6293 for(var i=0, l=attrs.length;i<l;i++){
6308 if(attrs[i].name.toLowerCase() === 'alt'){
6312 haveAlt = true;
6322 break;
633 }
634 }
6353 if(haveAlt === false){
6361 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
637 }
638 }
639 });
640 }
641});
642/**
643 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
644 * MIT Licensed
645 */
6461HTMLHint.addRule({
647 id: 'jshint',
648 description: 'Scan script with jshint.',
649 init: function(parser, reporter, options){
6504 var self = this;
6514 parser.addListener('cdata', function(event){
6524 if(event.tagName.toLowerCase() === 'script'){
653
6544 var mapAttrs = parser.getMapAttrs(event.attrs),
655 type = mapAttrs.type;
656
657 // Only scan internal javascript
6584 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
6592 return;
660 }
661
6622 var jsVerify;
663
6642 if(typeof exports === 'object' && require){
6652 jsVerify = require('jshint').JSHINT;
666 }
667 else{
6680 jsVerify = JSHINT;
669 }
670
6712 if(options !== undefined){
6722 var styleLine = event.line - 1,
673 styleCol = event.col - 1;
6742 var code = event.raw.replace(/\t/g,' ');
6752 try{
6762 var status = jsVerify(code, options);
6772 if(status === false){
6782 jsVerify.errors.forEach(function(error){
6798 var line = error.line;
6808 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
681 });
682 }
683 }
684 catch(e){}
685 }
686
687 }
688 });
689 }
690});
691/**
692 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
693 * MIT Licensed
694 */
6951HTMLHint.addRule({
696 id: 'spec-char-escape',
697 description: 'Special characters must be escaped.',
698 init: function(parser, reporter){
6993 var self = this;
7003 parser.addListener('text', function(event){
7013 var raw = event.raw,
702 reSpecChar = /[<>]/g,
703 match;
7043 while((match = reSpecChar.exec(raw))){
7053 var fixedPos = parser.fixPos(event, match.index);
7063 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
707 }
708 });
709 }
710});
711/**
712 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
713 * MIT Licensed
714 */
7151HTMLHint.addRule({
716 id: 'style-disabled',
717 description: 'Style tag can not be use.',
718 init: function(parser, reporter){
7192 var self = this;
7202 parser.addListener('tagstart', function(event){
7214 if(event.tagName.toLowerCase() === 'style'){
7221 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
723 }
724 });
725 }
726});
727/**
728 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
729 * MIT Licensed
730 */
7311HTMLHint.addRule({
732 id: 'tag-pair',
733 description: 'Tag must be paired.',
734 init: function(parser, reporter){
7354 var self = this;
7364 var stack=[],
737 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7384 parser.addListener('tagstart', function(event){
7395 var tagName = event.tagName.toLowerCase();
7405 if (mapEmptyTags[tagName] === undefined && !event.close){
7415 stack.push(tagName);
742 }
743 });
7444 parser.addListener('tagend', function(event){
7453 var tagName = event.tagName.toLowerCase();
746 //向上寻找匹配的开始标签
7473 for(var pos = stack.length-1;pos >= 0; pos--){
7483 if(stack[pos] === tagName){
7492 break;
750 }
751 }
7523 if(pos >= 0){
7532 var arrTags = [];
7542 for(var i=stack.length-1;i>pos;i--){
7551 arrTags.push('</'+stack[i]+'>');
756 }
7572 if(arrTags.length > 0){
7581 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
759 }
7602 stack.length=pos;
761 }
762 else{
7631 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
764 }
765 });
7664 parser.addListener('end', function(event){
7674 var arrTags = [];
7684 for(var i=stack.length-1;i>=0;i--){
7692 arrTags.push('</'+stack[i]+'>');
770 }
7714 if(arrTags.length > 0){
7722 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
773 }
774 });
775 }
776});
777/**
778 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
779 * MIT Licensed
780 */
7811HTMLHint.addRule({
782 id: 'tag-self-close',
783 description: 'The empty tag must closed by self.',
784 init: function(parser, reporter){
7852 var self = this;
7862 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7872 parser.addListener('tagstart', function(event){
7884 var tagName = event.tagName.toLowerCase();
7894 if(mapEmptyTags[tagName] !== undefined){
7904 if(!event.close){
7912 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
792 }
793 }
794 });
795 }
796});
797/**
798 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
799 * MIT Licensed
800 */
8011HTMLHint.addRule({
802 id: 'tagname-lowercase',
803 description: 'Tagname must be lowercase.',
804 init: function(parser, reporter){
8053 var self = this;
8063 parser.addListener('tagstart,tagend', function(event){
8079 var tagName = event.tagName;
8089 if(tagName !== tagName.toLowerCase()){
8094 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
810 }
811 });
812 }
813});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 1debb95bd..80a4ca05c 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -1,8 +1,8 @@ /*! - * HTMLHint v0.9.2 + * HTMLHint v0.9.3 * https://github.com/yaniswang/HTMLHint * * (c) 2013 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(e){var t={};return t.version="0.9.2",t.rules={},t.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0,"id-unique":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,r){r===e&&(r=t.defaultRuleset);var i,n=new HTMLParser,s=new t.Reporter(a.split(/\r?\n/),r),o=t.rules;for(var l in r)i=o[l],i!==e&&i.init(n,s,r[l]);return n.parse(a),s.messages},t}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,r,i){this.report("error",e,t,a,r,i)},warn:function(e,t,a,r,i){this.report("warning",e,t,a,r,i)},info:function(e,t,a,r,i){this.report("info",e,t,a,r,i)},report:function(e,t,a,r,i,n){var s=this;s.messages.push({type:e,message:t,raw:n,evidence:s.lines[a-1],line:a,col:r,rule:{id:i.id,description:i.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+i.id}})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),r=0;a.length>r;r++)t[a[r]]=!0;return t},parse:function(t){function a(t,a,r,i){var n=r-w+1;i===e&&(i={}),i.raw=a,i.pos=r,i.line=L,i.col=n,b.push(i),u.fire(t,i);for(var s;s=m.exec(a);)L++,w=r+m.lastIndex}var r,i,n,s,o,l,d,c,u=this,g=u._mapCdataTags,f=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,p=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,m=/\r?\n/g,v=0,h=0,w=0,L=1,b=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});r=f.exec(t);)if(i=r.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=f.lastIndex,!(n=r[1])||(o&&n===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(r[0]);else if(n=r[4]){s=[];for(var H,y=r[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",N=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:N,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",r[0],i,{tagName:n,attrs:s,close:r[6]}),g[n]&&(o=n,l=s.concat(),d=[],h=v)):a("text",r[0],i)}else(r[2]||r[3])&&a("comment",r[0],i,{content:r[2]||r[3],"long":r[2]?!0:!1});else a("tagend",r[0],i,{tagName:n});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:L,col:v-w+1})},addListener:function(t,a){for(var r,i=this._listeners,n=t.split(/[,\s]/),s=0,o=n.length;o>s;s++)r=n[s],i[r]===e&&(i[r]=[]),i[r].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var r=this,i=[],n=r._listeners[t],s=r._listeners.all;n!==e&&(i=i.concat(n)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(r,a)},removeListener:function(t,a){var r=this._listeners[t];if(r!==e)for(var i=0,n=r.length;n>i;i++)if(r[i]===a){r.splice(i,1);break}},fixPos:function(e,t){var a,r=e.raw.substr(0,t),i=r.split(/\r?\n/),n=i.length-1,s=e.line;return n>0?(s+=n,a=i[n].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},r=0,i=e.length;i>r;r++)t=e[r],a[t.name]=t.value;return a}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){r=i[s];var l=r.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,n+r.index,a,r.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],'"'===r.quote||""===r.value&&""!==(r.value===r.quote)||t.error("The value of attribute [ "+r.name+" ] must closed by double quotes.",e.line,n+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],""===r.quote&&""===r.value&&t.warn("The attribute [ "+r.name+" ] must set value.",e.line,n+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var n=e.line-1,s=e.col-1;try{var o=i(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,n+a,(1===a?s:0)+e.col,r,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,r=function(i){"start"===i.type||"text"===i.type&&/^\s*$/.test(i.raw)||(("comment"!==i.type&&i.long===!1||/^DOCTYPE\s+/i.test(i.content)===!1)&&t.error("Doctype must be first.",i.line,i.col,a,i.raw),e.removeListener("all",r))};e.addListener("all",r)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,i,e.raw)}function r(){e.removeListener("comment",a),e.removeListener("tagstart",r)}var i=this;e.addListener("all",a),e.addListener("tagstart",r)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,i,e.raw)}function r(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",r))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",r)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var r,i=this,n={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(r="string"==typeof a?n[a]:a,r&&r.regId){var s=r.regId,o=r.message;e.addListener("tagstart",function(e){for(var a,r=e.attrs,n=e.col+e.tagName.length+1,l=0,d=r.length;d>l;l++)if(a=r[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,n+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)c=u[g],c&&s.test(c)===!1&&t.warn(o,e.line,n+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,r={};e.addListener("tagstart",function(e){for(var i,n,s=e.attrs,o=e.col+e.tagName.length+1,l=0,d=s.length;d>l;l++)if(i=s[l],"id"===i.name.toLowerCase()){n=i.value,n&&(void 0===r[n]?r[n]=1:r[n]++,r[n]>1&&t.error("Id redefinition of [ "+n+" ].",e.line,o+i.index,a,i.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var r=e.attrs,i=!1,n=0,s=r.length;s>n;n++)if("alt"===r[n].name.toLowerCase()){i=!0;break}i===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var n=e.getMapAttrs(i.attrs),s=n.type;if(void 0!==n.src||s&&/^(text\/javascript)$/i.test(s)===!1)return;var o;if(o="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==a){var l=i.line-1,d=i.col-1,c=i.raw.replace(/\t/g," ");try{var u=o(c,a);u===!1&&o.errors.forEach(function(e){var a=e.line;t.warn(e.reason,l+a,(1===a?d:0)+e.character,r,e.evidence)})}catch(g){}}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(r){for(var i,n=r.raw,s=/[<>]/g;i=s.exec(n);){var o=e.fixPos(r,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,r.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,r=[],i=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var t=e.tagName.toLowerCase();void 0!==i[t]||e.close||r.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),n=r.length-1;n>=0&&r[n]!==i;n--);if(n>=0){for(var s=[],o=r.length-1;o>n;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),r.length=n}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var i=[],n=r.length-1;n>=0;n--)i.push("");i.length>0&&t.error("Tag must be paired, Missing: [ "+i.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,r=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var i=e.tagName.toLowerCase();void 0!==r[i]&&(e.close||t.warn("The empty tag : [ "+i+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var r=e.tagName;r!==r.toLowerCase()&&t.error("Tagname [ "+r+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file +var HTMLHint=function(e){var t={};return t.version="0.9.3",t.rules={},t.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0,"id-unique":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,r){r===e&&(r=t.defaultRuleset);var i,n=new HTMLParser,s=new t.Reporter(a.split(/\r?\n/),r),o=t.rules;for(var l in r)i=o[l],i!==e&&i.init(n,s,r[l]);return n.parse(a),s.messages},t}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,r,i){this.report("error",e,t,a,r,i)},warn:function(e,t,a,r,i){this.report("warning",e,t,a,r,i)},info:function(e,t,a,r,i){this.report("info",e,t,a,r,i)},report:function(e,t,a,r,i,n){var s=this;s.messages.push({type:e,message:t,raw:n,evidence:s.lines[a-1],line:a,col:r,rule:{id:i.id,description:i.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+i.id}})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),r=0;a.length>r;r++)t[a[r]]=!0;return t},parse:function(t){function a(t,a,r,i){var n=r-w+1;i===e&&(i={}),i.raw=a,i.pos=r,i.line=L,i.col=n,b.push(i),u.fire(t,i);for(var s;s=m.exec(a);)L++,w=r+m.lastIndex}var r,i,n,s,o,l,d,c,u=this,g=u._mapCdataTags,f=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,p=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,m=/\r?\n/g,v=0,h=0,w=0,L=1,b=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});r=f.exec(t);)if(i=r.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=f.lastIndex,!(n=r[1])||(o&&n===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(r[0]);else if(n=r[4]){s=[];for(var H,y=r[5],T=0;H=p.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",N=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:N,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",r[0],i,{tagName:n,attrs:s,close:r[6]}),g[n]&&(o=n,l=s.concat(),d=[],h=v)):a("text",r[0],i)}else(r[2]||r[3])&&a("comment",r[0],i,{content:r[2]||r[3],"long":r[2]?!0:!1});else a("tagend",r[0],i,{tagName:n});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:L,col:v-w+1})},addListener:function(t,a){for(var r,i=this._listeners,n=t.split(/[,\s]/),s=0,o=n.length;o>s;s++)r=n[s],i[r]===e&&(i[r]=[]),i[r].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var r=this,i=[],n=r._listeners[t],s=r._listeners.all;n!==e&&(i=i.concat(n)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(r,a)},removeListener:function(t,a){var r=this._listeners[t];if(r!==e)for(var i=0,n=r.length;n>i;i++)if(r[i]===a){r.splice(i,1);break}},fixPos:function(e,t){var a,r=e.raw.substr(0,t),i=r.split(/\r?\n/),n=i.length-1,s=e.line;return n>0?(s+=n,a=i[n].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},r=0,i=e.length;i>r;r++)t=e[r],a[t.name]=t.value;return a}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){r=i[s];var l=r.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,n+r.index,a,r.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],'"'===r.quote||""===r.value&&""!==(r.value===r.quote)||t.error("The value of attribute [ "+r.name+" ] must closed by double quotes.",e.line,n+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,i=e.attrs,n=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)r=i[s],""===r.quote&&""===r.value&&t.warn("The attribute [ "+r.name+" ] must set value.",e.line,n+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var n=e.line-1,s=e.col-1;try{var o=i(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"]("["+e.rule+"] "+e.message,n+a,(1===a?s:0)+e.col,r,"")})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,r=function(i){"start"===i.type||"text"===i.type&&/^\s*$/.test(i.raw)||(("comment"!==i.type&&i.long===!1||/^DOCTYPE\s+/i.test(i.content)===!1)&&t.error("Doctype must be first.",i.line,i.col,a,i.raw),e.removeListener("all",r))};e.addListener("all",r)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,i,e.raw)}function r(){e.removeListener("comment",a),e.removeListener("tagstart",r)}var i=this;e.addListener("all",a),e.addListener("tagstart",r)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,i,e.raw)}function r(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",r))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",r)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var r,i=this,n={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(r="string"==typeof a?n[a]:a,r&&r.regId){var s=r.regId,o=r.message;e.addListener("tagstart",function(e){for(var a,r=e.attrs,n=e.col+e.tagName.length+1,l=0,d=r.length;d>l;l++)if(a=r[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,n+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)c=u[g],c&&s.test(c)===!1&&t.warn(o,e.line,n+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,r={};e.addListener("tagstart",function(e){for(var i,n,s=e.attrs,o=e.col+e.tagName.length+1,l=0,d=s.length;d>l;l++)if(i=s[l],"id"===i.name.toLowerCase()){n=i.value,n&&(void 0===r[n]?r[n]=1:r[n]++,r[n]>1&&t.error("Id redefinition of [ "+n+" ].",e.line,o+i.index,a,i.raw));break}})}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var r=e.attrs,i=!1,n=0,s=r.length;s>n;n++)if("alt"===r[n].name.toLowerCase()){i=!0;break}i===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var n=e.getMapAttrs(i.attrs),s=n.type;if(void 0!==n.src||s&&/^(text\/javascript)$/i.test(s)===!1)return;var o;if(o="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==a){var l=i.line-1,d=i.col-1,c=i.raw.replace(/\t/g," ");try{var u=o(c,a);u===!1&&o.errors.forEach(function(e){var a=e.line;t.warn(e.reason,l+a,(1===a?d:0)+e.character,r,e.evidence)})}catch(g){}}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(r){for(var i,n=r.raw,s=/[<>]/g;i=s.exec(n);){var o=e.fixPos(r,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,r.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,r=[],i=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var t=e.tagName.toLowerCase();void 0!==i[t]||e.close||r.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),n=r.length-1;n>=0&&r[n]!==i;n--);if(n>=0){for(var s=[],o=r.length-1;o>n;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),r.length=n}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var i=[],n=r.length-1;n>=0;n--)i.push("");i.length>0&&t.error("Tag must be paired, Missing: [ "+i.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,r=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var i=e.tagName.toLowerCase();void 0!==r[i]&&(e.close||t.warn("The empty tag : [ "+i+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var r=e.tagName;r!==r.toLowerCase()&&t.error("Tagname [ "+r+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file diff --git a/package.json b/package.json index d0bd09614..6b1218e50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "htmlhint", - "version": "0.9.2", + "version": "0.9.3", "description": "A Static Code Analysis Tool for HTML", "main": "./index", "dependencies": { diff --git a/src/rules/csslint.js b/src/rules/csslint.js index 4d1da9863..e3a1e3ea1 100644 --- a/src/rules/csslint.js +++ b/src/rules/csslint.js @@ -26,7 +26,7 @@ HTMLHint.addRule({ var messages = cssVerify(event.raw, options).messages; messages.forEach(function(error){ var line = error.line; - reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence); + reporter[error.type==='warning'?'warn':'error']('['+error.rule+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, ''); }); } catch(e){}