diff --git a/coverage.html b/coverage.html index f477cbca6..ec802ea14 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%
408
402
6

htmlhint.js

98%
408
402
6
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var HTMLHint = {};
8
91 HTMLHint.version = '@VERSION';
10
111 HTMLHint.rules = {};
12
13 //默认配置
141 HTMLHint.defaultRuleset = {
15 'tagname-lowercase': true,
16 'attr-lowercase': true,
17 'attr-value-double-quotes': true,
18 'doctype-first': true,
19 'tag-pair': true,
20 'spec-char-escape': true,
21 'id-unique': true,
22 'src-not-empty': true,
23 'attr-no-duplication': true
24 };
25
261 HTMLHint.addRule = function(rule){
2721 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, ruleset){
3180 var id;
32 // parse inline ruleset
3380 html = html.replace(/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i, function(all, strRuleset){
342 if(ruleset === undefined){
350 ruleset = {};
36 }
372 strRuleset.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g, function(all, key, value){
382 if(value === 'false'){
391 value = false;
40 }
411 else if(value === 'true'){
421 value = true;
43 }
442 ruleset[key] = value;
45 });
462 return '';
47 });
4880 if(ruleset === undefined){
492 ruleset = HTMLHint.defaultRuleset;
50 }
51
5280 var parser = new HTMLParser();
5380 var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
54
5580 var rules = HTMLHint.rules,
56 rule;
5780 for (id in ruleset){
5896 rule = rules[id];
5996 if (rule !== undefined && ruleset[id] !== false){
6094 rule.init(parser, reporter, ruleset[id]);
61 }
62 }
63
6480 parser.parse(html);
65
6680 return reporter.messages;
67 };
68
691 return HTMLHint;
70
71})();
72
731if (typeof exports === 'object' && exports){
741 exports.HTMLHint = HTMLHint;
75}
76/**
77 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
78 * MIT Licensed
79 */
801(function(HTMLHint, undefined){
81
821 var Reporter = function(){
8380 var self = this;
8480 self._init.apply(self,arguments);
85 };
86
871 Reporter.prototype = {
88 _init: function(lines, ruleset){
8980 var self = this;
9080 self.lines = lines;
9180 self.ruleset = ruleset;
9280 self.messages = [];
93 },
94 //错误
95 error: function(message, line, col, rule, raw){
9647 this.report('error', message, line, col, rule, raw);
97 },
98 //警告
99 warn: function(message, line, col, rule, raw){
10044 this.report('warning', message, line, col, rule, raw);
101 },
102 //信息
103 info: function(message, line, col, rule, raw){
1040 this.report('info', message, line, col, rule, raw);
105 },
106 //报告
107 report: function(type, message, line, col, rule, raw){
10891 var self = this;
10991 self.messages.push({
110 type: type,
111 message: message,
112 raw: raw,
113 evidence: self.lines[line-1],
114 line: line,
115 col: col,
116 rule: {
117 id: rule.id,
118 description: rule.description,
119 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
120 }
121 });
122 }
123 };
124
1251 HTMLHint.Reporter = Reporter;
126
127})(HTMLHint);
128/**
129 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
130 * MIT Licensed
131 */
1321var HTMLParser = (function(undefined){
133
1341 var HTMLParser = function(){
135105 var self = this;
136105 self._init.apply(self,arguments);
137 };
138
1391 HTMLParser.prototype = {
140 _init: function(){
141105 var self = this;
142105 self._listeners = {};
143105 self._mapCdataTags = self.makeMap("script,style");
144105 self._arrBlocks = [];
145 },
146
147 makeMap: function(str){
148112 var obj = {}, items = str.split(",");
149112 for ( var i = 0; i < items.length; i++ ){
150308 obj[ items[i] ] = true;
151 }
152112 return obj;
153 },
154
155 // parse html code
156 parse: function(html){
157
158105 var self = this,
159 mapCdataTags = self._mapCdataTags;
160
161105 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
162 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
163 regLine = /\r?\n/g;
164
165105 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
166105 var lastLineIndex = 0, line = 1;
167105 var arrBlocks = self._arrBlocks;
168
169105 self.fire('start', {
170 pos: 0,
171 line: 1,
172 col: 1
173 });
174
175105 while((match = regTag.exec(html))){
176252 matchIndex = match.index;
177252 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
17876 text = html.substring(lastIndex, matchIndex);
17976 if(tagCDATA){
18010 arrCDATA.push(text);
181 }
182 else{//文本
18366 saveBlock('text', text, lastIndex);
184 }
185 }
186252 lastIndex = regTag.lastIndex;
187
188252 if((tagName = match[1])){
18989 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
19015 text = arrCDATA.join('');
19115 saveBlock('cdata', text, lastCDATAIndex, {
192 'tagName': tagCDATA,
193 'attrs': attrsCDATA
194 });
19515 tagCDATA = null;
19615 attrsCDATA = null;
19715 arrCDATA = null;
198 }
19989 if(!tagCDATA){
200 //标签结束
20188 saveBlock('tagend', match[0], matchIndex, {
202 'tagName': tagName
203 });
20488 continue;
205 }
206 }
207
208164 if(tagCDATA){
2091 arrCDATA.push(match[0]);
210 }
211 else{
212163 if((tagName = match[4])){//标签开始
213150 arrAttrs = [];
214150 var attrs = match[5],
215 attrMatch,
216 attrMatchCount = 0;
217150 while((attrMatch = regAttr.exec(attrs))){
218143 var name = attrMatch[1],
219 quote = attrMatch[2] ? attrMatch[2] :
220 attrMatch[4] ? attrMatch[4] : '',
221 value = attrMatch[3] ? attrMatch[3] :
222 attrMatch[5] ? attrMatch[5] :
223 attrMatch[6] ? attrMatch[6] : '';
224143 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
225143 attrMatchCount += attrMatch[0].length;
226 }
227150 if(attrMatchCount === attrs.length){
228150 saveBlock('tagstart', match[0], matchIndex, {
229 'tagName': tagName,
230 'attrs': arrAttrs,
231 'close': match[6]
232 });
233150 if(mapCdataTags[tagName]){
23415 tagCDATA = tagName;
23515 attrsCDATA = arrAttrs.concat();
23615 arrCDATA = [];
23715 lastCDATAIndex = lastIndex;
238 }
239 }
240 else{//如果出现漏匹配,则把当前内容匹配为text
2410 saveBlock('text', match[0], matchIndex);
242 }
243 }
24413 else if(match[2] || match[3]){//注释标签
24513 saveBlock('comment', match[0], matchIndex, {
246 'content': match[2] || match[3],
247 'long': match[2]?true:false
248 });
249 }
250 }
251 }
252
253105 if(html.length > lastIndex){
254 //结尾文本
25513 text = html.substring(lastIndex, html.length);
25613 saveBlock('text', text, lastIndex);
257 }
258
259105 self.fire('end', {
260 pos: lastIndex,
261 line: line,
262 col: lastIndex - lastLineIndex + 1
263 });
264
265 //存储区块
266105 function saveBlock(type, raw, pos, data){
267345 var col = pos - lastLineIndex + 1;
268345 if(data === undefined){
26979 data = {};
270 }
271345 data.raw = raw;
272345 data.pos = pos;
273345 data.line = line;
274345 data.col = col;
275345 arrBlocks.push(data);
276345 self.fire(type, data);
277345 var lineMatch;
278345 while((lineMatch = regLine.exec(raw))){
27922 line ++;
28022 lastLineIndex = pos + regLine.lastIndex;
281 }
282 }
283
284 },
285
286 // add event
287 addListener: function(types, listener){
288134 var _listeners = this._listeners;
289134 var arrTypes = types.split(/[,\s]/), type;
290134 for(var i=0, l = arrTypes.length;i<l;i++){
291138 type = arrTypes[i];
292138 if (_listeners[type] === undefined){
293124 _listeners[type] = [];
294 }
295138 _listeners[type].push(listener);
296 }
297 },
298
299 // fire event
300 fire: function(type, data){
301555 if (data === undefined){
3020 data = {};
303 }
304555 data.type = type;
305555 var self = this,
306 listeners = [],
307 listenersType = self._listeners[type],
308 listenersAll = self._listeners['all'];
309555 if (listenersType !== undefined){
310156 listeners = listeners.concat(listenersType);
311 }
312555 if (listenersAll !== undefined){
313130 listeners = listeners.concat(listenersAll);
314 }
315555 for (var i = 0, l = listeners.length; i < l; i++){
316299 listeners[i].call(self, data);
317 }
318 },
319
320 // remove event
321 removeListener: function(type, listener){
32214 var listenersType = this._listeners[type];
32314 if(listenersType !== undefined){
32412 for (var i = 0, l = listenersType.length; i < l; i++){
3259 if (listenersType[i] === listener){
3269 listenersType.splice(i, 1);
3279 break;
328 }
329 }
330 }
331 },
332
333 //fix pos if event.raw have \n
334 fixPos: function(event, index){
3357 var text = event.raw.substr(0, index);
3367 var arrLines = text.split(/\r?\n/),
337 lineCount = arrLines.length - 1,
338 line = event.line, col;
3397 if(lineCount > 0){
3402 line += lineCount;
3412 col = arrLines[lineCount].length + 1;
342 }
343 else{
3445 col = event.col + index;
345 }
3467 return {
347 line: line,
348 col: col
349 };
350 },
351
352 // covert array type of attrs to map
353 getMapAttrs: function(arrAttrs){
3546 var mapAttrs = {},
355 attr;
3566 for(var i=0,l=arrAttrs.length;i<l;i++){
3576 attr = arrAttrs[i];
3586 mapAttrs[attr.name] = attr.value;
359 }
3606 return mapAttrs;
361 }
362 };
363
3641 return HTMLParser;
365
366})();
367
3681if (typeof exports === 'object' && exports){
3691 exports.HTMLParser = HTMLParser;
370}
371/**
372 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
373 * MIT Licensed
374 */
3751HTMLHint.addRule({
376 id: 'attr-lowercase',
377 description: 'Attribute name must be lowercase.',
378 init: function(parser, reporter){
3794 var self = this;
3804 parser.addListener('tagstart', function(event){
3816 var attrs = event.attrs,
382 attr,
383 col = event.col + event.tagName.length + 1;
3846 for(var i=0, l=attrs.length;i<l;i++){
3859 attr = attrs[i];
3869 var attrName = attr.name;
3879 if(attrName !== attrName.toLowerCase()){
3883 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
389 }
390 }
391 });
392 }
393});
394/**
395 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
396 * MIT Licensed
397 */
3981HTMLHint.addRule({
399 id: 'attr-no-duplication',
400 description: 'Attribute name can not been duplication.',
401 init: function(parser, reporter){
4024 var self = this;
4034 parser.addListener('tagstart', function(event){
4046 var attrs = event.attrs;
4056 var attr;
4066 var attrName;
4076 var col = event.col + event.tagName.length + 1;
408
4096 var mapAttrName = {};
4106 for(var i=0, l=attrs.length;i<l;i++){
41110 attr = attrs[i];
41210 attrName = attr.name;
41310 if(mapAttrName[attrName] === true){
4142 reporter.error('The name of attribute [ '+attr.name+' ] been duplication.', event.line, col + attr.index, self, attr.raw);
415 }
41610 mapAttrName[attrName] = true;
417 }
418 });
419 }
420});
421/**
422 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
423 * MIT Licensed
424 */
4251HTMLHint.addRule({
426 id: 'attr-value-double-quotes',
427 description: 'Attribute value must closed by double quotes.',
428 init: function(parser, reporter){
4294 var self = this;
4304 parser.addListener('tagstart', function(event){
4316 var attrs = event.attrs,
432 attr,
433 col = event.col + event.tagName.length + 1;
4346 for(var i=0, l=attrs.length;i<l;i++){
43513 attr = attrs[i];
43613 if((attr.value !== '' && attr.quote !== '"') ||
437 (attr.value === '' && attr.quote === "'")){
4384 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
439 }
440 }
441 });
442 }
443});
444/**
445 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
446 * MIT Licensed
447 */
4481HTMLHint.addRule({
449 id: 'attr-value-not-empty',
450 description: 'Attribute must set value.',
451 init: function(parser, reporter){
4523 var self = this;
4533 parser.addListener('tagstart', function(event){
4543 var attrs = event.attrs,
455 attr,
456 col = event.col + event.tagName.length + 1;
4573 for(var i=0, l=attrs.length;i<l;i++){
4583 attr = attrs[i];
4593 if(attr.quote === '' && attr.value === ''){
4601 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
461 }
462 }
463 });
464 }
465});
466/**
467 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
468 * MIT Licensed
469 */
4701HTMLHint.addRule({
471 id: 'csslint',
472 description: 'Scan css with csslint.',
473 init: function(parser, reporter, options){
4741 var self = this;
4751 parser.addListener('cdata', function(event){
4761 if(event.tagName.toLowerCase() === 'style'){
477
4781 var cssVerify;
479
4801 if(typeof exports === 'object' && require){
4811 cssVerify = require("csslint").CSSLint.verify;
482 }
483 else{
4840 cssVerify = CSSLint.verify;
485 }
486
4871 if(options !== undefined){
4881 var styleLine = event.line - 1,
489 styleCol = event.col - 1;
4901 try{
4911 var messages = cssVerify(event.raw, options).messages;
4921 messages.forEach(function(error){
4932 var line = error.line;
4942 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
495 });
496 }
497 catch(e){}
498 }
499
500 }
501 });
502 }
503});
504/**
505 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
506 * MIT Licensed
507 */
5081HTMLHint.addRule({
509 id: 'doctype-first',
510 description: 'Doctype must be first.',
511 init: function(parser, reporter){
5124 var self = this;
5134 var allEvent = function(event){
5148 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
5154 return;
516 }
5174 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5183 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
519 }
5204 parser.removeListener('all', allEvent);
521 };
5224 parser.addListener('all', allEvent);
523 }
524});
525/**
526 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
527 * MIT Licensed
528 */
5291HTMLHint.addRule({
530 id: 'doctype-html5',
531 description: 'Doctype must be html5.',
532 init: function(parser, reporter){
5332 var self = this;
5342 function onComment(event){
5359 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5361 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
537 }
538 }
5392 function onTagStart(){
5402 parser.removeListener('comment', onComment);
5412 parser.removeListener('tagstart', onTagStart);
542 }
5432 parser.addListener('all', onComment);
5442 parser.addListener('tagstart', onTagStart);
545 }
546});
547/**
548 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
549 * MIT Licensed
550 */
5511HTMLHint.addRule({
552 id: 'head-script-disabled',
553 description: 'The script tag can not be used in head.',
554 init: function(parser, reporter){
5553 var self = this;
5563 function onTagStart(event){
5575 if(event.tagName.toLowerCase() === 'script'){
5582 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
559 }
560 }
5613 function onTagEnd(event){
5627 if(event.tagName.toLowerCase() === 'head'){
5633 parser.removeListener('tagstart', onTagStart);
5643 parser.removeListener('tagstart', onTagEnd);
565 }
566 }
5673 parser.addListener('tagstart', onTagStart);
5683 parser.addListener('tagend', onTagEnd);
569 }
570});
571/**
572 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
573 * MIT Licensed
574 */
5751HTMLHint.addRule({
576 id: 'href-abs-or-rel',
577 description: 'Href must be absolute or relative.',
578 init: function(parser, reporter, options){
5794 var self = this;
580
5814 var hrefMode = options === 'abs' ? 'absolute' : 'relative';
582
5834 parser.addListener('tagstart', function(event){
58416 var attrs = event.attrs;
58516 var attr;
58616 var col = event.col + event.tagName.length + 1;
587
58816 for(var i=0, l=attrs.length;i<l;i++){
58916 attr = attrs[i];
59016 if(attr.name === 'href'){
59116 if((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) ||
592 (hrefMode === 'relative' && /^https?:\/\//.test(attr.value) === true)){
5934 reporter.error('The value of href [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw);
594 }
59516 break;
596 }
597 }
598 });
599 }
600});
601/**
602 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
603 * MIT Licensed
604 */
6051HTMLHint.addRule({
606 id: 'id-class-ad-disabled',
607 description: 'Id and class can not use ad keyword, it will blocked by adblock software.',
608 init: function(parser, reporter){
60917 var self = this;
61017 parser.addListener('tagstart', function(event){
61117 var attrs = event.attrs;
61217 var attr;
61317 var attrName;
61417 var col = event.col + event.tagName.length + 1;
615
61617 for(var i=0, l=attrs.length;i<l;i++){
61720 attr = attrs[i];
61820 attrName = attr.name;
61920 if(/^(id|class)$/i.test(attrName)){
62020 if(/(^|[-\_])ad([-\_]|$)/i.test(attr.value)){
62114 reporter.warn('The value of '+attrName+' can not use ad keyword.', event.line, col + attr.index, self, attr.raw);
622 }
623 }
624 }
625 });
626 }
627});
628/**
629 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
630 * MIT Licensed
631 */
6321HTMLHint.addRule({
633 id: 'id-class-value',
634 description: 'Id and class value must meet some rules.',
635 init: function(parser, reporter, options){
6368 var self = this;
6378 var arrRules = {
638 'underline': {
639 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
640 'message': 'Id and class value must lower case and split by underline.'
641 },
642 'dash': {
643 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
644 'message': 'Id and class value must lower case and split by dash.'
645 },
646 'hump': {
647 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
648 'message': 'Id and class value must meet hump style.'
649 }
650 }, rule;
6518 if(typeof options === 'string'){
6526 rule = arrRules[options];
653 }
654 else{
6552 rule = options;
656 }
6578 if(rule && rule.regId){
6588 var regId = rule.regId,
659 message = rule.message;
6608 parser.addListener('tagstart', function(event){
6618 var attrs = event.attrs,
662 attr,
663 col = event.col + event.tagName.length + 1;
6648 for(var i=0, l1=attrs.length;i<l1;i++){
66516 attr = attrs[i];
66616 if(attr.name.toLowerCase() === 'id'){
6678 if(regId.test(attr.value) === false){
6684 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
669 }
670 }
67116 if(attr.name.toLowerCase() === 'class'){
6728 var arrClass = attr.value.split(/\s+/g), classValue;
6738 for(var j=0, l2=arrClass.length;j<l2;j++){
6748 classValue = arrClass[j];
6758 if(classValue && regId.test(classValue) === false){
6764 reporter.warn(message, event.line, col + attr.index, self, classValue);
677 }
678 }
679 }
680 }
681 });
682 }
683 }
684});
685/**
686 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
687 * MIT Licensed
688 */
6891HTMLHint.addRule({
690 id: 'id-unique',
691 description: 'Id must be unique.',
692 init: function(parser, reporter){
6934 var self = this;
6944 var mapIdCount = {};
6954 parser.addListener('tagstart', function(event){
6968 var attrs = event.attrs,
697 attr,
698 id,
699 col = event.col + event.tagName.length + 1;
7008 for(var i=0, l=attrs.length;i<l;i++){
70111 attr = attrs[i];
70211 if(attr.name.toLowerCase() === 'id'){
7036 id = attr.value;
7046 if(id){
7056 if(mapIdCount[id] === undefined){
7064 mapIdCount[id] = 1;
707 }
708 else{
7092 mapIdCount[id] ++;
710 }
7116 if(mapIdCount[id] > 1){
7122 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
713 }
714 }
7156 break;
716 }
717 }
718 });
719 }
720});
721/**
722 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
723 * MIT Licensed
724 */
7251HTMLHint.addRule({
726 id: 'img-alt-require',
727 description: 'Alt of img tag must be set value.',
728 init: function(parser, reporter){
7294 var self = this;
7304 parser.addListener('tagstart', function(event){
7314 if(event.tagName.toLowerCase() === 'img'){
7324 var attrs = event.attrs;
7334 var haveAlt = false;
7344 for(var i=0, l=attrs.length;i<l;i++){
7359 if(attrs[i].name.toLowerCase() === 'alt'){
7362 haveAlt = true;
7372 break;
738 }
739 }
7404 if(haveAlt === false){
7412 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
742 }
743 }
744 });
745 }
746});
747/**
748 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
749 * MIT Licensed
750 */
7511HTMLHint.addRule({
752 id: 'jshint',
753 description: 'Scan script with jshint.',
754 init: function(parser, reporter, options){
7554 var self = this;
7564 parser.addListener('cdata', function(event){
7574 if(event.tagName.toLowerCase() === 'script'){
758
7594 var mapAttrs = parser.getMapAttrs(event.attrs),
760 type = mapAttrs.type;
761
762 // Only scan internal javascript
7634 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
7642 return;
765 }
766
7672 var jsVerify;
768
7692 if(typeof exports === 'object' && require){
7702 jsVerify = require('jshint').JSHINT;
771 }
772 else{
7730 jsVerify = JSHINT;
774 }
775
7762 if(options !== undefined){
7772 var styleLine = event.line - 1,
778 styleCol = event.col - 1;
7792 var code = event.raw.replace(/\t/g,' ');
7802 try{
7812 var status = jsVerify(code, options);
7822 if(status === false){
7832 jsVerify.errors.forEach(function(error){
7848 var line = error.line;
7858 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
786 });
787 }
788 }
789 catch(e){}
790 }
791
792 }
793 });
794 }
795});
796/**
797 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
798 * MIT Licensed
799 */
8001HTMLHint.addRule({
801 id: 'space-tab-mixed-disabled',
802 description: 'Spaces and tabs can not mixed in front of line.',
803 init: function(parser, reporter){
8046 var self = this;
8056 parser.addListener('text', function(event){
80612 var raw = event.raw;
80712 var reMixed = /(^|\r?\n)( +\t|\t+ )/g;
80812 var match;
80912 while((match = reMixed.exec(raw))){
8103 var fixedPos = parser.fixPos(event, match.index + match[1].length);
8113 reporter.warn('Mixed spaces and tabs in front of line.', fixedPos.line, 1, self, event.raw);
812 }
813 });
814 }
815});
816/**
817 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
818 * MIT Licensed
819 */
8201HTMLHint.addRule({
821 id: 'spec-char-escape',
822 description: 'Special characters must be escaped.',
823 init: function(parser, reporter){
8244 var self = this;
8254 parser.addListener('text', function(event){
8264 var raw = event.raw,
827 reSpecChar = /[<>]/g,
828 match;
8294 while((match = reSpecChar.exec(raw))){
8304 var fixedPos = parser.fixPos(event, match.index);
8314 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
832 }
833 });
834 }
835});
836/**
837 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
838 * MIT Licensed
839 */
8401HTMLHint.addRule({
841 id: 'src-not-empty',
842 description: 'Src of img(script,link) must set value.',
843 init: function(parser, reporter){
8445 var self = this;
8455 parser.addListener('tagstart', function(event){
84632 var tagName = event.tagName,
847 attrs = event.attrs,
848 attr,
849 col = event.col + tagName.length + 1;
85032 for(var i=0, l=attrs.length;i<l;i++){
85136 attr = attrs[i];
85236 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
853 (tagName === 'link' && attr.name === 'href') ||
854 (tagName === 'object' && attr.name === 'data')) &&
855 attr.value === ''){
85615 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
857 }
858 }
859 });
860 }
861});
862/**
863 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
864 * MIT Licensed
865 */
8661HTMLHint.addRule({
867 id: 'style-disabled',
868 description: 'Style tag can not be use.',
869 init: function(parser, reporter){
8702 var self = this;
8712 parser.addListener('tagstart', function(event){
8724 if(event.tagName.toLowerCase() === 'style'){
8731 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
874 }
875 });
876 }
877});
878/**
879 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
880 * MIT Licensed
881 */
8821HTMLHint.addRule({
883 id: 'tag-pair',
884 description: 'Tag must be paired.',
885 init: function(parser, reporter){
8865 var self = this;
8875 var stack=[],
888 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8895 parser.addListener('tagstart', function(event){
8908 var tagName = event.tagName.toLowerCase();
8918 if (mapEmptyTags[tagName] === undefined && !event.close){
8927 stack.push(tagName);
893 }
894 });
8955 parser.addListener('tagend', function(event){
8964 var tagName = event.tagName.toLowerCase();
897 //向上寻找匹配的开始标签
8984 for(var pos = stack.length-1;pos >= 0; pos--){
8994 if(stack[pos] === tagName){
9003 break;
901 }
902 }
9034 if(pos >= 0){
9043 var arrTags = [];
9053 for(var i=stack.length-1;i>pos;i--){
9061 arrTags.push('</'+stack[i]+'>');
907 }
9083 if(arrTags.length > 0){
9091 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
910 }
9113 stack.length=pos;
912 }
913 else{
9141 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
915 }
916 });
9175 parser.addListener('end', function(event){
9185 var arrTags = [];
9195 for(var i=stack.length-1;i>=0;i--){
9203 arrTags.push('</'+stack[i]+'>');
921 }
9225 if(arrTags.length > 0){
9233 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
924 }
925 });
926 }
927});
928/**
929 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
930 * MIT Licensed
931 */
9321HTMLHint.addRule({
933 id: 'tag-self-close',
934 description: 'The empty tag must closed by self.',
935 init: function(parser, reporter){
9362 var self = this;
9372 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9382 parser.addListener('tagstart', function(event){
9394 var tagName = event.tagName.toLowerCase();
9404 if(mapEmptyTags[tagName] !== undefined){
9414 if(!event.close){
9422 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
943 }
944 }
945 });
946 }
947});
948/**
949 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
950 * MIT Licensed
951 */
9521HTMLHint.addRule({
953 id: 'tagname-lowercase',
954 description: 'Tagname must be lowercase.',
955 init: function(parser, reporter){
9564 var self = this;
9574 parser.addListener('tagstart,tagend', function(event){
95813 var tagName = event.tagName;
95913 if(tagName !== tagName.toLowerCase()){
9605 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
961 }
962 });
963 }
964});
\ No newline at end of file +

Coverage

98%
406
400
6

htmlhint.js

98%
406
400
6
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var HTMLHint = {};
8
91 HTMLHint.version = '@VERSION';
10
111 HTMLHint.rules = {};
12
13 //默认配置
141 HTMLHint.defaultRuleset = {
15 'tagname-lowercase': true,
16 'attr-lowercase': true,
17 'attr-value-double-quotes': true,
18 'doctype-first': true,
19 'tag-pair': true,
20 'spec-char-escape': true,
21 'id-unique': true,
22 'src-not-empty': true,
23 'attr-no-duplication': true
24 };
25
261 HTMLHint.addRule = function(rule){
2721 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, ruleset){
31 // parse inline ruleset
3280 html = html.replace(/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i, function(all, strRuleset){
332 if(ruleset === undefined){
340 ruleset = {};
35 }
362 strRuleset.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g, function(all, key, value){
372 if(value === 'false'){
381 value = false;
39 }
401 else if(value === 'true'){
411 value = true;
42 }
432 ruleset[key] = value;
44 });
452 return '';
46 });
4780 ruleset = ruleset || HTMLHint.defaultRuleset;
48
4980 var parser = new HTMLParser();
5080 var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
51
5280 var rules = HTMLHint.rules,
53 rule;
5480 for (var id in ruleset){
5596 rule = rules[id];
5696 if (rule !== undefined && ruleset[id] !== false){
5794 rule.init(parser, reporter, ruleset[id]);
58 }
59 }
60
6180 parser.parse(html);
62
6380 return reporter.messages;
64 };
65
661 return HTMLHint;
67
68})();
69
701if (typeof exports === 'object' && exports){
711 exports.HTMLHint = HTMLHint;
72}
73/**
74 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
75 * MIT Licensed
76 */
771(function(HTMLHint, undefined){
78
791 var Reporter = function(){
8080 var self = this;
8180 self._init.apply(self,arguments);
82 };
83
841 Reporter.prototype = {
85 _init: function(lines, ruleset){
8680 var self = this;
8780 self.lines = lines;
8880 self.ruleset = ruleset;
8980 self.messages = [];
90 },
91 //错误
92 error: function(message, line, col, rule, raw){
9343 this.report('error', message, line, col, rule, raw);
94 },
95 //警告
96 warn: function(message, line, col, rule, raw){
9748 this.report('warning', message, line, col, rule, raw);
98 },
99 //信息
100 info: function(message, line, col, rule, raw){
1010 this.report('info', message, line, col, rule, raw);
102 },
103 //报告
104 report: function(type, message, line, col, rule, raw){
10591 var self = this;
10691 self.messages.push({
107 type: type,
108 message: message,
109 raw: raw,
110 evidence: self.lines[line-1],
111 line: line,
112 col: col,
113 rule: {
114 id: rule.id,
115 description: rule.description,
116 link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
117 }
118 });
119 }
120 };
121
1221 HTMLHint.Reporter = Reporter;
123
124})(HTMLHint);
125/**
126 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
127 * MIT Licensed
128 */
1291var HTMLParser = (function(undefined){
130
1311 var HTMLParser = function(){
132105 var self = this;
133105 self._init.apply(self,arguments);
134 };
135
1361 HTMLParser.prototype = {
137 _init: function(){
138105 var self = this;
139105 self._listeners = {};
140105 self._mapCdataTags = self.makeMap("script,style");
141105 self._arrBlocks = [];
142 },
143
144 makeMap: function(str){
145112 var obj = {}, items = str.split(",");
146112 for ( var i = 0; i < items.length; i++ ){
147308 obj[ items[i] ] = true;
148 }
149112 return obj;
150 },
151
152 // parse html code
153 parse: function(html){
154
155105 var self = this,
156 mapCdataTags = self._mapCdataTags;
157
158105 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
159 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
160 regLine = /\r?\n/g;
161
162105 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
163105 var lastLineIndex = 0, line = 1;
164105 var arrBlocks = self._arrBlocks;
165
166105 self.fire('start', {
167 pos: 0,
168 line: 1,
169 col: 1
170 });
171
172105 while((match = regTag.exec(html))){
173252 matchIndex = match.index;
174252 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
17576 text = html.substring(lastIndex, matchIndex);
17676 if(tagCDATA){
17710 arrCDATA.push(text);
178 }
179 else{//文本
18066 saveBlock('text', text, lastIndex);
181 }
182 }
183252 lastIndex = regTag.lastIndex;
184
185252 if((tagName = match[1])){
18689 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
18715 text = arrCDATA.join('');
18815 saveBlock('cdata', text, lastCDATAIndex, {
189 'tagName': tagCDATA,
190 'attrs': attrsCDATA
191 });
19215 tagCDATA = null;
19315 attrsCDATA = null;
19415 arrCDATA = null;
195 }
19689 if(!tagCDATA){
197 //标签结束
19888 saveBlock('tagend', match[0], matchIndex, {
199 'tagName': tagName
200 });
20188 continue;
202 }
203 }
204
205164 if(tagCDATA){
2061 arrCDATA.push(match[0]);
207 }
208 else{
209163 if((tagName = match[4])){//标签开始
210150 arrAttrs = [];
211150 var attrs = match[5],
212 attrMatch,
213 attrMatchCount = 0;
214150 while((attrMatch = regAttr.exec(attrs))){
215143 var name = attrMatch[1],
216 quote = attrMatch[2] ? attrMatch[2] :
217 attrMatch[4] ? attrMatch[4] : '',
218 value = attrMatch[3] ? attrMatch[3] :
219 attrMatch[5] ? attrMatch[5] :
220 attrMatch[6] ? attrMatch[6] : '';
221143 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
222143 attrMatchCount += attrMatch[0].length;
223 }
224150 if(attrMatchCount === attrs.length){
225150 saveBlock('tagstart', match[0], matchIndex, {
226 'tagName': tagName,
227 'attrs': arrAttrs,
228 'close': match[6]
229 });
230150 if(mapCdataTags[tagName]){
23115 tagCDATA = tagName;
23215 attrsCDATA = arrAttrs.concat();
23315 arrCDATA = [];
23415 lastCDATAIndex = lastIndex;
235 }
236 }
237 else{//如果出现漏匹配,则把当前内容匹配为text
2380 saveBlock('text', match[0], matchIndex);
239 }
240 }
24113 else if(match[2] || match[3]){//注释标签
24213 saveBlock('comment', match[0], matchIndex, {
243 'content': match[2] || match[3],
244 'long': match[2]?true:false
245 });
246 }
247 }
248 }
249
250105 if(html.length > lastIndex){
251 //结尾文本
25213 text = html.substring(lastIndex, html.length);
25313 saveBlock('text', text, lastIndex);
254 }
255
256105 self.fire('end', {
257 pos: lastIndex,
258 line: line,
259 col: lastIndex - lastLineIndex + 1
260 });
261
262 //存储区块
263105 function saveBlock(type, raw, pos, data){
264345 var col = pos - lastLineIndex + 1;
265345 if(data === undefined){
26679 data = {};
267 }
268345 data.raw = raw;
269345 data.pos = pos;
270345 data.line = line;
271345 data.col = col;
272345 arrBlocks.push(data);
273345 self.fire(type, data);
274345 var lineMatch;
275345 while((lineMatch = regLine.exec(raw))){
27622 line ++;
27722 lastLineIndex = pos + regLine.lastIndex;
278 }
279 }
280
281 },
282
283 // add event
284 addListener: function(types, listener){
285134 var _listeners = this._listeners;
286134 var arrTypes = types.split(/[,\s]/), type;
287134 for(var i=0, l = arrTypes.length;i<l;i++){
288138 type = arrTypes[i];
289138 if (_listeners[type] === undefined){
290124 _listeners[type] = [];
291 }
292138 _listeners[type].push(listener);
293 }
294 },
295
296 // fire event
297 fire: function(type, data){
298555 if (data === undefined){
2990 data = {};
300 }
301555 data.type = type;
302555 var self = this,
303 listeners = [],
304 listenersType = self._listeners[type],
305 listenersAll = self._listeners['all'];
306555 if (listenersType !== undefined){
307156 listeners = listeners.concat(listenersType);
308 }
309555 if (listenersAll !== undefined){
310130 listeners = listeners.concat(listenersAll);
311 }
312555 for (var i = 0, l = listeners.length; i < l; i++){
313299 listeners[i].call(self, data);
314 }
315 },
316
317 // remove event
318 removeListener: function(type, listener){
31914 var listenersType = this._listeners[type];
32014 if(listenersType !== undefined){
32112 for (var i = 0, l = listenersType.length; i < l; i++){
3229 if (listenersType[i] === listener){
3239 listenersType.splice(i, 1);
3249 break;
325 }
326 }
327 }
328 },
329
330 //fix pos if event.raw have \n
331 fixPos: function(event, index){
3327 var text = event.raw.substr(0, index);
3337 var arrLines = text.split(/\r?\n/),
334 lineCount = arrLines.length - 1,
335 line = event.line, col;
3367 if(lineCount > 0){
3372 line += lineCount;
3382 col = arrLines[lineCount].length + 1;
339 }
340 else{
3415 col = event.col + index;
342 }
3437 return {
344 line: line,
345 col: col
346 };
347 },
348
349 // covert array type of attrs to map
350 getMapAttrs: function(arrAttrs){
3516 var mapAttrs = {},
352 attr;
3536 for(var i=0,l=arrAttrs.length;i<l;i++){
3546 attr = arrAttrs[i];
3556 mapAttrs[attr.name] = attr.value;
356 }
3576 return mapAttrs;
358 }
359 };
360
3611 return HTMLParser;
362
363})();
364
3651if (typeof exports === 'object' && exports){
3661 exports.HTMLParser = HTMLParser;
367}
368/**
369 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
370 * MIT Licensed
371 */
3721HTMLHint.addRule({
373 id: 'attr-lowercase',
374 description: 'Attribute name must be lowercase.',
375 init: function(parser, reporter){
3764 var self = this;
3774 parser.addListener('tagstart', function(event){
3786 var attrs = event.attrs,
379 attr,
380 col = event.col + event.tagName.length + 1;
3816 for(var i=0, l=attrs.length;i<l;i++){
3829 attr = attrs[i];
3839 var attrName = attr.name;
3849 if(attrName !== attrName.toLowerCase()){
3853 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
386 }
387 }
388 });
389 }
390});
391/**
392 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
393 * MIT Licensed
394 */
3951HTMLHint.addRule({
396 id: 'attr-no-duplication',
397 description: 'Attribute name can not been duplication.',
398 init: function(parser, reporter){
3994 var self = this;
4004 parser.addListener('tagstart', function(event){
4016 var attrs = event.attrs;
4026 var attr;
4036 var attrName;
4046 var col = event.col + event.tagName.length + 1;
405
4066 var mapAttrName = {};
4076 for(var i=0, l=attrs.length;i<l;i++){
40810 attr = attrs[i];
40910 attrName = attr.name;
41010 if(mapAttrName[attrName] === true){
4112 reporter.error('The name of attribute [ '+attr.name+' ] been duplication.', event.line, col + attr.index, self, attr.raw);
412 }
41310 mapAttrName[attrName] = true;
414 }
415 });
416 }
417});
418/**
419 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
420 * MIT Licensed
421 */
4221HTMLHint.addRule({
423 id: 'attr-value-double-quotes',
424 description: 'Attribute value must closed by double quotes.',
425 init: function(parser, reporter){
4264 var self = this;
4274 parser.addListener('tagstart', function(event){
4286 var attrs = event.attrs,
429 attr,
430 col = event.col + event.tagName.length + 1;
4316 for(var i=0, l=attrs.length;i<l;i++){
43213 attr = attrs[i];
43313 if((attr.value !== '' && attr.quote !== '"') ||
434 (attr.value === '' && attr.quote === "'")){
4354 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
436 }
437 }
438 });
439 }
440});
441/**
442 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
443 * MIT Licensed
444 */
4451HTMLHint.addRule({
446 id: 'attr-value-not-empty',
447 description: 'Attribute must set value.',
448 init: function(parser, reporter){
4493 var self = this;
4503 parser.addListener('tagstart', function(event){
4513 var attrs = event.attrs,
452 attr,
453 col = event.col + event.tagName.length + 1;
4543 for(var i=0, l=attrs.length;i<l;i++){
4553 attr = attrs[i];
4563 if(attr.quote === '' && attr.value === ''){
4571 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
458 }
459 }
460 });
461 }
462});
463/**
464 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
465 * MIT Licensed
466 */
4671HTMLHint.addRule({
468 id: 'csslint',
469 description: 'Scan css with csslint.',
470 init: function(parser, reporter, options){
4711 var self = this;
4721 parser.addListener('cdata', function(event){
4731 if(event.tagName.toLowerCase() === 'style'){
474
4751 var cssVerify;
476
4771 if(typeof exports === 'object' && require){
4781 cssVerify = require("csslint").CSSLint.verify;
479 }
480 else{
4810 cssVerify = CSSLint.verify;
482 }
483
4841 if(options !== undefined){
4851 var styleLine = event.line - 1,
486 styleCol = event.col - 1;
4871 try{
4881 var messages = cssVerify(event.raw, options).messages;
4891 messages.forEach(function(error){
4902 var line = error.line;
4912 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
492 });
493 }
494 catch(e){}
495 }
496
497 }
498 });
499 }
500});
501/**
502 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
503 * MIT Licensed
504 */
5051HTMLHint.addRule({
506 id: 'doctype-first',
507 description: 'Doctype must be first.',
508 init: function(parser, reporter){
5094 var self = this;
5104 var allEvent = function(event){
5118 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
5124 return;
513 }
5144 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5153 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
516 }
5174 parser.removeListener('all', allEvent);
518 };
5194 parser.addListener('all', allEvent);
520 }
521});
522/**
523 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
524 * MIT Licensed
525 */
5261HTMLHint.addRule({
527 id: 'doctype-html5',
528 description: 'Doctype must be html5.',
529 init: function(parser, reporter){
5302 var self = this;
5312 function onComment(event){
5329 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5331 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
534 }
535 }
5362 function onTagStart(){
5372 parser.removeListener('comment', onComment);
5382 parser.removeListener('tagstart', onTagStart);
539 }
5402 parser.addListener('all', onComment);
5412 parser.addListener('tagstart', onTagStart);
542 }
543});
544/**
545 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
546 * MIT Licensed
547 */
5481HTMLHint.addRule({
549 id: 'head-script-disabled',
550 description: 'The script tag can not be used in head.',
551 init: function(parser, reporter){
5523 var self = this;
5533 function onTagStart(event){
5545 if(event.tagName.toLowerCase() === 'script'){
5552 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
556 }
557 }
5583 function onTagEnd(event){
5597 if(event.tagName.toLowerCase() === 'head'){
5603 parser.removeListener('tagstart', onTagStart);
5613 parser.removeListener('tagstart', onTagEnd);
562 }
563 }
5643 parser.addListener('tagstart', onTagStart);
5653 parser.addListener('tagend', onTagEnd);
566 }
567});
568/**
569 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
570 * MIT Licensed
571 */
5721HTMLHint.addRule({
573 id: 'href-abs-or-rel',
574 description: 'Href must be absolute or relative.',
575 init: function(parser, reporter, options){
5764 var self = this;
577
5784 var hrefMode = options === 'abs' ? 'absolute' : 'relative';
579
5804 parser.addListener('tagstart', function(event){
58116 var attrs = event.attrs;
58216 var attr;
58316 var col = event.col + event.tagName.length + 1;
584
58516 for(var i=0, l=attrs.length;i<l;i++){
58616 attr = attrs[i];
58716 if(attr.name === 'href'){
58816 if((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) ||
589 (hrefMode === 'relative' && /^https?:\/\//.test(attr.value) === true)){
5904 reporter.warn('The value of href [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw);
591 }
59216 break;
593 }
594 }
595 });
596 }
597});
598/**
599 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
600 * MIT Licensed
601 */
6021HTMLHint.addRule({
603 id: 'id-class-ad-disabled',
604 description: 'Id and class can not use ad keyword, it will blocked by adblock software.',
605 init: function(parser, reporter){
60617 var self = this;
60717 parser.addListener('tagstart', function(event){
60817 var attrs = event.attrs;
60917 var attr;
61017 var attrName;
61117 var col = event.col + event.tagName.length + 1;
612
61317 for(var i=0, l=attrs.length;i<l;i++){
61420 attr = attrs[i];
61520 attrName = attr.name;
61620 if(/^(id|class)$/i.test(attrName)){
61720 if(/(^|[-\_])ad([-\_]|$)/i.test(attr.value)){
61814 reporter.warn('The value of '+attrName+' can not use ad keyword.', event.line, col + attr.index, self, attr.raw);
619 }
620 }
621 }
622 });
623 }
624});
625/**
626 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
627 * MIT Licensed
628 */
6291HTMLHint.addRule({
630 id: 'id-class-value',
631 description: 'Id and class value must meet some rules.',
632 init: function(parser, reporter, options){
6338 var self = this;
6348 var arrRules = {
635 'underline': {
636 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
637 'message': 'Id and class value must lower case and split by underline.'
638 },
639 'dash': {
640 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
641 'message': 'Id and class value must lower case and split by dash.'
642 },
643 'hump': {
644 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
645 'message': 'Id and class value must meet hump style.'
646 }
647 }, rule;
6488 if(typeof options === 'string'){
6496 rule = arrRules[options];
650 }
651 else{
6522 rule = options;
653 }
6548 if(rule && rule.regId){
6558 var regId = rule.regId,
656 message = rule.message;
6578 parser.addListener('tagstart', function(event){
6588 var attrs = event.attrs,
659 attr,
660 col = event.col + event.tagName.length + 1;
6618 for(var i=0, l1=attrs.length;i<l1;i++){
66216 attr = attrs[i];
66316 if(attr.name.toLowerCase() === 'id'){
6648 if(regId.test(attr.value) === false){
6654 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
666 }
667 }
66816 if(attr.name.toLowerCase() === 'class'){
6698 var arrClass = attr.value.split(/\s+/g), classValue;
6708 for(var j=0, l2=arrClass.length;j<l2;j++){
6718 classValue = arrClass[j];
6728 if(classValue && regId.test(classValue) === false){
6734 reporter.warn(message, event.line, col + attr.index, self, classValue);
674 }
675 }
676 }
677 }
678 });
679 }
680 }
681});
682/**
683 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
684 * MIT Licensed
685 */
6861HTMLHint.addRule({
687 id: 'id-unique',
688 description: 'Id must be unique.',
689 init: function(parser, reporter){
6904 var self = this;
6914 var mapIdCount = {};
6924 parser.addListener('tagstart', function(event){
6938 var attrs = event.attrs,
694 attr,
695 id,
696 col = event.col + event.tagName.length + 1;
6978 for(var i=0, l=attrs.length;i<l;i++){
69811 attr = attrs[i];
69911 if(attr.name.toLowerCase() === 'id'){
7006 id = attr.value;
7016 if(id){
7026 if(mapIdCount[id] === undefined){
7034 mapIdCount[id] = 1;
704 }
705 else{
7062 mapIdCount[id] ++;
707 }
7086 if(mapIdCount[id] > 1){
7092 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
710 }
711 }
7126 break;
713 }
714 }
715 });
716 }
717});
718/**
719 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
720 * MIT Licensed
721 */
7221HTMLHint.addRule({
723 id: 'img-alt-require',
724 description: 'Alt of img tag must be set value.',
725 init: function(parser, reporter){
7264 var self = this;
7274 parser.addListener('tagstart', function(event){
7284 if(event.tagName.toLowerCase() === 'img'){
7294 var attrs = event.attrs;
7304 var haveAlt = false;
7314 for(var i=0, l=attrs.length;i<l;i++){
7329 if(attrs[i].name.toLowerCase() === 'alt'){
7332 haveAlt = true;
7342 break;
735 }
736 }
7374 if(haveAlt === false){
7382 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
739 }
740 }
741 });
742 }
743});
744/**
745 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
746 * MIT Licensed
747 */
7481HTMLHint.addRule({
749 id: 'jshint',
750 description: 'Scan script with jshint.',
751 init: function(parser, reporter, options){
7524 var self = this;
7534 parser.addListener('cdata', function(event){
7544 if(event.tagName.toLowerCase() === 'script'){
755
7564 var mapAttrs = parser.getMapAttrs(event.attrs),
757 type = mapAttrs.type;
758
759 // Only scan internal javascript
7604 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
7612 return;
762 }
763
7642 var jsVerify;
765
7662 if(typeof exports === 'object' && require){
7672 jsVerify = require('jshint').JSHINT;
768 }
769 else{
7700 jsVerify = JSHINT;
771 }
772
7732 if(options !== undefined){
7742 var styleLine = event.line - 1,
775 styleCol = event.col - 1;
7762 var code = event.raw.replace(/\t/g,' ');
7772 try{
7782 var status = jsVerify(code, options);
7792 if(status === false){
7802 jsVerify.errors.forEach(function(error){
7818 var line = error.line;
7828 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
783 });
784 }
785 }
786 catch(e){}
787 }
788
789 }
790 });
791 }
792});
793/**
794 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
795 * MIT Licensed
796 */
7971HTMLHint.addRule({
798 id: 'space-tab-mixed-disabled',
799 description: 'Spaces and tabs can not mixed in front of line.',
800 init: function(parser, reporter){
8016 var self = this;
8026 parser.addListener('text', function(event){
80312 var raw = event.raw;
80412 var reMixed = /(^|\r?\n)( +\t|\t+ )/g;
80512 var match;
80612 while((match = reMixed.exec(raw))){
8073 var fixedPos = parser.fixPos(event, match.index + match[1].length);
8083 reporter.warn('Mixed spaces and tabs in front of line.', fixedPos.line, 1, self, event.raw);
809 }
810 });
811 }
812});
813/**
814 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
815 * MIT Licensed
816 */
8171HTMLHint.addRule({
818 id: 'spec-char-escape',
819 description: 'Special characters must be escaped.',
820 init: function(parser, reporter){
8214 var self = this;
8224 parser.addListener('text', function(event){
8234 var raw = event.raw,
824 reSpecChar = /[<>]/g,
825 match;
8264 while((match = reSpecChar.exec(raw))){
8274 var fixedPos = parser.fixPos(event, match.index);
8284 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
829 }
830 });
831 }
832});
833/**
834 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
835 * MIT Licensed
836 */
8371HTMLHint.addRule({
838 id: 'src-not-empty',
839 description: 'Src of img(script,link) must set value.',
840 init: function(parser, reporter){
8415 var self = this;
8425 parser.addListener('tagstart', function(event){
84332 var tagName = event.tagName,
844 attrs = event.attrs,
845 attr,
846 col = event.col + tagName.length + 1;
84732 for(var i=0, l=attrs.length;i<l;i++){
84836 attr = attrs[i];
84936 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
850 (tagName === 'link' && attr.name === 'href') ||
851 (tagName === 'object' && attr.name === 'data')) &&
852 attr.value === ''){
85315 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
854 }
855 }
856 });
857 }
858});
859/**
860 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
861 * MIT Licensed
862 */
8631HTMLHint.addRule({
864 id: 'style-disabled',
865 description: 'Style tag can not be use.',
866 init: function(parser, reporter){
8672 var self = this;
8682 parser.addListener('tagstart', function(event){
8694 if(event.tagName.toLowerCase() === 'style'){
8701 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
871 }
872 });
873 }
874});
875/**
876 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
877 * MIT Licensed
878 */
8791HTMLHint.addRule({
880 id: 'tag-pair',
881 description: 'Tag must be paired.',
882 init: function(parser, reporter){
8835 var self = this;
8845 var stack=[],
885 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
8865 parser.addListener('tagstart', function(event){
8878 var tagName = event.tagName.toLowerCase();
8888 if (mapEmptyTags[tagName] === undefined && !event.close){
8897 stack.push(tagName);
890 }
891 });
8925 parser.addListener('tagend', function(event){
8934 var tagName = event.tagName.toLowerCase();
894 //向上寻找匹配的开始标签
8954 for(var pos = stack.length-1;pos >= 0; pos--){
8964 if(stack[pos] === tagName){
8973 break;
898 }
899 }
9004 if(pos >= 0){
9013 var arrTags = [];
9023 for(var i=stack.length-1;i>pos;i--){
9031 arrTags.push('</'+stack[i]+'>');
904 }
9053 if(arrTags.length > 0){
9061 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
907 }
9083 stack.length=pos;
909 }
910 else{
9111 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
912 }
913 });
9145 parser.addListener('end', function(event){
9155 var arrTags = [];
9165 for(var i=stack.length-1;i>=0;i--){
9173 arrTags.push('</'+stack[i]+'>');
918 }
9195 if(arrTags.length > 0){
9203 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
921 }
922 });
923 }
924});
925/**
926 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
927 * MIT Licensed
928 */
9291HTMLHint.addRule({
930 id: 'tag-self-close',
931 description: 'The empty tag must closed by self.',
932 init: function(parser, reporter){
9332 var self = this;
9342 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9352 parser.addListener('tagstart', function(event){
9364 var tagName = event.tagName.toLowerCase();
9374 if(mapEmptyTags[tagName] !== undefined){
9384 if(!event.close){
9392 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
940 }
941 }
942 });
943 }
944});
945/**
946 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
947 * MIT Licensed
948 */
9491HTMLHint.addRule({
950 id: 'tagname-lowercase',
951 description: 'Tagname must be lowercase.',
952 init: function(parser, reporter){
9534 var self = this;
9544 parser.addListener('tagstart,tagend', function(event){
95513 var tagName = event.tagName;
95613 if(tagName !== tagName.toLowerCase()){
9575 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
958 }
959 });
960 }
961});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 322cf29e1..81e846d01 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={};return t.version="0.9.6",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,"src-not-empty":!0,"attr-no-duplication":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,n){var i;a=a.replace(/^\s*/i,function(t,a){return n===e&&(n={}),a.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g,function(e,t,a){"false"===a?a=!1:"true"===a&&(a=!0),n[t]=a}),""}),n===e&&(n=t.defaultRuleset);var r,s=new HTMLParser,o=new t.Reporter(a.split(/\r?\n/),n),l=t.rules;for(i in n)r=l[i],r!==e&&n[i]!==!1&&r.init(s,o,n[i]);return s.parse(a),o.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,n,i){this.report("error",e,t,a,n,i)},warn:function(e,t,a,n,i){this.report("warning",e,t,a,n,i)},info:function(e,t,a,n,i){this.report("info",e,t,a,n,i)},report:function(e,t,a,n,i,r){var s=this;s.messages.push({type:e,message:t,raw:r,evidence:s.lines[a-1],line:a,col:n,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(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,i){var r=n-w+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=b,i.col=r,L.push(i),u.fire(t,i);for(var s;s=p.exec(a);)b++,w=n+p.lastIndex}var n,i,r,s,o,l,d,c,u=this,f=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,m=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,p=/\r?\n/g,v=0,h=0,w=0,b=1,L=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=g.lastIndex,!(r=n[1])||(o&&r===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(n[0]);else if(r=n[4]){s=[];for(var H,y=n[5],T=0;H=m.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",R=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:R,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),d=[],h=v)):a("text",n[0],i)}else(n[2]||n[3])&&a("comment",n[0],i,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],i,{tagName:r});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:b,col:v-w+1})},addListener:function(t,a){for(var n,i=this._listeners,r=t.split(/[,\s]/),s=0,o=r.length;o>s;s++)n=r[s],i[n]===e&&(i[n]=[]),i[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,i=[],r=n._listeners[t],s=n._listeners.all;r!==e&&(i=i.concat(r)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var i=0,r=n.length;r>i;i++)if(n[i]===a){n.splice(i,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),i=n.split(/\r?\n/),r=i.length-1,s=e.line;return r>0?(s+=r,a=i[r].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},n=0,i=e.length;i>n;n++)t=e[n],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 n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){n=i[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Attribute name can not been duplication.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o={},l=0,d=r.length;d>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("The name of attribute [ "+n.name+" ] been duplication.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),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,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],(""!==n.value&&'"'!==n.quote||""===n.value&&"'"===n.quote)&&t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,r+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,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,r+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 i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var r=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.id+"] "+e.message,r+a,(1===a?s:0)+e.col,n,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=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",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,i,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var i=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,i,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"href-abs-or-rel",description:"Href must be absolute or relative.",init:function(e,t,a){var n=this,i="abs"===a?"absolute":"relative";e.addListener("tagstart",function(e){for(var a,r=e.attrs,s=e.col+e.tagName.length+1,o=0,l=r.length;l>o;o++)if(a=r[o],"href"===a.name){("absolute"===i&&/^\w+?:/.test(a.value)===!1||"relative"===i&&/^https?:\/\//.test(a.value)===!0)&&t.error("The value of href [ "+a.value+" ] must be "+i+".",e.line,s+a.index,n,a.raw);break}})}}),HTMLHint.addRule({id:"id-class-ad-disabled",description:"Id and class can not use ad keyword, it will blocked by adblock software.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o=0,l=r.length;l>o;o++)n=r[o],i=n.name,/^(id|class)$/i.test(i)&&/(^|[-\_])ad([-\_]|$)/i.test(n.value)&&t.warn("The value of "+i+" can not use ad keyword.",e.line,s+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,i=this,r={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?r[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,r=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,r+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),f=0,g=u.length;g>f;f++)c=u[f],c&&s.test(c)===!1&&t.warn(o,e.line,r+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,n={};e.addListener("tagstart",function(e){for(var i,r,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()){r=i.value,r&&(void 0===n[r]?n[r]=1:n[r]++,n[r]>1&&t.error("Id redefinition of [ "+r+" ].",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 n=e.attrs,i=!1,r=0,s=n.length;s>r;r++)if("alt"===n[r].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 n=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var r=e.getMapAttrs(i.attrs),s=r.type;if(void 0!==r.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,n,e.evidence)})}catch(f){}}}})}}),HTMLHint.addRule({id:"space-tab-mixed-disabled",description:"Spaces and tabs can not mixed in front of line.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var i,r=n.raw,s=/(^|\r?\n)( +\t|\t+ )/g;i=s.exec(r);){var o=e.fixPos(n,i.index+i[1].length);t.warn("Mixed spaces and tabs in front of line.",o.line,1,a,n.raw)}})}}),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 i,r=n.raw,s=/[<>]/g;i=s.exec(r);){var o=e.fixPos(n,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.tagName,r=e.attrs,s=e.col+i.length+1,o=0,l=r.length;l>o;o++)n=r[o],(/^(img|script|embed|bgsound|iframe)$/.test(i)===!0&&"src"===n.name||"link"===i&&"href"===n.name||"object"===i&&"data"===n.name)&&""===n.value&&t.error("[ "+n.name+"] of [ "+i+" ] must set value.",e.line,s+n.index,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=[],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||n.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),r=n.length-1;r>=0&&n[r]!==i;r--);if(r>=0){for(var s=[],o=n.length-1;o>r;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),n.length=r}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=[],r=n.length-1;r>=0;r--)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,n=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!==n[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 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={};return t.version="0.9.6",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,"src-not-empty":!0,"attr-no-duplication":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,n){a=a.replace(/^\s*/i,function(t,a){return n===e&&(n={}),a.replace(/(?:^|,)\s*([^:]+)\s*:\s*([^,\s]+)/g,function(e,t,a){"false"===a?a=!1:"true"===a&&(a=!0),n[t]=a}),""}),n=n||t.defaultRuleset;var i,r=new HTMLParser,s=new t.Reporter(a.split(/\r?\n/),n),o=t.rules;for(var l in n)i=o[l],i!==e&&n[l]!==!1&&i.init(r,s,n[l]);return r.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,n,i){this.report("error",e,t,a,n,i)},warn:function(e,t,a,n,i){this.report("warning",e,t,a,n,i)},info:function(e,t,a,n,i){this.report("info",e,t,a,n,i)},report:function(e,t,a,n,i,r){var s=this;s.messages.push({type:e,message:t,raw:r,evidence:s.lines[a-1],line:a,col:n,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(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,i){var r=n-w+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=b,i.col=r,L.push(i),u.fire(t,i);for(var s;s=p.exec(a);)b++,w=n+p.lastIndex}var n,i,r,s,o,l,d,c,u=this,f=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,m=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,p=/\r?\n/g,v=0,h=0,w=0,b=1,L=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>v&&(c=t.substring(v,i),o?d.push(c):a("text",c,v)),v=g.lastIndex,!(r=n[1])||(o&&r===o&&(c=d.join(""),a("cdata",c,h,{tagName:o,attrs:l}),o=null,l=null,d=null),o))if(o)d.push(n[0]);else if(r=n[4]){s=[];for(var H,y=n[5],T=0;H=m.exec(y);){var x=H[1],M=H[2]?H[2]:H[4]?H[4]:"",R=H[3]?H[3]:H[5]?H[5]:H[6]?H[6]:"";s.push({name:x,value:R,quote:M,index:H.index,raw:H[0]}),T+=H[0].length}T===y.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),d=[],h=v)):a("text",n[0],i)}else(n[2]||n[3])&&a("comment",n[0],i,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],i,{tagName:r});t.length>v&&(c=t.substring(v,t.length),a("text",c,v)),u.fire("end",{pos:v,line:b,col:v-w+1})},addListener:function(t,a){for(var n,i=this._listeners,r=t.split(/[,\s]/),s=0,o=r.length;o>s;s++)n=r[s],i[n]===e&&(i[n]=[]),i[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,i=[],r=n._listeners[t],s=n._listeners.all;r!==e&&(i=i.concat(r)),s!==e&&(i=i.concat(s));for(var o=0,l=i.length;l>o;o++)i[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var i=0,r=n.length;r>i;i++)if(n[i]===a){n.splice(i,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),i=n.split(/\r?\n/),r=i.length-1,s=e.line;return r>0?(s+=r,a=i[r].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},n=0,i=e.length;i>n;n++)t=e[n],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 n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){n=i[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Attribute name can not been duplication.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o={},l=0,d=r.length;d>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("The name of attribute [ "+n.name+" ] been duplication.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),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,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],(""!==n.value&&'"'!==n.quote||""===n.value&&"'"===n.quote)&&t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,r+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,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,r+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 i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var r=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.id+"] "+e.message,r+a,(1===a?s:0)+e.col,n,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=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",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,i,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var i=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,i,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var i=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"href-abs-or-rel",description:"Href must be absolute or relative.",init:function(e,t,a){var n=this,i="abs"===a?"absolute":"relative";e.addListener("tagstart",function(e){for(var a,r=e.attrs,s=e.col+e.tagName.length+1,o=0,l=r.length;l>o;o++)if(a=r[o],"href"===a.name){("absolute"===i&&/^\w+?:/.test(a.value)===!1||"relative"===i&&/^https?:\/\//.test(a.value)===!0)&&t.warn("The value of href [ "+a.value+" ] must be "+i+".",e.line,s+a.index,n,a.raw);break}})}}),HTMLHint.addRule({id:"id-class-ad-disabled",description:"Id and class can not use ad keyword, it will blocked by adblock software.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o=0,l=r.length;l>o;o++)n=r[o],i=n.name,/^(id|class)$/i.test(i)&&/(^|[-\_])ad([-\_]|$)/i.test(n.value)&&t.warn("The value of "+i+" can not use ad keyword.",e.line,s+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,i=this,r={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?r[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,r=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,r+a.index,i,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),f=0,g=u.length;g>f;f++)c=u[f],c&&s.test(c)===!1&&t.warn(o,e.line,r+a.index,i,c)})}}}),HTMLHint.addRule({id:"id-unique",description:"Id must be unique.",init:function(e,t){var a=this,n={};e.addListener("tagstart",function(e){for(var i,r,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()){r=i.value,r&&(void 0===n[r]?n[r]=1:n[r]++,n[r]>1&&t.error("Id redefinition of [ "+r+" ].",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 n=e.attrs,i=!1,r=0,s=n.length;s>r;r++)if("alt"===n[r].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 n=this;e.addListener("cdata",function(i){if("script"===i.tagName.toLowerCase()){var r=e.getMapAttrs(i.attrs),s=r.type;if(void 0!==r.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,n,e.evidence)})}catch(f){}}}})}}),HTMLHint.addRule({id:"space-tab-mixed-disabled",description:"Spaces and tabs can not mixed in front of line.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var i,r=n.raw,s=/(^|\r?\n)( +\t|\t+ )/g;i=s.exec(r);){var o=e.fixPos(n,i.index+i[1].length);t.warn("Mixed spaces and tabs in front of line.",o.line,1,a,n.raw)}})}}),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 i,r=n.raw,s=/[<>]/g;i=s.exec(r);){var o=e.fixPos(n,i.index);t.error("Special characters must be escaped : [ "+i[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"src-not-empty",description:"Src of img(script,link) must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.tagName,r=e.attrs,s=e.col+i.length+1,o=0,l=r.length;l>o;o++)n=r[o],(/^(img|script|embed|bgsound|iframe)$/.test(i)===!0&&"src"===n.name||"link"===i&&"href"===n.name||"object"===i&&"data"===n.name)&&""===n.value&&t.error("[ "+n.name+"] of [ "+i+" ] must set value.",e.line,s+n.index,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=[],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||n.push(t)}),e.addListener("tagend",function(e){for(var i=e.tagName.toLowerCase(),r=n.length-1;r>=0&&n[r]!==i;r--);if(r>=0){for(var s=[],o=n.length-1;o>r;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),n.length=r}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=[],r=n.length-1;r>=0;r--)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,n=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!==n[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 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/core.js b/src/core.js index be3ef6cb1..ecc87cc09 100644 --- a/src/core.js +++ b/src/core.js @@ -28,7 +28,6 @@ var HTMLHint = (function (undefined) { }; HTMLHint.verify = function(html, ruleset){ - var id; // parse inline ruleset html = html.replace(/^\s*/i, function(all, strRuleset){ if(ruleset === undefined){ @@ -45,16 +44,14 @@ var HTMLHint = (function (undefined) { }); return ''; }); - if(ruleset === undefined){ - ruleset = HTMLHint.defaultRuleset; - } + ruleset = ruleset || HTMLHint.defaultRuleset; var parser = new HTMLParser(); var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset); var rules = HTMLHint.rules, rule; - for (id in ruleset){ + for (var id in ruleset){ rule = rules[id]; if (rule !== undefined && ruleset[id] !== false){ rule.init(parser, reporter, ruleset[id]); diff --git a/src/rules/href-abs-or-rel.js b/src/rules/href-abs-or-rel.js index af5d0afe6..f96361f1c 100644 --- a/src/rules/href-abs-or-rel.js +++ b/src/rules/href-abs-or-rel.js @@ -20,7 +20,7 @@ HTMLHint.addRule({ if(attr.name === 'href'){ if((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) || (hrefMode === 'relative' && /^https?:\/\//.test(attr.value) === true)){ - reporter.error('The value of href [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw); + reporter.warn('The value of href [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw); } break; }