diff --git a/coverage.html b/coverage.html index 983174936..dc0755ed9 100644 --- a/coverage.html +++ b/coverage.html @@ -352,4 +352,4 @@ code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

98%
417
411
6

htmlhint.js

98%
417
411
6
LineHitsSource
1/**
2 * Copyright (c) 2015, 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){
2722 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, ruleset){
31 // parse inline ruleset
3298 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 });
47
4898 if(ruleset === undefined || Object.keys(ruleset).length ===0){
493 ruleset = HTMLHint.defaultRuleset;
50 }
51
5298 var parser = new HTMLParser();
5398 var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
54
5598 var rules = HTMLHint.rules,
56 rule;
5798 for (var id in ruleset){
58122 rule = rules[id];
59122 if (rule !== undefined && ruleset[id] !== false){
60119 rule.init(parser, reporter, ruleset[id]);
61 }
62 }
63
6498 parser.parse(html);
65
6698 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) 2015, Yanis Wang <yanis.wang@gmail.com>
78 * MIT Licensed
79 */
801(function(HTMLHint, undefined){
81
821 var Reporter = function(){
8398 var self = this;
8498 self._init.apply(self,arguments);
85 };
86
871 Reporter.prototype = {
88 _init: function(lines, ruleset){
8998 var self = this;
9098 self.lines = lines;
9198 self.ruleset = ruleset;
9298 self.messages = [];
93 },
94 //错误
95 error: function(message, line, col, rule, raw){
9652 this.report('error', message, line, col, rule, raw);
97 },
98 //警告
99 warn: function(message, line, col, rule, raw){
10055 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){
108107 var self = this;
109107 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) 2015, Yanis Wang <yanis.wang@gmail.com>
130 * MIT Licensed
131 */
1321var HTMLParser = (function(undefined){
133
1341 var HTMLParser = function(){
135123 var self = this;
136123 self._init.apply(self,arguments);
137 };
138
1391 HTMLParser.prototype = {
140 _init: function(){
141123 var self = this;
142123 self._listeners = {};
143123 self._mapCdataTags = self.makeMap("script,style");
144123 self._arrBlocks = [];
145 },
146
147 makeMap: function(str){
148131 var obj = {}, items = str.split(",");
149131 for ( var i = 0; i < items.length; i++ ){
150358 obj[ items[i] ] = true;
151 }
152131 return obj;
153 },
154
155 // parse html code
156 parse: function(html){
157
158123 var self = this,
159 mapCdataTags = self._mapCdataTags;
160
161123 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
165123 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
166123 var lastLineIndex = 0, line = 1;
167123 var arrBlocks = self._arrBlocks;
168
169123 self.fire('start', {
170 pos: 0,
171 line: 1,
172 col: 1
173 });
174
175123 while((match = regTag.exec(html))){
176288 matchIndex = match.index;
177288 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
17880 text = html.substring(lastIndex, matchIndex);
17980 if(tagCDATA){
18012 arrCDATA.push(text);
181 }
182 else{//文本
18368 saveBlock('text', text, lastIndex);
184 }
185 }
186288 lastIndex = regTag.lastIndex;
187
188288 if((tagName = match[1])){
18999 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
19019 text = arrCDATA.join('');
19119 saveBlock('cdata', text, lastCDATAIndex, {
192 'tagName': tagCDATA,
193 'attrs': attrsCDATA
194 });
19519 tagCDATA = null;
19619 attrsCDATA = null;
19719 arrCDATA = null;
198 }
19999 if(!tagCDATA){
200 //标签结束
20198 saveBlock('tagend', match[0], matchIndex, {
202 'tagName': tagName
203 });
20498 continue;
205 }
206 }
207
208190 if(tagCDATA){
2093 arrCDATA.push(match[0]);
210 }
211 else{
212187 if((tagName = match[4])){//标签开始
213174 arrAttrs = [];
214174 var attrs = match[5],
215 attrMatch,
216 attrMatchCount = 0;
217174 while((attrMatch = regAttr.exec(attrs))){
218167 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] : '';
224167 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
225167 attrMatchCount += attrMatch[0].length;
226 }
227174 if(attrMatchCount === attrs.length){
228174 saveBlock('tagstart', match[0], matchIndex, {
229 'tagName': tagName,
230 'attrs': arrAttrs,
231 'close': match[6]
232 });
233174 if(mapCdataTags[tagName]){
23419 tagCDATA = tagName;
23519 attrsCDATA = arrAttrs.concat();
23619 arrCDATA = [];
23719 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
253123 if(html.length > lastIndex){
254 //结尾文本
25513 text = html.substring(lastIndex, html.length);
25613 saveBlock('text', text, lastIndex);
257 }
258
259123 self.fire('end', {
260 pos: lastIndex,
261 line: line,
262 col: lastIndex - lastLineIndex + 1
263 });
264
265 //存储区块
266123 function saveBlock(type, raw, pos, data){
267385 var col = pos - lastLineIndex + 1;
268385 if(data === undefined){
26981 data = {};
270 }
271385 data.raw = raw;
272385 data.pos = pos;
273385 data.line = line;
274385 data.col = col;
275385 arrBlocks.push(data);
276385 self.fire(type, data);
277385 var lineMatch;
278385 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){
288165 var _listeners = this._listeners;
289165 var arrTypes = types.split(/[,\s]/), type;
290165 for(var i=0, l = arrTypes.length;i<l;i++){
291170 type = arrTypes[i];
292170 if (_listeners[type] === undefined){
293149 _listeners[type] = [];
294 }
295170 _listeners[type].push(listener);
296 }
297 },
298
299 // fire event
300 fire: function(type, data){
301631 if (data === undefined){
3020 data = {};
303 }
304631 data.type = type;
305631 var self = this,
306 listeners = [],
307 listenersType = self._listeners[type],
308 listenersAll = self._listeners['all'];
309631 if (listenersType !== undefined){
310190 listeners = listeners.concat(listenersType);
311 }
312631 if (listenersAll !== undefined){
313137 listeners = listeners.concat(listenersAll);
314 }
315631 for (var i = 0, l = listeners.length; i < l; i++){
316354 listeners[i].call(self, data);
317 }
318 },
319
320 // remove event
321 removeListener: function(type, listener){
32223 var listenersType = this._listeners[type];
32323 if(listenersType !== undefined){
32421 for (var i = 0, l = listenersType.length; i < l; i++){
32514 if (listenersType[i] === listener){
32614 listenersType.splice(i, 1);
32714 break;
328 }
329 }
330 }
331 },
332
333 //fix pos if event.raw have \n
334 fixPos: function(event, index){
3358 var text = event.raw.substr(0, index);
3368 var arrLines = text.split(/\r?\n/),
337 lineCount = arrLines.length - 1,
338 line = event.line, col;
3398 if(lineCount > 0){
3402 line += lineCount;
3412 col = arrLines[lineCount].length + 1;
342 }
343 else{
3446 col = event.col + index;
345 }
3468 return {
347 line: line,
348 col: col
349 };
350 },
351
352 // covert array type of attrs to map
353 getMapAttrs: function(arrAttrs){
35433 var mapAttrs = {},
355 attr;
35633 for(var i=0,l=arrAttrs.length;i<l;i++){
35731 attr = arrAttrs[i];
35831 mapAttrs[attr.name] = attr.value;
359 }
36033 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) 2015, Yanis Wang <yanis.wang@gmail.com>
373 * Copyright (c) 2014, Takeshi Kurosawa <taken.spc@gmail.com>
374 * MIT Licensed
375 */
3761HTMLHint.addRule({
377 id: 'alt-require',
378 description: 'Alt of img must be present and alt of area[href] and input[type=image] must be set value.',
379 init: function(parser, reporter){
38014 var self = this;
38114 parser.addListener('tagstart', function(event){
38214 var tagName = event.tagName.toLowerCase(),
383 mapAttrs = parser.getMapAttrs(event.attrs),
384 col = event.col + tagName.length + 1,
385 selector;
38614 if(tagName === 'img' && !('alt' in mapAttrs)){
3872 reporter.warn('Alt of img tag must be present.', event.line, col, self, event.raw);
388 }
38912 else if((tagName === 'area' && 'href' in mapAttrs) ||
390 (tagName === 'input' && mapAttrs['type'] === 'image')){
3916 if(!('alt' in mapAttrs) || mapAttrs['alt'] === ''){
3924 selector = tagName === 'area' ? 'area[href]' : 'input[type=image]';
3934 reporter.warn('Alt of ' + selector + ' must be set value.', event.line, col, self, event.raw);
394 }
395 }
396 });
397 }
398});
399
400/**
401 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
402 * MIT Licensed
403 */
4041HTMLHint.addRule({
405 id: 'attr-lowercase',
406 description: 'Attribute name must be lowercase.',
407 init: function(parser, reporter){
4085 var self = this;
4095 parser.addListener('tagstart', function(event){
4109 var attrs = event.attrs,
411 attr,
412 col = event.col + event.tagName.length + 1;
4139 for(var i=0, l=attrs.length;i<l;i++){
41415 attr = attrs[i];
41515 var attrName = attr.name;
41615 if(attrName !== attrName.toLowerCase()){
4174 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
418 }
419 }
420 });
421 }
422});
423/**
424 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
425 * MIT Licensed
426 */
4271HTMLHint.addRule({
428 id: 'attr-no-duplication',
429 description: 'Attribute name can not been duplication.',
430 init: function(parser, reporter){
4315 var self = this;
4325 parser.addListener('tagstart', function(event){
4339 var attrs = event.attrs;
4349 var attr;
4359 var attrName;
4369 var col = event.col + event.tagName.length + 1;
437
4389 var mapAttrName = {};
4399 for(var i=0, l=attrs.length;i<l;i++){
44016 attr = attrs[i];
44116 attrName = attr.name;
44216 if(mapAttrName[attrName] === true){
4433 reporter.error('The name of attribute [ '+attr.name+' ] been duplication.', event.line, col + attr.index, self, attr.raw);
444 }
44516 mapAttrName[attrName] = true;
446 }
447 });
448 }
449});
450/**
451 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
452 * MIT Licensed
453 */
4541HTMLHint.addRule({
455 id: 'attr-unsafe-chars',
456 description: 'Attribute value cant not use unsafe chars.',
457 init: function(parser, reporter){
4582 var self = this;
4592 parser.addListener('tagstart', function(event){
4602 var attrs = event.attrs,
461 attr,
462 col = event.col + event.tagName.length + 1;
4632 var regUnsafe = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/;
4642 for(var i=0, l=attrs.length;i<l;i++){
4652 attr = attrs[i];
4662 if(regUnsafe.test(attr.value) === true){
4671 reporter.warn('The value of attribute [ '+attr.name+' ] cant not use unsafe chars.', event.line, col + attr.index, self, attr.raw);
468 }
469 }
470 });
471 }
472});
473/**
474 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
475 * MIT Licensed
476 */
4771HTMLHint.addRule({
478 id: 'attr-value-double-quotes',
479 description: 'Attribute value must closed by double quotes.',
480 init: function(parser, reporter){
4815 var self = this;
4825 parser.addListener('tagstart', function(event){
4839 var attrs = event.attrs,
484 attr,
485 col = event.col + event.tagName.length + 1;
4869 for(var i=0, l=attrs.length;i<l;i++){
48719 attr = attrs[i];
48819 if((attr.value !== '' && attr.quote !== '"') ||
489 (attr.value === '' && attr.quote === "'")){
4905 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
491 }
492 }
493 });
494 }
495});
496/**
497 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
498 * MIT Licensed
499 */
5001HTMLHint.addRule({
501 id: 'attr-value-not-empty',
502 description: 'Attribute must set value.',
503 init: function(parser, reporter){
5043 var self = this;
5053 parser.addListener('tagstart', function(event){
5063 var attrs = event.attrs,
507 attr,
508 col = event.col + event.tagName.length + 1;
5093 for(var i=0, l=attrs.length;i<l;i++){
5103 attr = attrs[i];
5113 if(attr.quote === '' && attr.value === ''){
5121 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
513 }
514 }
515 });
516 }
517});
518/**
519 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
520 * MIT Licensed
521 */
5221HTMLHint.addRule({
523 id: 'csslint',
524 description: 'Scan css with csslint.',
525 init: function(parser, reporter, options){
5261 var self = this;
5271 parser.addListener('cdata', function(event){
5281 if(event.tagName.toLowerCase() === 'style'){
529
5301 var cssVerify;
531
5321 if(typeof exports === 'object' && require){
5331 cssVerify = require("csslint").CSSLint.verify;
534 }
535 else{
5360 cssVerify = CSSLint.verify;
537 }
538
5391 if(options !== undefined){
5401 var styleLine = event.line - 1,
541 styleCol = event.col - 1;
5421 try{
5431 var messages = cssVerify(event.raw, options).messages;
5441 messages.forEach(function(error){
5452 var line = error.line;
5462 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
547 });
548 }
549 catch(e){}
550 }
551
552 }
553 });
554 }
555});
556/**
557 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
558 * MIT Licensed
559 */
5601HTMLHint.addRule({
561 id: 'doctype-first',
562 description: 'Doctype must be first.',
563 init: function(parser, reporter){
5645 var self = this;
5655 var allEvent = function(event){
56610 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
5675 return;
568 }
5695 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5704 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
571 }
5725 parser.removeListener('all', allEvent);
573 };
5745 parser.addListener('all', allEvent);
575 }
576});
577/**
578 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
579 * MIT Licensed
580 */
5811HTMLHint.addRule({
582 id: 'doctype-html5',
583 description: 'Doctype must be html5.',
584 init: function(parser, reporter){
5852 var self = this;
5862 function onComment(event){
5879 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5881 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
589 }
590 }
5912 function onTagStart(){
5922 parser.removeListener('comment', onComment);
5932 parser.removeListener('tagstart', onTagStart);
594 }
5952 parser.addListener('all', onComment);
5962 parser.addListener('tagstart', onTagStart);
597 }
598});
599/**
600 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
601 * MIT Licensed
602 */
6031HTMLHint.addRule({
604 id: 'head-script-disabled',
605 description: 'The script tag can not be used in head.',
606 init: function(parser, reporter){
6077 var self = this;
6087 var reScript = /^(text\/javascript|application\/javascript)$/i;
6097 function onTagStart(event){
61013 var mapAttrs = parser.getMapAttrs(event.attrs);
61113 var type = mapAttrs.type;
61213 if(event.tagName.toLowerCase() === 'script' &&
613 (!type || reScript.test(type) === true)){
6144 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
615 }
616 }
6177 function onTagEnd(event){
61815 if(event.tagName.toLowerCase() === 'head'){
6197 parser.removeListener('tagstart', onTagStart);
6207 parser.removeListener('tagstart', onTagEnd);
621 }
622 }
6237 parser.addListener('tagstart', onTagStart);
6247 parser.addListener('tagend', onTagEnd);
625 }
626});
627/**
628 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
629 * MIT Licensed
630 */
6311HTMLHint.addRule({
632 id: 'href-abs-or-rel',
633 description: 'Href must be absolute or relative.',
634 init: function(parser, reporter, options){
6354 var self = this;
636
6374 var hrefMode = options === 'abs' ? 'absolute' : 'relative';
638
6394 parser.addListener('tagstart', function(event){
64016 var attrs = event.attrs;
64116 var attr;
64216 var col = event.col + event.tagName.length + 1;
643
64416 for(var i=0, l=attrs.length;i<l;i++){
64516 attr = attrs[i];
64616 if(attr.name === 'href'){
64716 if((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) ||
648 (hrefMode === 'relative' && /^https?:\/\//.test(attr.value) === true)){
6494 reporter.warn('The value of href [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw);
650 }
65116 break;
652 }
653 }
654 });
655 }
656});
657/**
658 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
659 * MIT Licensed
660 */
6611HTMLHint.addRule({
662 id: 'id-class-ad-disabled',
663 description: 'Id and class can not use ad keyword, it will blocked by adblock software.',
664 init: function(parser, reporter){
66517 var self = this;
66617 parser.addListener('tagstart', function(event){
66717 var attrs = event.attrs;
66817 var attr;
66917 var attrName;
67017 var col = event.col + event.tagName.length + 1;
671
67217 for(var i=0, l=attrs.length;i<l;i++){
67320 attr = attrs[i];
67420 attrName = attr.name;
67520 if(/^(id|class)$/i.test(attrName)){
67620 if(/(^|[-\_])ad([-\_]|$)/i.test(attr.value)){
67714 reporter.warn('The value of '+attrName+' can not use ad keyword.', event.line, col + attr.index, self, attr.raw);
678 }
679 }
680 }
681 });
682 }
683});
684/**
685 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
686 * MIT Licensed
687 */
6881HTMLHint.addRule({
689 id: 'id-class-value',
690 description: 'Id and class value must meet some rules.',
691 init: function(parser, reporter, options){
6928 var self = this;
6938 var arrRules = {
694 'underline': {
695 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
696 'message': 'Id and class value must lower case and split by underline.'
697 },
698 'dash': {
699 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
700 'message': 'Id and class value must lower case and split by dash.'
701 },
702 'hump': {
703 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
704 'message': 'Id and class value must meet hump style.'
705 }
706 }, rule;
7078 if(typeof options === 'string'){
7086 rule = arrRules[options];
709 }
710 else{
7112 rule = options;
712 }
7138 if(rule && rule.regId){
7148 var regId = rule.regId,
715 message = rule.message;
7168 parser.addListener('tagstart', function(event){
7178 var attrs = event.attrs,
718 attr,
719 col = event.col + event.tagName.length + 1;
7208 for(var i=0, l1=attrs.length;i<l1;i++){
72116 attr = attrs[i];
72216 if(attr.name.toLowerCase() === 'id'){
7238 if(regId.test(attr.value) === false){
7244 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
725 }
726 }
72716 if(attr.name.toLowerCase() === 'class'){
7288 var arrClass = attr.value.split(/\s+/g), classValue;
7298 for(var j=0, l2=arrClass.length;j<l2;j++){
7308 classValue = arrClass[j];
7318 if(classValue && regId.test(classValue) === false){
7324 reporter.warn(message, event.line, col + attr.index, self, classValue);
733 }
734 }
735 }
736 }
737 });
738 }
739 }
740});
741/**
742 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
743 * MIT Licensed
744 */
7451HTMLHint.addRule({
746 id: 'id-unique',
747 description: 'Id must be unique.',
748 init: function(parser, reporter){
7495 var self = this;
7505 var mapIdCount = {};
7515 parser.addListener('tagstart', function(event){
75211 var attrs = event.attrs,
753 attr,
754 id,
755 col = event.col + event.tagName.length + 1;
75611 for(var i=0, l=attrs.length;i<l;i++){
75717 attr = attrs[i];
75817 if(attr.name.toLowerCase() === 'id'){
7598 id = attr.value;
7608 if(id){
7618 if(mapIdCount[id] === undefined){
7625 mapIdCount[id] = 1;
763 }
764 else{
7653 mapIdCount[id] ++;
766 }
7678 if(mapIdCount[id] > 1){
7683 reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
769 }
770 }
7718 break;
772 }
773 }
774 });
775 }
776});
777/**
778 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
779 * MIT Licensed
780 */
7811HTMLHint.addRule({
782 id: 'jshint',
783 description: 'Scan script with jshint.',
784 init: function(parser, reporter, options){
7854 var self = this;
7864 parser.addListener('cdata', function(event){
7874 if(event.tagName.toLowerCase() === 'script'){
788
7894 var mapAttrs = parser.getMapAttrs(event.attrs),
790 type = mapAttrs.type;
791
792 // Only scan internal javascript
7934 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
7942 return;
795 }
796
7972 var jsVerify;
798
7992 if(typeof exports === 'object' && require){
8002 jsVerify = require('jshint').JSHINT;
801 }
802 else{
8030 jsVerify = JSHINT;
804 }
805
8062 if(options !== undefined){
8072 var styleLine = event.line - 1,
808 styleCol = event.col - 1;
8092 var code = event.raw.replace(/\t/g,' ');
8102 try{
8112 var status = jsVerify(code, options);
8122 if(status === false){
8132 jsVerify.errors.forEach(function(error){
8148 var line = error.line;
8158 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
816 });
817 }
818 }
819 catch(e){}
820 }
821
822 }
823 });
824 }
825});
826/**
827 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
828 * MIT Licensed
829 */
8301HTMLHint.addRule({
831 id: 'space-tab-mixed-disabled',
832 description: 'Spaces and tabs can not mixed in front of line.',
833 init: function(parser, reporter){
8346 var self = this;
8356 parser.addListener('text', function(event){
83612 var raw = event.raw;
83712 var reMixed = /(^|\r?\n)( +\t|\t+ )/g;
83812 var match;
83912 while((match = reMixed.exec(raw))){
8403 var fixedPos = parser.fixPos(event, match.index + match[1].length);
8413 reporter.warn('Mixed spaces and tabs in front of line.', fixedPos.line, 1, self, event.raw);
842 }
843 });
844 }
845});
846/**
847 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
848 * MIT Licensed
849 */
8501HTMLHint.addRule({
851 id: 'spec-char-escape',
852 description: 'Special characters must be escaped.',
853 init: function(parser, reporter){
8545 var self = this;
8555 parser.addListener('text', function(event){
8565 var raw = event.raw,
857 reSpecChar = /[<>]/g,
858 match;
8595 while((match = reSpecChar.exec(raw))){
8605 var fixedPos = parser.fixPos(event, match.index);
8615 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
862 }
863 });
864 }
865});
866/**
867 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
868 * MIT Licensed
869 */
8701HTMLHint.addRule({
871 id: 'src-not-empty',
872 description: 'Src of img(script,link) must set value.',
873 init: function(parser, reporter){
8746 var self = this;
8756 parser.addListener('tagstart', function(event){
87635 var tagName = event.tagName,
877 attrs = event.attrs,
878 attr,
879 col = event.col + tagName.length + 1;
88035 for(var i=0, l=attrs.length;i<l;i++){
88142 attr = attrs[i];
88242 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
883 (tagName === 'link' && attr.name === 'href') ||
884 (tagName === 'object' && attr.name === 'data')) &&
885 attr.value === ''){
88616 reporter.error('[ '+attr.name + '] of [ '+tagName+' ] must set value.', event.line, col + attr.index, self, attr.raw);
887 }
888 }
889 });
890 }
891});
892/**
893 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
894 * MIT Licensed
895 */
8961HTMLHint.addRule({
897 id: 'style-disabled',
898 description: 'Style tag can not be use.',
899 init: function(parser, reporter){
9002 var self = this;
9012 parser.addListener('tagstart', function(event){
9024 if(event.tagName.toLowerCase() === 'style'){
9031 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
904 }
905 });
906 }
907});
908/**
909 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
910 * MIT Licensed
911 */
9121HTMLHint.addRule({
913 id: 'tag-pair',
914 description: 'Tag must be paired.',
915 init: function(parser, reporter){
9166 var self = this;
9176 var stack=[],
918 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9196 parser.addListener('tagstart', function(event){
92011 var tagName = event.tagName.toLowerCase();
92111 if (mapEmptyTags[tagName] === undefined && !event.close){
9229 stack.push(tagName);
923 }
924 });
9256 parser.addListener('tagend', function(event){
9265 var tagName = event.tagName.toLowerCase();
927 //向上寻找匹配的开始标签
9285 for(var pos = stack.length-1;pos >= 0; pos--){
9295 if(stack[pos] === tagName){
9304 break;
931 }
932 }
9335 if(pos >= 0){
9344 var arrTags = [];
9354 for(var i=stack.length-1;i>pos;i--){
9361 arrTags.push('</'+stack[i]+'>');
937 }
9384 if(arrTags.length > 0){
9391 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
940 }
9414 stack.length=pos;
942 }
943 else{
9441 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
945 }
946 });
9476 parser.addListener('end', function(event){
9486 var arrTags = [];
9496 for(var i=stack.length-1;i>=0;i--){
9504 arrTags.push('</'+stack[i]+'>');
951 }
9526 if(arrTags.length > 0){
9534 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
954 }
955 });
956 }
957});
958/**
959 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
960 * MIT Licensed
961 */
9621HTMLHint.addRule({
963 id: 'tag-self-close',
964 description: 'The empty tag must closed by self.',
965 init: function(parser, reporter){
9662 var self = this;
9672 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9682 parser.addListener('tagstart', function(event){
9694 var tagName = event.tagName.toLowerCase();
9704 if(mapEmptyTags[tagName] !== undefined){
9714 if(!event.close){
9722 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
973 }
974 }
975 });
976 }
977});
978/**
979 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
980 * MIT Licensed
981 */
9821HTMLHint.addRule({
983 id: 'tagname-lowercase',
984 description: 'Tagname must be lowercase.',
985 init: function(parser, reporter){
9865 var self = this;
9875 parser.addListener('tagstart,tagend', function(event){
98817 var tagName = event.tagName;
98917 if(tagName !== tagName.toLowerCase()){
9906 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
991 }
992 });
993 }
994});
\ No newline at end of file +

Coverage

98%
417
411
6

htmlhint.js

98%
417
411
6
LineHitsSource
1/**
2 * Copyright (c) 2015, 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){
2722 HTMLHint.rules[rule.id] = rule;
28 };
29
301 HTMLHint.verify = function(html, ruleset){
31 // parse inline ruleset
3298 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 });
47
4898 if(ruleset === undefined || Object.keys(ruleset).length ===0){
493 ruleset = HTMLHint.defaultRuleset;
50 }
51
5298 var parser = new HTMLParser();
5398 var reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
54
5598 var rules = HTMLHint.rules,
56 rule;
5798 for (var id in ruleset){
58122 rule = rules[id];
59122 if (rule !== undefined && ruleset[id] !== false){
60119 rule.init(parser, reporter, ruleset[id]);
61 }
62 }
63
6498 parser.parse(html);
65
6698 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) 2015, Yanis Wang <yanis.wang@gmail.com>
78 * MIT Licensed
79 */
801(function(HTMLHint, undefined){
81
821 var Reporter = function(){
8398 var self = this;
8498 self._init.apply(self,arguments);
85 };
86
871 Reporter.prototype = {
88 _init: function(lines, ruleset){
8998 var self = this;
9098 self.lines = lines;
9198 self.ruleset = ruleset;
9298 self.messages = [];
93 },
94 //错误
95 error: function(message, line, col, rule, raw){
9652 this.report('error', message, line, col, rule, raw);
97 },
98 //警告
99 warn: function(message, line, col, rule, raw){
10055 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){
108107 var self = this;
109107 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) 2015, Yanis Wang <yanis.wang@gmail.com>
130 * MIT Licensed
131 */
1321var HTMLParser = (function(undefined){
133
1341 var HTMLParser = function(){
135123 var self = this;
136123 self._init.apply(self,arguments);
137 };
138
1391 HTMLParser.prototype = {
140 _init: function(){
141123 var self = this;
142123 self._listeners = {};
143123 self._mapCdataTags = self.makeMap("script,style");
144123 self._arrBlocks = [];
145 },
146
147 makeMap: function(str){
148131 var obj = {}, items = str.split(",");
149131 for ( var i = 0; i < items.length; i++ ){
150376 obj[ items[i] ] = true;
151 }
152131 return obj;
153 },
154
155 // parse html code
156 parse: function(html){
157
158123 var self = this,
159 mapCdataTags = self._mapCdataTags;
160
161123 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
165123 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
166123 var lastLineIndex = 0, line = 1;
167123 var arrBlocks = self._arrBlocks;
168
169123 self.fire('start', {
170 pos: 0,
171 line: 1,
172 col: 1
173 });
174
175123 while((match = regTag.exec(html))){
176288 matchIndex = match.index;
177288 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
17880 text = html.substring(lastIndex, matchIndex);
17980 if(tagCDATA){
18012 arrCDATA.push(text);
181 }
182 else{//文本
18368 saveBlock('text', text, lastIndex);
184 }
185 }
186288 lastIndex = regTag.lastIndex;
187
188288 if((tagName = match[1])){
18999 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
19019 text = arrCDATA.join('');
19119 saveBlock('cdata', text, lastCDATAIndex, {
192 'tagName': tagCDATA,
193 'attrs': attrsCDATA
194 });
19519 tagCDATA = null;
19619 attrsCDATA = null;
19719 arrCDATA = null;
198 }
19999 if(!tagCDATA){
200 //标签结束
20198 saveBlock('tagend', match[0], matchIndex, {
202 'tagName': tagName
203 });
20498 continue;
205 }
206 }
207
208190 if(tagCDATA){
2093 arrCDATA.push(match[0]);
210 }
211 else{
212187 if((tagName = match[4])){//标签开始
213174 arrAttrs = [];
214174 var attrs = match[5],
215 attrMatch,
216 attrMatchCount = 0;
217174 while((attrMatch = regAttr.exec(attrs))){
218167 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] : '';
224167 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
225167 attrMatchCount += attrMatch[0].length;
226 }
227174 if(attrMatchCount === attrs.length){
228174 saveBlock('tagstart', match[0], matchIndex, {
229 'tagName': tagName,
230 'attrs': arrAttrs,
231 'close': match[6]
232 });
233174 if(mapCdataTags[tagName]){
23419 tagCDATA = tagName;
23519 attrsCDATA = arrAttrs.concat();
23619 arrCDATA = [];
23719 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
253123 if(html.length > lastIndex){
254 //结尾文本
25513 text = html.substring(lastIndex, html.length);
25613 saveBlock('text', text, lastIndex);
257 }
258
259123 self.fire('end', {
260 pos: lastIndex,
261 line: line,
262 col: lastIndex - lastLineIndex + 1
263 });
264
265 //存储区块
266123 function saveBlock(type, raw, pos, data){
267385 var col = pos - lastLineIndex + 1;
268385 if(data === undefined){
26981 data = {};
270 }
271385 data.raw = raw;
272385 data.pos = pos;
273385 data.line = line;
274385 data.col = col;
275385 arrBlocks.push(data);
276385 self.fire(type, data);
277385 var lineMatch;
278385 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){
288165 var _listeners = this._listeners;
289165 var arrTypes = types.split(/[,\s]/), type;
290165 for(var i=0, l = arrTypes.length;i<l;i++){
291170 type = arrTypes[i];
292170 if (_listeners[type] === undefined){
293149 _listeners[type] = [];
294 }
295170 _listeners[type].push(listener);
296 }
297 },
298
299 // fire event
300 fire: function(type, data){
301631 if (data === undefined){
3020 data = {};
303 }
304631 data.type = type;
305631 var self = this,
306 listeners = [],
307 listenersType = self._listeners[type],
308 listenersAll = self._listeners['all'];
309631 if (listenersType !== undefined){
310190 listeners = listeners.concat(listenersType);
311 }
312631 if (listenersAll !== undefined){
313137 listeners = listeners.concat(listenersAll);
314 }
315631 for (var i = 0, l = listeners.length; i < l; i++){
316354 listeners[i].call(self, data);
317 }
318 },
319
320 // remove event
321 removeListener: function(type, listener){
32223 var listenersType = this._listeners[type];
32323 if(listenersType !== undefined){
32421 for (var i = 0, l = listenersType.length; i < l; i++){
32514 if (listenersType[i] === listener){
32614 listenersType.splice(i, 1);
32714 break;
328 }
329 }
330 }
331 },
332
333 //fix pos if event.raw have \n
334 fixPos: function(event, index){
3358 var text = event.raw.substr(0, index);
3368 var arrLines = text.split(/\r?\n/),
337 lineCount = arrLines.length - 1,
338 line = event.line, col;
3398 if(lineCount > 0){
3402 line += lineCount;
3412 col = arrLines[lineCount].length + 1;
342 }
343 else{
3446 col = event.col + index;
345 }
3468 return {
347 line: line,
348 col: col
349 };
350 },
351
352 // covert array type of attrs to map
353 getMapAttrs: function(arrAttrs){
35433 var mapAttrs = {},
355 attr;
35633 for(var i=0,l=arrAttrs.length;i<l;i++){
35731 attr = arrAttrs[i];
35831 mapAttrs[attr.name] = attr.value;
359 }
36033 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) 2015, Yanis Wang <yanis.wang@gmail.com>
373 * Copyright (c) 2014, Takeshi Kurosawa <taken.spc@gmail.com>
374 * MIT Licensed
375 */
3761HTMLHint.addRule({
377 id: 'alt-require',
378 description: 'The alt attribute of an <img> element must be present and alt attribute of area[href] and input[type=image] must have a value.',
379 init: function(parser, reporter){
38014 var self = this;
38114 parser.addListener('tagstart', function(event){
38214 var tagName = event.tagName.toLowerCase(),
383 mapAttrs = parser.getMapAttrs(event.attrs),
384 col = event.col + tagName.length + 1,
385 selector;
38614 if(tagName === 'img' && !('alt' in mapAttrs)){
3872 reporter.warn('An alt attribute must be present on <img> elements.', event.line, col, self, event.raw);
388 }
38912 else if((tagName === 'area' && 'href' in mapAttrs) ||
390 (tagName === 'input' && mapAttrs['type'] === 'image')){
3916 if(!('alt' in mapAttrs) || mapAttrs['alt'] === ''){
3924 selector = tagName === 'area' ? 'area[href]' : 'input[type=image]';
3934 reporter.warn('The alt attribute of ' + selector + ' must have a value.', event.line, col, self, event.raw);
394 }
395 }
396 });
397 }
398});
399
400/**
401 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
402 * MIT Licensed
403 */
4041HTMLHint.addRule({
405 id: 'attr-lowercase',
406 description: 'All attribute names must be in lowercase.',
407 init: function(parser, reporter){
4085 var self = this;
4095 parser.addListener('tagstart', function(event){
4109 var attrs = event.attrs,
411 attr,
412 col = event.col + event.tagName.length + 1;
4139 for(var i=0, l=attrs.length;i<l;i++){
41415 attr = attrs[i];
41515 var attrName = attr.name;
41615 if(attrName !== attrName.toLowerCase()){
4174 reporter.error('The attribute name of [ '+attrName+' ] must be in lowercase.', event.line, col + attr.index, self, attr.raw);
418 }
419 }
420 });
421 }
422});
423/**
424 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
425 * MIT Licensed
426 */
4271HTMLHint.addRule({
428 id: 'attr-no-duplication',
429 description: 'Elements cannot have duplicate attributes.',
430 init: function(parser, reporter){
4315 var self = this;
4325 parser.addListener('tagstart', function(event){
4339 var attrs = event.attrs;
4349 var attr;
4359 var attrName;
4369 var col = event.col + event.tagName.length + 1;
437
4389 var mapAttrName = {};
4399 for(var i=0, l=attrs.length;i<l;i++){
44016 attr = attrs[i];
44116 attrName = attr.name;
44216 if(mapAttrName[attrName] === true){
4433 reporter.error('Duplicate of attribute name [ '+attr.name+' ] was found.', event.line, col + attr.index, self, attr.raw);
444 }
44516 mapAttrName[attrName] = true;
446 }
447 });
448 }
449});
450/**
451 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
452 * MIT Licensed
453 */
4541HTMLHint.addRule({
455 id: 'attr-unsafe-chars',
456 description: 'Attribute values cannot contain unsafe chars.',
457 init: function(parser, reporter){
4582 var self = this;
4592 parser.addListener('tagstart', function(event){
4602 var attrs = event.attrs,
461 attr,
462 col = event.col + event.tagName.length + 1;
4632 var regUnsafe = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/;
4642 for(var i=0, l=attrs.length;i<l;i++){
4652 attr = attrs[i];
4662 if(regUnsafe.test(attr.value) === true){
4671 reporter.warn('The value of attribute [ '+attr.name+' ] cannot contain an unsafe char.', event.line, col + attr.index, self, attr.raw);
468 }
469 }
470 });
471 }
472});
473/**
474 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
475 * MIT Licensed
476 */
4771HTMLHint.addRule({
478 id: 'attr-value-double-quotes',
479 description: 'Attribute values must be in double quotes.',
480 init: function(parser, reporter){
4815 var self = this;
4825 parser.addListener('tagstart', function(event){
4839 var attrs = event.attrs,
484 attr,
485 col = event.col + event.tagName.length + 1;
4869 for(var i=0, l=attrs.length;i<l;i++){
48719 attr = attrs[i];
48819 if((attr.value !== '' && attr.quote !== '"') ||
489 (attr.value === '' && attr.quote === "'")){
4905 reporter.error('The value of attribute [ '+attr.name+' ] must be in double quotes.', event.line, col + attr.index, self, attr.raw);
491 }
492 }
493 });
494 }
495});
496/**
497 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
498 * MIT Licensed
499 */
5001HTMLHint.addRule({
501 id: 'attr-value-not-empty',
502 description: 'All attributes must have values.',
503 init: function(parser, reporter){
5043 var self = this;
5053 parser.addListener('tagstart', function(event){
5063 var attrs = event.attrs,
507 attr,
508 col = event.col + event.tagName.length + 1;
5093 for(var i=0, l=attrs.length;i<l;i++){
5103 attr = attrs[i];
5113 if(attr.quote === '' && attr.value === ''){
5121 reporter.warn('The attribute [ '+attr.name+' ] must have a value.', event.line, col + attr.index, self, attr.raw);
513 }
514 }
515 });
516 }
517});
518/**
519 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
520 * MIT Licensed
521 */
5221HTMLHint.addRule({
523 id: 'csslint',
524 description: 'Scan css with csslint.',
525 init: function(parser, reporter, options){
5261 var self = this;
5271 parser.addListener('cdata', function(event){
5281 if(event.tagName.toLowerCase() === 'style'){
529
5301 var cssVerify;
531
5321 if(typeof exports === 'object' && require){
5331 cssVerify = require("csslint").CSSLint.verify;
534 }
535 else{
5360 cssVerify = CSSLint.verify;
537 }
538
5391 if(options !== undefined){
5401 var styleLine = event.line - 1,
541 styleCol = event.col - 1;
5421 try{
5431 var messages = cssVerify(event.raw, options).messages;
5441 messages.forEach(function(error){
5452 var line = error.line;
5462 reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
547 });
548 }
549 catch(e){}
550 }
551
552 }
553 });
554 }
555});
556/**
557 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
558 * MIT Licensed
559 */
5601HTMLHint.addRule({
561 id: 'doctype-first',
562 description: 'Doctype must be declared first.',
563 init: function(parser, reporter){
5645 var self = this;
5655 var allEvent = function(event){
56610 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
5675 return;
568 }
5695 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
5704 reporter.error('Doctype must be declared first.', event.line, event.col, self, event.raw);
571 }
5725 parser.removeListener('all', allEvent);
573 };
5745 parser.addListener('all', allEvent);
575 }
576});
577/**
578 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
579 * MIT Licensed
580 */
5811HTMLHint.addRule({
582 id: 'doctype-html5',
583 description: 'Invalid doctype. Use: "<!DOCTYPE html>"',
584 init: function(parser, reporter){
5852 var self = this;
5862 function onComment(event){
5879 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
5881 reporter.warn('Invalid doctype. Use: "<!DOCTYPE html>"', event.line, event.col, self, event.raw);
589 }
590 }
5912 function onTagStart(){
5922 parser.removeListener('comment', onComment);
5932 parser.removeListener('tagstart', onTagStart);
594 }
5952 parser.addListener('all', onComment);
5962 parser.addListener('tagstart', onTagStart);
597 }
598});
599/**
600 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
601 * MIT Licensed
602 */
6031HTMLHint.addRule({
604 id: 'head-script-disabled',
605 description: 'The <script> tag cannot be used in a <head> tag.',
606 init: function(parser, reporter){
6077 var self = this;
6087 var reScript = /^(text\/javascript|application\/javascript)$/i;
6097 function onTagStart(event){
61013 var mapAttrs = parser.getMapAttrs(event.attrs);
61113 var type = mapAttrs.type;
61213 if(event.tagName.toLowerCase() === 'script' &&
613 (!type || reScript.test(type) === true)){
6144 reporter.warn('The <script> tag cannot be used in a <head> tag.', event.line, event.col, self, event.raw);
615 }
616 }
6177 function onTagEnd(event){
61815 if(event.tagName.toLowerCase() === 'head'){
6197 parser.removeListener('tagstart', onTagStart);
6207 parser.removeListener('tagstart', onTagEnd);
621 }
622 }
6237 parser.addListener('tagstart', onTagStart);
6247 parser.addListener('tagend', onTagEnd);
625 }
626});
627/**
628 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
629 * MIT Licensed
630 */
6311HTMLHint.addRule({
632 id: 'href-abs-or-rel',
633 description: 'An href attribute must be either absolute or relative.',
634 init: function(parser, reporter, options){
6354 var self = this;
636
6374 var hrefMode = options === 'abs' ? 'absolute' : 'relative';
638
6394 parser.addListener('tagstart', function(event){
64016 var attrs = event.attrs;
64116 var attr;
64216 var col = event.col + event.tagName.length + 1;
643
64416 for(var i=0, l=attrs.length;i<l;i++){
64516 attr = attrs[i];
64616 if(attr.name === 'href'){
64716 if((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) ||
648 (hrefMode === 'relative' && /^https?:\/\//.test(attr.value) === true)){
6494 reporter.warn('The value of the href attribute [ '+attr.value+' ] must be '+hrefMode+'.', event.line, col + attr.index, self, attr.raw);
650 }
65116 break;
652 }
653 }
654 });
655 }
656});
657/**
658 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
659 * MIT Licensed
660 */
6611HTMLHint.addRule({
662 id: 'id-class-ad-disabled',
663 description: 'The id and class attributes cannot use the ad keyword, it will be blocked by adblock software.',
664 init: function(parser, reporter){
66517 var self = this;
66617 parser.addListener('tagstart', function(event){
66717 var attrs = event.attrs;
66817 var attr;
66917 var attrName;
67017 var col = event.col + event.tagName.length + 1;
671
67217 for(var i=0, l=attrs.length;i<l;i++){
67320 attr = attrs[i];
67420 attrName = attr.name;
67520 if(/^(id|class)$/i.test(attrName)){
67620 if(/(^|[-\_])ad([-\_]|$)/i.test(attr.value)){
67714 reporter.warn('The value of attribute '+attrName+' cannot use the ad keyword.', event.line, col + attr.index, self, attr.raw);
678 }
679 }
680 }
681 });
682 }
683});
684/**
685 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
686 * MIT Licensed
687 */
6881HTMLHint.addRule({
689 id: 'id-class-value',
690 description: 'The id and class attribute values must meet the specified rules.',
691 init: function(parser, reporter, options){
6928 var self = this;
6938 var arrRules = {
694 'underline': {
695 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
696 'message': 'The id and class attribute values must be in lowercase and split by an underscore.'
697 },
698 'dash': {
699 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
700 'message': 'The id and class attribute values must be in lowercase and split by a dash.'
701 },
702 'hump': {
703 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
704 'message': 'The id and class attribute values must meet the camelCase style.'
705 }
706 }, rule;
7078 if(typeof options === 'string'){
7086 rule = arrRules[options];
709 }
710 else{
7112 rule = options;
712 }
7138 if(rule && rule.regId){
7148 var regId = rule.regId,
715 message = rule.message;
7168 parser.addListener('tagstart', function(event){
7178 var attrs = event.attrs,
718 attr,
719 col = event.col + event.tagName.length + 1;
7208 for(var i=0, l1=attrs.length;i<l1;i++){
72116 attr = attrs[i];
72216 if(attr.name.toLowerCase() === 'id'){
7238 if(regId.test(attr.value) === false){
7244 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
725 }
726 }
72716 if(attr.name.toLowerCase() === 'class'){
7288 var arrClass = attr.value.split(/\s+/g), classValue;
7298 for(var j=0, l2=arrClass.length;j<l2;j++){
7308 classValue = arrClass[j];
7318 if(classValue && regId.test(classValue) === false){
7324 reporter.warn(message, event.line, col + attr.index, self, classValue);
733 }
734 }
735 }
736 }
737 });
738 }
739 }
740});
741/**
742 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
743 * MIT Licensed
744 */
7451HTMLHint.addRule({
746 id: 'id-unique',
747 description: 'The value of id attributes must be unique.',
748 init: function(parser, reporter){
7495 var self = this;
7505 var mapIdCount = {};
7515 parser.addListener('tagstart', function(event){
75211 var attrs = event.attrs,
753 attr,
754 id,
755 col = event.col + event.tagName.length + 1;
75611 for(var i=0, l=attrs.length;i<l;i++){
75717 attr = attrs[i];
75817 if(attr.name.toLowerCase() === 'id'){
7598 id = attr.value;
7608 if(id){
7618 if(mapIdCount[id] === undefined){
7625 mapIdCount[id] = 1;
763 }
764 else{
7653 mapIdCount[id] ++;
766 }
7678 if(mapIdCount[id] > 1){
7683 reporter.error('The id value [ '+id+' ] must be unique.', event.line, col + attr.index, self, attr.raw);
769 }
770 }
7718 break;
772 }
773 }
774 });
775 }
776});
777/**
778 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
779 * MIT Licensed
780 */
7811HTMLHint.addRule({
782 id: 'jshint',
783 description: 'Scan script with jshint.',
784 init: function(parser, reporter, options){
7854 var self = this;
7864 parser.addListener('cdata', function(event){
7874 if(event.tagName.toLowerCase() === 'script'){
788
7894 var mapAttrs = parser.getMapAttrs(event.attrs),
790 type = mapAttrs.type;
791
792 // Only scan internal javascript
7934 if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
7942 return;
795 }
796
7972 var jsVerify;
798
7992 if(typeof exports === 'object' && require){
8002 jsVerify = require('jshint').JSHINT;
801 }
802 else{
8030 jsVerify = JSHINT;
804 }
805
8062 if(options !== undefined){
8072 var styleLine = event.line - 1,
808 styleCol = event.col - 1;
8092 var code = event.raw.replace(/\t/g,' ');
8102 try{
8112 var status = jsVerify(code, options);
8122 if(status === false){
8132 jsVerify.errors.forEach(function(error){
8148 var line = error.line;
8158 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
816 });
817 }
818 }
819 catch(e){}
820 }
821
822 }
823 });
824 }
825});
826/**
827 * Copyright (c) 2014, Yanis Wang <yanis.wang@gmail.com>
828 * MIT Licensed
829 */
8301HTMLHint.addRule({
831 id: 'space-tab-mixed-disabled',
832 description: 'Do not mix tabs and spaces for indentation.',
833 init: function(parser, reporter){
8346 var self = this;
8356 parser.addListener('text', function(event){
83612 var raw = event.raw;
83712 var reMixed = /(^|\r?\n)( +\t|\t+ )/g;
83812 var match;
83912 while((match = reMixed.exec(raw))){
8403 var fixedPos = parser.fixPos(event, match.index + match[1].length);
8413 reporter.warn('Do not mix tabs and spaces for indentation.', fixedPos.line, 1, self, event.raw);
842 }
843 });
844 }
845});
846/**
847 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
848 * MIT Licensed
849 */
8501HTMLHint.addRule({
851 id: 'spec-char-escape',
852 description: 'Special characters must be escaped.',
853 init: function(parser, reporter){
8545 var self = this;
8555 parser.addListener('text', function(event){
8565 var raw = event.raw,
857 reSpecChar = /[<>]/g,
858 match;
8595 while((match = reSpecChar.exec(raw))){
8605 var fixedPos = parser.fixPos(event, match.index);
8615 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
862 }
863 });
864 }
865});
866/**
867 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
868 * MIT Licensed
869 */
8701HTMLHint.addRule({
871 id: 'src-not-empty',
872 description: 'The src attribute of an img(script,link) must have a value.',
873 init: function(parser, reporter){
8746 var self = this;
8756 parser.addListener('tagstart', function(event){
87635 var tagName = event.tagName,
877 attrs = event.attrs,
878 attr,
879 col = event.col + tagName.length + 1;
88035 for(var i=0, l=attrs.length;i<l;i++){
88142 attr = attrs[i];
88242 if(((/^(img|script|embed|bgsound|iframe)$/.test(tagName) === true && attr.name === 'src') ||
883 (tagName === 'link' && attr.name === 'href') ||
884 (tagName === 'object' && attr.name === 'data')) &&
885 attr.value === ''){
88616 reporter.error('The attribute [ '+attr.name + ' ] of the tag [ '+tagName+' ] must have a value.', event.line, col + attr.index, self, attr.raw);
887 }
888 }
889 });
890 }
891});
892/**
893 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
894 * MIT Licensed
895 */
8961HTMLHint.addRule({
897 id: 'style-disabled',
898 description: '<style> tags cannot be used.',
899 init: function(parser, reporter){
9002 var self = this;
9012 parser.addListener('tagstart', function(event){
9024 if(event.tagName.toLowerCase() === 'style'){
9031 reporter.warn('The <style> tag cannot be used.', event.line, event.col, self, event.raw);
904 }
905 });
906 }
907});
908
909/**
910 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
911 * MIT Licensed
912 */
9131HTMLHint.addRule({
914 id: 'tag-pair',
915 description: 'Tag must be paired.',
916 init: function(parser, reporter){
9176 var self = this;
9186 var stack=[],
919 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track");//HTML 4.01 + HTML5
9206 parser.addListener('tagstart', function(event){
92111 var tagName = event.tagName.toLowerCase();
92211 if (mapEmptyTags[tagName] === undefined && !event.close){
9239 stack.push(tagName);
924 }
925 });
9266 parser.addListener('tagend', function(event){
9275 var tagName = event.tagName.toLowerCase();
928 //向上寻找匹配的开始标签
9295 for(var pos = stack.length-1;pos >= 0; pos--){
9305 if(stack[pos] === tagName){
9314 break;
932 }
933 }
9345 if(pos >= 0){
9354 var arrTags = [];
9364 for(var i=stack.length-1;i>pos;i--){
9371 arrTags.push('</'+stack[i]+'>');
938 }
9394 if(arrTags.length > 0){
9401 reporter.error('Tag must be paired, missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
941 }
9424 stack.length=pos;
943 }
944 else{
9451 reporter.error('Tag must be paired, no start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
946 }
947 });
9486 parser.addListener('end', function(event){
9496 var arrTags = [];
9506 for(var i=stack.length-1;i>=0;i--){
9514 arrTags.push('</'+stack[i]+'>');
952 }
9536 if(arrTags.length > 0){
9544 reporter.error('Tag must be paired, missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
955 }
956 });
957 }
958});
959/**
960 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
961 * MIT Licensed
962 */
9631HTMLHint.addRule({
964 id: 'tag-self-close',
965 description: 'Empty tags must be self closed.',
966 init: function(parser, reporter){
9672 var self = this;
9682 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
9692 parser.addListener('tagstart', function(event){
9704 var tagName = event.tagName.toLowerCase();
9714 if(mapEmptyTags[tagName] !== undefined){
9724 if(!event.close){
9732 reporter.warn('The empty tag : [ '+tagName+' ] must be self closed.', event.line, event.col, self, event.raw);
974 }
975 }
976 });
977 }
978});
979/**
980 * Copyright (c) 2015, Yanis Wang <yanis.wang@gmail.com>
981 * MIT Licensed
982 */
9831HTMLHint.addRule({
984 id: 'tagname-lowercase',
985 description: 'All html element names must be in lowercase.',
986 init: function(parser, reporter){
9875 var self = this;
9885 parser.addListener('tagstart,tagend', function(event){
98917 var tagName = event.tagName;
99017 if(tagName !== tagName.toLowerCase()){
9916 reporter.error('The html element name of [ '+tagName+' ] must be in lowercase.', event.line, event.col, self, event.raw);
992 }
993 });
994 }
995});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 9d4b55221..4df644028 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -5,4 +5,4 @@ * (c) 2015 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(e){var t={};return t.version="0.9.7",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===e||0===Object.keys(n).length)&&(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),c.fire(t,i);for(var s;s=m.exec(a);)b++,w=n+m.lastIndex}var n,i,r,s,o,l,d,u,c=this,f=c._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,p=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,m=/\r?\n/g,v=0,h=0,w=0,b=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>v&&(u=t.substring(v,i),o?d.push(u):a("text",u,v)),v=g.lastIndex,!(r=n[1])||(o&&r===o&&(u=d.join(""),a("cdata",u,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=p.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&&(u=t.substring(v,t.length),a("text",u,v)),c.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:"alt-require",description:"Alt of img must be present and alt of area[href] and input[type=image] must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(n){var i,r=n.tagName.toLowerCase(),s=e.getMapAttrs(n.attrs),o=n.col+r.length+1;"img"!==r||"alt"in s?("area"===r&&"href"in s||"input"===r&&"image"===s.type)&&("alt"in s&&""!==s.alt||(i="area"===r?"area[href]":"input[type=image]",t.warn("Alt of "+i+" must be set value.",n.line,o,a,n.raw))):t.warn("Alt of img tag must be present.",n.line,o,a,n.raw)})}}),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-unsafe-chars",description:"Attribute value cant not use unsafe chars.",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=/[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/,o=0,l=i.length;l>o;o++)n=i[o],s.test(n.value)===!0&&t.warn("The value of attribute [ "+n.name+" ] cant not use unsafe chars.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,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(a){var n=e.getMapAttrs(a.attrs),s=n.type;"script"!==a.tagName.toLowerCase()||s&&r.test(s)!==!0||t.warn("The script tag can not be used in head.",a.line,a.col,i,a.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var i=this,r=/^(text\/javascript|application\/javascript)$/i;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 u,c=a.value.split(/\s+/g),f=0,g=c.length;g>f;f++)u=c[f],u&&s.test(u)===!1&&t.warn(o,e.line,r+a.index,i,u)})}}}),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:"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,u=i.raw.replace(/\t/g," ");try{var c=o(u,a);c===!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.7",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===e||0===Object.keys(n).length)&&(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-b+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=w,i.col=r,L.push(i),c.fire(t,i);for(var s;s=p.exec(a);)w++,b=n+p.lastIndex}var n,i,r,s,o,l,u,d,c=this,f=c._mapCdataTags,h=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,g=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,p=/\r?\n/g,m=0,v=0,b=0,w=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=h.exec(t);)if(i=n.index,i>m&&(d=t.substring(m,i),o?u.push(d):a("text",d,m)),m=h.lastIndex,!(r=n[1])||(o&&r===o&&(d=u.join(""),a("cdata",d,v,{tagName:o,attrs:l}),o=null,l=null,u=null),o))if(o)u.push(n[0]);else if(r=n[4]){s=[];for(var T,H=n[5],y=0;T=g.exec(H);){var x=T[1],M=T[2]?T[2]:T[4]?T[4]:"",R=T[3]?T[3]:T[5]?T[5]:T[6]?T[6]:"";s.push({name:x,value:R,quote:M,index:T.index,raw:T[0]}),y+=T[0].length}y===H.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),u=[],v=m)):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>m&&(d=t.substring(m,t.length),a("text",d,m)),c.fire("end",{pos:m,line:w,col:m-b+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:"alt-require",description:"The alt attribute of an element must be present and alt attribute of area[href] and input[type=image] must have a value.",init:function(e,t){var a=this;e.addListener("tagstart",function(n){var i,r=n.tagName.toLowerCase(),s=e.getMapAttrs(n.attrs),o=n.col+r.length+1;"img"!==r||"alt"in s?("area"===r&&"href"in s||"input"===r&&"image"===s.type)&&("alt"in s&&""!==s.alt||(i="area"===r?"area[href]":"input[type=image]",t.warn("The alt attribute of "+i+" must have a value.",n.line,o,a,n.raw))):t.warn("An alt attribute must be present on elements.",n.line,o,a,n.raw)})}}),HTMLHint.addRule({id:"attr-lowercase",description:"All attribute names must be in 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("The attribute name of [ "+l+" ] must be in lowercase.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Elements cannot have duplicate attributes.",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,u=r.length;u>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("Duplicate of attribute name [ "+n.name+" ] was found.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),HTMLHint.addRule({id:"attr-unsafe-chars",description:"Attribute values cannot contain unsafe chars.",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=/[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/,o=0,l=i.length;l>o;o++)n=i[o],s.test(n.value)===!0&&t.warn("The value of attribute [ "+n.name+" ] cannot contain an unsafe char.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute values must be in 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 be in double quotes.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"All attributes must have values.",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 have a 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 declared 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 declared first.",i.line,i.col,a,i.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:'Invalid doctype. Use: ""',init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn('Invalid doctype. Use: ""',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