# Coverage

98%
339
334
5

## htmlhint.js

98%
339
334
5

Line | Hits | Source
--- | --- | ---
1 |  | /**
2 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 |  | * MIT Licensed
4 |  | */
5 | 1 | var HTMLHint = (function (undefined) {
6 |  |
7 | 1 | var HTMLHint = {};
8 |  |
9 | 1 | HTMLHint.version = '@VERSION';
10 |  |
11 | 1 | HTMLHint.rules = {};
12 |  |
13 |  | //默认配置
14 | 1 | HTMLHint.defaultRuleset = {
15 |  | 'tagname-lowercase': true,
16 |  | 'attr-lowercase': true,
17 |  | 'attr-value-double-quotes': true,
18 |  | 'doctype-first': true,
19 |  | 'tag-pair': true,
20 |  | 'spec-char-escape': true,
21 |  | 'id-unique': true
22 |  | };
23 |  |
24 | 1 | HTMLHint.addRule = function(rule){
25 | 16 | HTMLHint.rules[rule.id] = rule;
26 |  | };
27 |  |
28 | 1 | HTMLHint.verify = function(html, ruleset){
29 | 45 | if(ruleset === undefined){
30 | 1 | ruleset = HTMLHint.defaultRuleset;
31 |  | }
32 | 45 | var parser = new HTMLParser(),
33 |  | reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
34 |  |
35 | 45 | var rules = HTMLHint.rules,
36 |  | rule;
37 | 45 | for (var id in ruleset){
38 | 51 | rule = rules[id];
39 | 51 | if (rule !== undefined){
40 | 51 | rule.init(parser, reporter, ruleset[id]);
41 |  | }
42 |  | }
43 |  |
44 | 45 | parser.parse(html);
45 |  |
46 | 45 | return reporter.messages;
47 |  | };
48 |  |
49 | 1 | return HTMLHint;
50 |  |
51 |  | })();
52 |  |
53 | 1 | if (typeof exports === 'object' && exports){
54 | 1 | exports.HTMLHint = HTMLHint;
55 |  | }
56 |  | /**
57 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
58 |  | * MIT Licensed
59 |  | */
60 | 1 | (function(HTMLHint, undefined){
61 |  |
62 | 1 | var Reporter = function(){
63 | 45 | var self = this;
64 | 45 | self._init.apply(self,arguments);
65 |  | };
66 |  |
67 | 1 | Reporter.prototype = {
68 |  | _init: function(lines, ruleset){
69 | 45 | var self = this;
70 | 45 | self.lines = lines;
71 | 45 | self.ruleset = ruleset;
72 | 45 | self.messages = [];
73 |  | },
74 |  | //错误
75 |  | error: function(message, line, col, rule, raw){
76 | 18 | this.report('error', message, line, col, rule, raw);
77 |  | },
78 |  | //警告
79 |  | warn: function(message, line, col, rule, raw){
80 | 26 | this.report('warning', message, line, col, rule, raw);
81 |  | },
82 |  | //信息
83 |  | info: function(message, line, col, rule, raw){
84 | 0 | this.report('info', message, line, col, rule, raw);
85 |  | },
86 |  | //报告
87 |  | report: function(type, message, line, col, rule, raw){
88 | 44 | var self = this;
89 | 44 | self.messages.push({
90 |  | type: type,
91 |  | message: message,
92 |  | raw: raw,
93 |  | evidence: self.lines[line-1],
94 |  | line: line,
95 |  | col: col,
96 |  | rule: {
97 |  | id: rule.id,
98 |  | description: rule.description,
99 |  | link: 'https://github.com/yaniswang/HTMLHint/wiki/' + rule.id
100 |  | }
101 |  | });
102 |  | }
103 |  | };
104 |  |
105 | 1 | HTMLHint.Reporter = Reporter;
106 |  |
107 |  | })(HTMLHint);
108 |  | /**
109 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
110 |  | * MIT Licensed
111 |  | */
112 | 1 | var HTMLParser = (function(undefined){
113 |  |
114 | 1 | var HTMLParser = function(){
115 | 70 | var self = this;
116 | 70 | self._init.apply(self,arguments);
117 |  | };
118 |  |
119 | 1 | HTMLParser.prototype = {
120 |  | _init: function(){
121 | 70 | var self = this;
122 | 70 | self._listeners = {};
123 | 70 | self._mapCdataTags = self.makeMap("script,style");
124 | 70 | self._arrBlocks = [];
125 |  | },
126 |  |
127 |  | makeMap: function(str){
128 | 76 | var obj = {}, items = str.split(",");
129 | 76 | for ( var i = 0; i < items.length; i++ ){
130 | 224 | obj[ items[i] ] = true;
131 |  | }
132 | 76 | return obj;
133 |  | },
134 |  |
135 |  | // parse html code
136 |  | parse: function(html){
137 |  |
138 | 70 | var self = this,
139 |  | mapCdataTags = self._mapCdataTags;
140 |  |
141 | 70 | var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?)*?)\s*(\/?))>/g,
142 |  | regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"']+)))?/g,
143 |  | regLine = /\r?\n/g;
144 |  |
145 | 70 | var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, attrsCDATA, arrCDATA, lastCDATAIndex = 0, text;
146 | 70 | var lastLineIndex = 0, line = 1;
147 | 70 | var arrBlocks = self._arrBlocks;
148 |  |
149 | 70 | self.fire('start', {
150 |  | pos: 0,
151 |  | line: 1,
152 |  | col: 1
153 |  | });
154 |  |
155 | 70 | while((match = regTag.exec(html))){
156 | 130 | matchIndex = match.index;
157 | 130 | if(matchIndex > lastIndex){//保存前面的文本或者CDATA
158 | 26 | text = html.substring(lastIndex, matchIndex);
159 | 26 | if(tagCDATA){
160 | 10 | arrCDATA.push(text);
161 |  | }
162 |  | else{//文本
163 | 16 | saveBlock('text', text, lastIndex);
164 |  | }
165 |  | }
166 | 130 | lastIndex = regTag.lastIndex;
167 |  |
168 | 130 | if((tagName = match[1])){
169 | 43 | if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
170 | 11 | text = arrCDATA.join('');
171 | 11 | saveBlock('cdata', text, lastCDATAIndex, {
172 |  | 'tagName': tagCDATA,
173 |  | 'attrs': attrsCDATA
174 |  | });
175 | 11 | tagCDATA = null;
176 | 11 | attrsCDATA = null;
177 | 11 | arrCDATA = null;
178 |  | }
179 | 43 | if(!tagCDATA){
180 |  | //标签结束
181 | 42 | saveBlock('tagend', match[0], matchIndex, {
182 |  | 'tagName': tagName
183 |  | });
184 | 42 | continue;
185 |  | }
186 |  | }
187 |  |
188 | 88 | if(tagCDATA){
189 | 1 | arrCDATA.push(match[0]);
190 |  | }
191 |  | else{
192 | 87 | if((tagName = match[4])){//标签开始
193 | 74 | arrAttrs = [];
194 | 74 | var attrs = match[5],
195 |  | attrMatch,
196 |  | attrMatchCount = 0;
197 | 74 | while((attrMatch = regAttr.exec(attrs))){
198 | 59 | var name = attrMatch[1],
199 |  | quote = attrMatch[2] ? attrMatch[2] :
200 |  | attrMatch[4] ? attrMatch[4] : '',
201 |  | value = attrMatch[3] ? attrMatch[3] :
202 |  | attrMatch[5] ? attrMatch[5] :
203 |  | attrMatch[6] ? attrMatch[6] : '';
204 | 59 | arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
205 | 59 | attrMatchCount += attrMatch[0].length;
206 |  | }
207 | 74 | if(attrMatchCount === attrs.length){
208 | 74 | saveBlock('tagstart', match[0], matchIndex, {
209 |  | 'tagName': tagName,
210 |  | 'attrs': arrAttrs,
211 |  | 'close': match[6]
212 |  | });
213 | 74 | if(mapCdataTags[tagName]){
214 | 11 | tagCDATA = tagName;
215 | 11 | attrsCDATA = arrAttrs.concat();
216 | 11 | arrCDATA = [];
217 | 11 | lastCDATAIndex = lastIndex;
218 |  | }
219 |  | }
220 |  | else{//如果出现漏匹配，则把当前内容匹配为text
221 | 0 | saveBlock('text', match[0], matchIndex);
222 |  | }
223 |  | }
224 | 13 | else if(match[2] || match[3]){//注释标签
225 | 13 | saveBlock('comment', match[0], matchIndex, {
226 |  | 'content': match[2] || match[3],
227 |  | 'long': match[2]?true:false
228 |  | });
229 |  | }
230 |  | }
231 |  | }
232 |  |
233 | 70 | if(html.length > lastIndex){
234 |  | //结尾文本
235 | 13 | text = html.substring(lastIndex, html.length);
236 | 13 | saveBlock('text', text, lastIndex);
237 |  | }
238 |  |
239 | 70 | self.fire('end', {
240 |  | pos: lastIndex,
241 |  | line: line,
242 |  | col: lastIndex - lastLineIndex + 1
243 |  | });
244 |  |
245 |  | //存储区块
246 | 70 | function saveBlock(type, raw, pos, data){
247 | 169 | var col = pos - lastLineIndex + 1;
248 | 169 | if(data === undefined){
249 | 29 | data = {};
250 |  | }
251 | 169 | data.raw = raw;
252 | 169 | data.pos = pos;
253 | 169 | data.line = line;
254 | 169 | data.col = col;
255 | 169 | arrBlocks.push(data);
256 | 169 | self.fire(type, data);
257 | 169 | var lineMatch;
258 | 169 | while((lineMatch = regLine.exec(raw))){
259 | 18 | line ++;
260 | 18 | lastLineIndex = pos + regLine.lastIndex;
261 |  | }
262 |  | }
263 |  |
264 |  | },
265 |  |
266 |  | // add event
267 |  | addListener: function(types, listener){
268 | 89 | var _listeners = this._listeners;
269 | 89 | var arrTypes = types.split(/[,\s]/), type;
270 | 89 | for(var i=0, l = arrTypes.length;i<l;i++){
271 | 92 | type = arrTypes[i];
272 | 92 | if (_listeners[type] === undefined){
273 | 87 | _listeners[type] = [];
274 |  | }
275 | 92 | _listeners[type].push(listener);
276 |  | }
277 |  | },
278 |  |
279 |  | // fire event
280 |  | fire: function(type, data){
281 | 309 | if (data === undefined){
282 | 0 | data = {};
283 |  | }
284 | 309 | data.type = type;
285 | 309 | var self = this,
286 |  | listeners = [],
287 |  | listenersType = self._listeners[type],
288 |  | listenersAll = self._listeners['all'];
289 | 309 | if (listenersType !== undefined){
290 | 75 | listeners = listeners.concat(listenersType);
291 |  | }
292 | 309 | if (listenersAll !== undefined){
293 | 123 | listeners = listeners.concat(listenersAll);
294 |  | }
295 | 309 | for (var i = 0, l = listeners.length; i < l; i++){
296 | 195 | listeners[i].call(self, data);
297 |  | }
298 |  | },
299 |  |
300 |  | // remove event
301 |  | removeListener: function(type, listener){
302 | 13 | var listenersType = this._listeners[type];
303 | 13 | if(listenersType !== undefined){
304 | 11 | for (var i = 0, l = listenersType.length; i < l; i++){
305 | 8 | if (listenersType[i] === listener){
306 | 8 | listenersType.splice(i, 1);
307 | 8 | break;
308 |  | }
309 |  | }
310 |  | }
311 |  | },
312 |  |
313 |  | //fix pos if event.raw have \n
314 |  | fixPos: function(event, index){
315 | 3 | var text = event.raw.substr(0, index);
316 | 3 | var arrLines = text.split(/\r?\n/),
317 |  | lineCount = arrLines.length - 1,
318 |  | line = event.line, col;
319 | 3 | if(lineCount > 0){
320 | 1 | line += lineCount;
321 | 1 | col = arrLines[lineCount].length + 1;
322 |  | }
323 |  | else{
324 | 2 | col = event.col + index;
325 |  | }
326 | 3 | return {
327 |  | line: line,
328 |  | col: col
329 |  | };
330 |  | },
331 |  |
332 |  | // covert array type of attrs to map
333 |  | getMapAttrs: function(arrAttrs){
334 | 6 | var mapAttrs = {},
335 |  | attr;
336 | 6 | for(var i=0,l=arrAttrs.length;i<l;i++){
337 | 6 | attr = arrAttrs[i];
338 | 6 | mapAttrs[attr.name] = attr.value;
339 |  | }
340 | 6 | return mapAttrs;
341 |  | }
342 |  | };
343 |  |
344 | 1 | return HTMLParser;
345 |  |
346 |  | })();
347 |  |
348 | 1 | if (typeof exports === 'object' && exports){
349 | 1 | exports.HTMLParser = HTMLParser;
350 |  | }
351 |  | /**
352 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
353 |  | * MIT Licensed
354 |  | */
355 | 1 | HTMLHint.addRule({
356 |  | id: 'attr-lowercase',
357 |  | description: 'Attribute name must be lowercase.',
358 |  | init: function(parser, reporter){
359 | 3 | var self = this;
360 | 3 | parser.addListener('tagstart', function(event){
361 | 3 | var attrs = event.attrs,
362 |  | attr,
363 |  | col = event.col + event.tagName.length + 1;
364 | 3 | for(var i=0, l=attrs.length;i<l;i++){
365 | 3 | attr = attrs[i];
366 | 3 | var attrName = attr.name;
367 | 3 | if(attrName !== attrName.toLowerCase()){
368 | 2 | reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
369 |  | }
370 |  | }
371 |  | });
372 |  | }
373 |  | });
374 |  | /**
375 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
376 |  | * MIT Licensed
377 |  | */
378 | 1 | HTMLHint.addRule({
379 |  | id: 'attr-value-double-quotes',
380 |  | description: 'Attribute value must closed by double quotes.',
381 |  | init: function(parser, reporter){
382 | 4 | var self = this;
383 | 4 | parser.addListener('tagstart', function(event){
384 | 4 | var attrs = event.attrs,
385 |  | attr,
386 |  | col = event.col + event.tagName.length + 1;
387 | 4 | for(var i=0, l=attrs.length;i<l;i++){
388 | 6 | attr = attrs[i];
389 | 6 | if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
390 | 2 | reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
391 |  | }
392 |  | }
393 |  | });
394 |  | }
395 |  | });
396 |  | /**
397 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
398 |  | * MIT Licensed
399 |  | */
400 | 1 | HTMLHint.addRule({
401 |  | id: 'attr-value-not-empty',
402 |  | description: 'Attribute must set value.',
403 |  | init: function(parser, reporter){
404 | 3 | var self = this;
405 | 3 | parser.addListener('tagstart', function(event){
406 | 3 | var attrs = event.attrs,
407 |  | attr,
408 |  | col = event.col + event.tagName.length + 1;
409 | 3 | for(var i=0, l=attrs.length;i<l;i++){
410 | 3 | attr = attrs[i];
411 | 3 | if(attr.quote === '' && attr.value === ''){
412 | 1 | reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
413 |  | }
414 |  | }
415 |  | });
416 |  | }
417 |  | });
418 |  | /**
419 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
420 |  | * MIT Licensed
421 |  | */
422 | 1 | HTMLHint.addRule({
423 |  | id: 'csslint',
424 |  | description: 'Scan css with csslint.',
425 |  | init: function(parser, reporter, options){
426 | 1 | var self = this;
427 | 1 | parser.addListener('cdata', function(event){
428 | 1 | if(event.tagName.toLowerCase() === 'style'){
429 |  |
430 | 1 | var cssVerify;
431 |  |
432 | 1 | if(typeof exports === 'object' && require){
433 | 1 | cssVerify = require("csslint").CSSLint.verify;
434 |  | }
435 |  | else{
436 | 0 | cssVerify = CSSLint.verify;
437 |  | }
438 |  |
439 | 1 | if(options !== undefined){
440 | 1 | var styleLine = event.line - 1,
441 |  | styleCol = event.col - 1;
442 | 1 | try{
443 | 1 | var messages = cssVerify(event.raw, options).messages;
444 | 1 | messages.forEach(function(error){
445 | 2 | var line = error.line;
446 | 2 | reporter[error.type==='warning'?'warn':'error']('['+error.rule.id+'] '+error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
447 |  | });
448 |  | }
449 |  | catch(e){}
450 |  | }
451 |  |
452 |  | }
453 |  | });
454 |  | }
455 |  | });
456 |  | /**
457 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
458 |  | * MIT Licensed
459 |  | */
460 | 1 | HTMLHint.addRule({
461 |  | id: 'doctype-first',
462 |  | description: 'Doctype must be first.',
463 |  | init: function(parser, reporter){
464 | 3 | var self = this;
465 | 3 | var allEvent = function(event){
466 | 6 | if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
467 | 3 | return;
468 |  | }
469 | 3 | if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
470 | 2 | reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
471 |  | }
472 | 3 | parser.removeListener('all', allEvent);
473 |  | };
474 | 3 | parser.addListener('all', allEvent);
475 |  | }
476 |  | });
477 |  | /**
478 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
479 |  | * MIT Licensed
480 |  | */
481 | 1 | HTMLHint.addRule({
482 |  | id: 'doctype-html5',
483 |  | description: 'Doctype must be html5.',
484 |  | init: function(parser, reporter){
485 | 2 | var self = this;
486 | 2 | function onComment(event){
487 | 9 | if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
488 | 1 | reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
489 |  | }
490 |  | }
491 | 2 | function onTagStart(){
492 | 2 | parser.removeListener('comment', onComment);
493 | 2 | parser.removeListener('tagstart', onTagStart);
494 |  | }
495 | 2 | parser.addListener('all', onComment);
496 | 2 | parser.addListener('tagstart', onTagStart);
497 |  | }
498 |  | });
499 |  | /**
500 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
501 |  | * MIT Licensed
502 |  | */
503 | 1 | HTMLHint.addRule({
504 |  | id: 'head-script-disabled',
505 |  | description: 'The script tag can not be used in head.',
506 |  | init: function(parser, reporter){
507 | 3 | var self = this;
508 | 3 | function onTagStart(event){
509 | 5 | if(event.tagName.toLowerCase() === 'script'){
510 | 2 | reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
511 |  | }
512 |  | }
513 | 3 | function onTagEnd(event){
514 | 7 | if(event.tagName.toLowerCase() === 'head'){
515 | 3 | parser.removeListener('tagstart', onTagStart);
516 | 3 | parser.removeListener('tagstart', onTagEnd);
517 |  | }
518 |  | }
519 | 3 | parser.addListener('tagstart', onTagStart);
520 | 3 | parser.addListener('tagend', onTagEnd);
521 |  | }
522 |  | });
523 |  | /**
524 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
525 |  | * MIT Licensed
526 |  | */
527 | 1 | HTMLHint.addRule({
528 |  | id: 'id-class-value',
529 |  | description: 'Id and class value must meet some rules.',
530 |  | init: function(parser, reporter, options){
531 | 8 | var self = this;
532 | 8 | var arrRules = {
533 |  | 'underline': {
534 |  | 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
535 |  | 'message': 'Id and class value must lower case and split by underline.'
536 |  | },
537 |  | 'dash': {
538 |  | 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
539 |  | 'message': 'Id and class value must lower case and split by dash.'
540 |  | },
541 |  | 'hump': {
542 |  | 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
543 |  | 'message': 'Id and class value must meet hump style.'
544 |  | }
545 |  | }, rule;
546 | 8 | if(typeof options === 'string'){
547 | 6 | rule = arrRules[options];
548 |  | }
549 |  | else{
550 | 2 | rule = options;
551 |  | }
552 | 8 | if(rule && rule.regId){
553 | 8 | var regId = rule.regId,
554 |  | message = rule.message;
555 | 8 | parser.addListener('tagstart', function(event){
556 | 8 | var attrs = event.attrs,
557 |  | attr,
558 |  | col = event.col + event.tagName.length + 1;
559 | 8 | for(var i=0, l1=attrs.length;i<l1;i++){
560 | 16 | attr = attrs[i];
561 | 16 | if(attr.name.toLowerCase() === 'id'){
562 | 8 | if(regId.test(attr.value) === false){
563 | 4 | reporter.warn(message, event.line, col + attr.index, self, attr.raw);
564 |  | }
565 |  | }
566 | 16 | if(attr.name.toLowerCase() === 'class'){
567 | 8 | var arrClass = attr.value.split(/\s+/g), classValue;
568 | 8 | for(var j=0, l2=arrClass.length;j<l2;j++){
569 | 8 | classValue = arrClass[j];
570 | 8 | if(classValue && regId.test(classValue) === false){
571 | 4 | reporter.warn(message, event.line, col + attr.index, self, classValue);
572 |  | }
573 |  | }
574 |  | }
575 |  | }
576 |  | });
577 |  | }
578 |  | }
579 |  | });
580 |  | /**
581 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
582 |  | * MIT Licensed
583 |  | */
584 | 1 | HTMLHint.addRule({
585 |  | id: 'id-unique',
586 |  | description: 'Id must be unique.',
587 |  | init: function(parser, reporter){
588 | 3 | var self = this;
589 | 3 | var mapIdCount = {};
590 | 3 | parser.addListener('tagstart', function(event){
591 | 5 | var attrs = event.attrs,
592 |  | attr,
593 |  | id,
594 |  | col = event.col + event.tagName.length + 1;
595 | 5 | for(var i=0, l=attrs.length;i<l;i++){
596 | 5 | attr = attrs[i];
597 | 5 | if(attr.name.toLowerCase() === 'id'){
598 | 4 | id = attr.value;
599 | 4 | if(id){
600 | 4 | if(mapIdCount[id] === undefined){
601 | 3 | mapIdCount[id] = 1;
602 |  | }
603 |  | else{
604 | 1 | mapIdCount[id] ++;
605 |  | }
606 | 4 | if(mapIdCount[id] > 1){
607 | 1 | reporter.error('Id redefinition of [ '+id+' ].', event.line, col + attr.index, self, attr.raw);
608 |  | }
609 |  | }
610 | 4 | break;
611 |  | }
612 |  | }
613 |  | });
614 |  | }
615 |  | });
616 |  | /**
617 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
618 |  | * MIT Licensed
619 |  | */
620 | 1 | HTMLHint.addRule({
621 |  | id: 'img-alt-require',
622 |  | description: 'Alt of img tag must be set value.',
623 |  | init: function(parser, reporter){
624 | 3 | var self = this;
625 | 3 | parser.addListener('tagstart', function(event){
626 | 3 | if(event.tagName.toLowerCase() === 'img'){
627 | 3 | var attrs = event.attrs;
628 | 3 | var haveAlt = false;
629 | 3 | for(var i=0, l=attrs.length;i<l;i++){
630 | 8 | if(attrs[i].name.toLowerCase() === 'alt'){
631 | 2 | haveAlt = true;
632 | 2 | break;
633 |  | }
634 |  | }
635 | 3 | if(haveAlt === false){
636 | 1 | reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
637 |  | }
638 |  | }
639 |  | });
640 |  | }
641 |  | });
642 |  | /**
643 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
644 |  | * MIT Licensed
645 |  | */
646 | 1 | HTMLHint.addRule({
647 |  | id: 'jshint',
648 |  | description: 'Scan script with jshint.',
649 |  | init: function(parser, reporter, options){
650 | 4 | var self = this;
651 | 4 | parser.addListener('cdata', function(event){
652 | 4 | if(event.tagName.toLowerCase() === 'script'){
653 |  |
654 | 4 | var mapAttrs = parser.getMapAttrs(event.attrs),
655 |  | type = mapAttrs.type;
656 |  |
657 |  | // Only scan internal javascript
658 | 4 | if(mapAttrs.src !== undefined || (type && /^(text\/javascript)$/i.test(type) === false)){
659 | 2 | return;
660 |  | }
661 |  |
662 | 2 | var jsVerify;
663 |  |
664 | 2 | if(typeof exports === 'object' && require){
665 | 2 | jsVerify = require('jshint').JSHINT;
666 |  | }
667 |  | else{
668 | 0 | jsVerify = JSHINT;
669 |  | }
670 |  |
671 | 2 | if(options !== undefined){
672 | 2 | var styleLine = event.line - 1,
673 |  | styleCol = event.col - 1;
674 | 2 | var code = event.raw.replace(/\t/g,' ');
675 | 2 | try{
676 | 2 | var status = jsVerify(code, options);
677 | 2 | if(status === false){
678 | 2 | jsVerify.errors.forEach(function(error){
679 | 8 | var line = error.line;
680 | 8 | reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
681 |  | });
682 |  | }
683 |  | }
684 |  | catch(e){}
685 |  | }
686 |  |
687 |  | }
688 |  | });
689 |  | }
690 |  | });
691 |  | /**
692 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
693 |  | * MIT Licensed
694 |  | */
695 | 1 | HTMLHint.addRule({
696 |  | id: 'spec-char-escape',
697 |  | description: 'Special characters must be escaped.',
698 |  | init: function(parser, reporter){
699 | 3 | var self = this;
700 | 3 | parser.addListener('text', function(event){
701 | 3 | var raw = event.raw,
702 |  | reSpecChar = /[<>]/g,
703 |  | match;
704 | 3 | while((match = reSpecChar.exec(raw))){
705 | 3 | var fixedPos = parser.fixPos(event, match.index);
706 | 3 | reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
707 |  | }
708 |  | });
709 |  | }
710 |  | });
711 |  | /**
712 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
713 |  | * MIT Licensed
714 |  | */
715 | 1 | HTMLHint.addRule({
716 |  | id: 'style-disabled',
717 |  | description: 'Style tag can not be use.',
718 |  | init: function(parser, reporter){
719 | 2 | var self = this;
720 | 2 | parser.addListener('tagstart', function(event){
721 | 4 | if(event.tagName.toLowerCase() === 'style'){
722 | 1 | reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
723 |  | }
724 |  | });
725 |  | }
726 |  | });
727 |  | /**
728 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
729 |  | * MIT Licensed
730 |  | */
731 | 1 | HTMLHint.addRule({
732 |  | id: 'tag-pair',
733 |  | description: 'Tag must be paired.',
734 |  | init: function(parser, reporter){
735 | 4 | var self = this;
736 | 4 | var stack=[],
737 |  | mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
738 | 4 | parser.addListener('tagstart', function(event){
739 | 5 | var tagName = event.tagName.toLowerCase();
740 | 5 | if (mapEmptyTags[tagName] === undefined && !event.close){
741 | 5 | stack.push(tagName);
742 |  | }
743 |  | });
744 | 4 | parser.addListener('tagend', function(event){
745 | 3 | var tagName = event.tagName.toLowerCase();
746 |  | //向上寻找匹配的开始标签
747 | 3 | for(var pos = stack.length-1;pos >= 0; pos--){
748 | 3 | if(stack[pos] === tagName){
749 | 2 | break;
750 |  | }
751 |  | }
752 | 3 | if(pos >= 0){
753 | 2 | var arrTags = [];
754 | 2 | for(var i=stack.length-1;i>pos;i--){
755 | 1 | arrTags.push('</'+stack[i]+'>');
756 |  | }
757 | 2 | if(arrTags.length > 0){
758 | 1 | reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
759 |  | }
760 | 2 | stack.length=pos;
761 |  | }
762 |  | else{
763 | 1 | reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
764 |  | }
765 |  | });
766 | 4 | parser.addListener('end', function(event){
767 | 4 | var arrTags = [];
768 | 4 | for(var i=stack.length-1;i>=0;i--){
769 | 2 | arrTags.push('</'+stack[i]+'>');
770 |  | }
771 | 4 | if(arrTags.length > 0){
772 | 2 | reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
773 |  | }
774 |  | });
775 |  | }
776 |  | });
777 |  | /**
778 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
779 |  | * MIT Licensed
780 |  | */
781 | 1 | HTMLHint.addRule({
782 |  | id: 'tag-self-close',
783 |  | description: 'The empty tag must closed by self.',
784 |  | init: function(parser, reporter){
785 | 2 | var self = this;
786 | 2 | var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
787 | 2 | parser.addListener('tagstart', function(event){
788 | 4 | var tagName = event.tagName.toLowerCase();
789 | 4 | if(mapEmptyTags[tagName] !== undefined){
790 | 4 | if(!event.close){
791 | 2 | reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
792 |  | }
793 |  | }
794 |  | });
795 |  | }
796 |  | });
797 |  | /**
798 |  | * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
799 |  | * MIT Licensed
800 |  | */
801 | 1 | HTMLHint.addRule({
802 |  | id: 'tagname-lowercase',
803 |  | description: 'Tagname must be lowercase.',
804 |  | init: function(parser, reporter){
805 | 3 | var self = this;
806 | 3 | parser.addListener('tagstart,tagend', function(event){
807 | 9 | var tagName = event.tagName;
808 | 9 | if(tagName !== tagName.toLowerCase()){
809 | 4 | reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
810 |  | }
811 |  | });
812 |  | }
813 |  | });