From d6b5007771b6257acbcef4c65e1136dc0918be48 Mon Sep 17 00:00:00 2001 From: Yanis Wang Date: Sat, 30 Mar 2013 15:31:41 +0800 Subject: [PATCH] change rule: jshint, csslint add test: default rule --- jshint.json => .jshintrc | 4 +++- CHANGE.md | 20 +++++++++++++++--- Gruntfile.js | 2 +- bin/htmlhint.js | 34 +++++++++++++++++++++++++++++++ coverage.html | 2 +- lib/htmlhint.js | 2 +- package.json | 13 ++++++++---- src/rules/csslint.js | 14 +++++++++---- src/rules/jshint.js | 14 +++++++++---- test/htmlparser.spec.js | 4 ++-- test/rules/csslint.js | 9 ++------ test/rules/default.spec.js | 18 ++++++++++++++++ test/rules/id-class-value.spec.js | 24 ++++++++++++++++++++++ test/rules/jshint.js | 9 ++------ 14 files changed, 134 insertions(+), 35 deletions(-) rename jshint.json => .jshintrc (88%) create mode 100644 bin/htmlhint.js create mode 100644 test/rules/default.spec.js diff --git a/jshint.json b/.jshintrc similarity index 88% rename from jshint.json rename to .jshintrc index 83108e357..95e34101e 100644 --- a/jshint.json +++ b/.jshintrc @@ -23,6 +23,8 @@ "process", "describe", "it", - "console" + "console", + "JSHINT", + "CSSLint" ] } \ No newline at end of file diff --git a/CHANGE.md b/CHANGE.md index 16459012a..c9c1b451e 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -5,8 +5,8 @@ HTMLHint change log add: -1. add csslint rule -2. add jshint rule +1. add rule: csslint +2. add rule: jshint fix: @@ -19,4 +19,18 @@ fix: ## ver 0.9.1 (2013-3-23) -first version \ No newline at end of file +add: + +1. add rule: attr-lowercase +2. add rule: attr-value-double-quotes +3. add rule: attr-value-not-empty +4. add rule: doctype-first +5. add rule: doctype-html5 +6. add rule: head-script-disabled +7. add rule: id-class-value +8. add rule: img-alt-require +9. add rule: spec-char-escape +10. add rule: style-disabled +11. add rule: tagname-lowercase +12. add rule: tag-pair +13. add rule: tag-self-close \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 3cfda7c1f..3aa367405 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -21,7 +21,7 @@ module.exports = function(grunt) { all: { src: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], options: { - jshintrc: "jshint.json" + jshintrc: ".jshintrc" } } }, diff --git a/bin/htmlhint.js b/bin/htmlhint.js new file mode 100644 index 000000000..7f97762bf --- /dev/null +++ b/bin/htmlhint.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +var program = require('commander'); + +function map(val) { + var objMap = {}; + val.split(',').forEach(function(item){ + var arrItem = item.split(/\s*=\s*/); + objMap[arrItem[0]] = arrItem[1]?arrItem[1]:true; + }); + return objMap; +} + +program.on('--help', function(){ + console.log(' Examples:'); + console.log(''); + console.log(' htmlhint -l'); + console.log(' htmlhint -r tag-pair,id-class-value=underline test.html'); + console.log(' htmlhint -c .htmlhintrc test.html'); + console.log(''); +}); + +program + .version('0.9.2') + .usage('[options] ') + .option('-l, --list', 'show all of the rules available.') + .option('-c, --config ', 'custom configuration file.') + .option('-r, --rules ', 'set all of the rules available.', map) + .parse(process.argv); + +console.log(' list: %j', program.list); +console.log(' config: %j', program.config); +console.log(' rules: %j', program.rules); +console.log(' args: %j', program.args); \ No newline at end of file diff --git a/coverage.html b/coverage.html index 8aae09db4..ee35a81fc 100644 --- a/coverage.html +++ b/coverage.html @@ -338,4 +338,4 @@ code .string { color: #5890AD } code .keyword { color: #8A6343 } code .number { color: #2F6FAD } -

Coverage

98%
305
300
5

htmlhint.js

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

Coverage

98%
311
307
4

htmlhint.js

98%
311
307
4
LineHitsSource
1/**
2 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
3 * MIT Licensed
4 */
51var HTMLHint = (function (undefined) {
6
71 var rules = [];
8
91 var HTMLHint = {};
10
111 HTMLHint.version = '@VERSION';
12
13 //默认配置
141 HTMLHint.defaultRuleset = {
15 'tagname-lowercase': true,
16 'attr-lowercase': true,
17 'attr-value-double-quotes': true,
18 'doctype-first': true,
19 'tag-pair': true,
20 'spec-char-escape': true
21 };
22
231 HTMLHint.addRule = function(rule){
2415 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2840 if(ruleset === undefined){
291 ruleset = HTMLHint.defaultRuleset;
30 }
3140 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3440 var rule;
3540 for (var id in ruleset){
3645 rule = rules[id];
3745 if (rule !== undefined){
3845 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4240 parser.parse(html);
43
4440 return reporter.messages;
45 };
46
471 return HTMLHint;
48
49})();
50
511if (typeof exports === 'object' && exports){
521 exports.HTMLHint = HTMLHint;
53}
54/**
55 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
56 * MIT Licensed
57 */
581(function(HTMLHint, undefined){
59
601 var Reporter = function(){
6140 var self = this;
6240 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6740 var self = this;
6840 self.lines = lines;
6940 self.ruleset = ruleset;
7040 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7417 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7822 this.report('warning', message, line, col, rule, raw);
79 },
80 //信息
81 info: function(message, line, col, rule, raw){
820 this.report('info', message, line, col, rule, raw);
83 },
84 //报告
85 report: function(type, message, line, col, rule, raw){
8639 var self = this;
8739 self.messages.push({
88 type: type,
89 message: message,
90 raw: raw,
91 evidence: self.lines[line-1],
92 line: line,
93 col: col,
94 rule: rule
95 });
96 }
97 };
98
991 HTMLHint.Reporter = Reporter;
100
101})(HTMLHint);
102/**
103 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
104 * MIT Licensed
105 */
1061var HTMLParser = (function(undefined){
107
1081 var HTMLParser = function(){
10962 var self = this;
11062 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11562 var self = this;
11662 self._listeners = {};
11762 self._mapCdataTags = self.makeMap("script,style");
11862 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12268 var obj = {}, items = str.split(",");
12368 for ( var i = 0; i < items.length; i++ ){
124208 obj[ items[i] ] = true;
125 }
12668 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13262 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13562 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13962 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, arrCDATA, lastCDATAIndex = 0, text;
14062 var lastLineIndex = 0, line = 1;
14162 var arrBlocks = self._arrBlocks;
142
14362 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14962 while((match = regTag.exec(html))){
150116 matchIndex = match.index;
151116 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15219 text = html.substring(lastIndex, matchIndex);
15319 if(tagCDATA){
1547 arrCDATA.push(text);
155 }
156 else{//文本
15712 saveBlock('text', text, lastIndex);
158 }
159 }
160116 lastIndex = regTag.lastIndex;
161
162116 if((tagName = match[1])){
16336 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
1648 text = arrCDATA.join('');
1658 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA
167 });
1688 tagCDATA = null;
1698 arrCDATA = null;
170 }
17136 if(!tagCDATA){
172 //标签结束
17335 saveBlock('tagend', match[0], matchIndex, {
174 'tagName': tagName
175 });
17635 continue;
177 }
178 }
179
18081 if(tagCDATA){
1811 arrCDATA.push(match[0]);
182 }
183 else{
18480 if((tagName = match[4])){//标签开始
18567 arrAttrs = [];
18667 var attrs = match[5],
187 attrMatch,
188 attrMatchCount = 0;
18967 while((attrMatch = regAttr.exec(attrs))){
19053 var name = attrMatch[1],
191 quote = attrMatch[2] ? attrMatch[2] :
192 attrMatch[4] ? attrMatch[4] : '',
193 value = attrMatch[3] ? attrMatch[3] :
194 attrMatch[5] ? attrMatch[5] :
195 attrMatch[6] ? attrMatch[6] : '';
19653 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
19753 attrMatchCount += attrMatch[0].length;
198 }
19967 if(attrMatchCount === attrs.length){
20066 saveBlock('tagstart', match[0], matchIndex, {
201 'tagName': tagName,
202 'attrs': arrAttrs,
203 'close': match[6]
204 });
20566 if(mapCdataTags[tagName]){
2068 tagCDATA = tagName;
2078 arrCDATA = [];
2088 lastCDATAIndex = lastIndex;
209 }
210 }
211 else{//如果出现漏匹配,则把当前内容匹配为text
2121 saveBlock('text', match[0], matchIndex);
213 }
214 }
21513 else if(match[2] || match[3]){//注释标签
21613 saveBlock('comment', match[0], matchIndex, {
217 'content': match[2] || match[3],
218 'long': match[2]?true:false
219 });
220 }
221 }
222 }
223
22462 if(html.length > lastIndex){
225 //结尾文本
2266 text = html.substring(lastIndex, html.length);
2276 saveBlock('text', text, lastIndex);
228 }
229
23062 self.fire('end', {
231 pos: lastIndex,
232 line: line,
233 col: lastIndex - lastLineIndex + 1
234 });
235
236 //存储区块
23762 function saveBlock(type, raw, pos, data){
238141 var col = pos - lastLineIndex + 1;
239141 if(data === undefined){
24019 data = {};
241 }
242141 data.raw = raw;
243141 data.pos = pos;
244141 data.line = line;
245141 data.col = col;
246141 arrBlocks.push(data);
247141 self.fire(type, data);
248141 var lineMatch;
249141 while((lineMatch = regLine.exec(raw))){
2509 line ++;
2519 lastLineIndex = pos + regLine.lastIndex;
252 }
253 }
254
255 },
256
257 // add event
258 addListener: function(types, listener){
25980 var _listeners = this._listeners;
26080 var arrTypes = types.split(/[,\s]/), type;
26180 for(var i=0, l = arrTypes.length;i<l;i++){
26283 type = arrTypes[i];
26383 if (_listeners[type] === undefined){
26479 _listeners[type] = [];
265 }
26683 _listeners[type].push(listener);
267 }
268 },
269
270 // fire event
271 fire: function(type, data){
272265 if (data === undefined){
2730 data = {};
274 }
275265 data.type = type;
276265 var self = this,
277 listeners = [],
278 listenersType = self._listeners[type],
279 listenersAll = self._listeners['all'];
280265 if (listenersType !== undefined){
28168 listeners = listeners.concat(listenersType);
282 }
283265 if (listenersAll !== undefined){
284112 listeners = listeners.concat(listenersAll);
285 }
286265 for (var i = 0, l = listeners.length; i < l; i++){
287176 listeners[i].call(self, data);
288 }
289 },
290
291 // remove event
292 removeListener: function(type, listener){
29313 var listenersType = this._listeners[type];
29413 if(listenersType !== undefined){
29511 for (var i = 0, l = listenersType.length; i < l; i++){
2968 if (listenersType[i] === listener){
2978 listenersType.splice(i, 1);
2988 break;
299 }
300 }
301 }
302 },
303
304 //fix pos if event.raw have \n
305 fixPos: function(event, index){
3063 var text = event.raw.substr(0, index);
3073 var arrLines = text.split(/\r?\n/),
308 lineCount = arrLines.length - 1,
309 line = event.line, col;
3103 if(lineCount > 0){
3111 line += lineCount;
3121 col = arrLines[lineCount].length + 1;
313 }
314 else{
3152 col = event.col + index;
316 }
3173 return {
318 line: line,
319 col: col
320 };
321
322 }
323 };
324
3251 return HTMLParser;
326
327})();
328
3291if (typeof exports === 'object' && exports){
3301 exports.HTMLParser = HTMLParser;
331}
332/**
333 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
334 * MIT Licensed
335 */
3361HTMLHint.addRule({
337 id: 'attr-lowercase',
338 description: 'Attribute name must be lowercase.',
339 init: function(parser, reporter){
3403 var self = this;
3413 parser.addListener('tagstart', function(event){
3423 var attrs = event.attrs,
343 attr,
344 col = event.col + event.tagName.length + 1;
3453 for(var i=0, l=attrs.length;i<l;i++){
3463 attr = attrs[i];
3473 var attrName = attr.name;
3483 if(attrName !== attrName.toLowerCase()){
3492 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
350 }
351 }
352 });
353 }
354});
355/**
356 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
357 * MIT Licensed
358 */
3591HTMLHint.addRule({
360 id: 'attr-value-double-quotes',
361 description: 'Attribute value must closed by double quotes.',
362 init: function(parser, reporter){
3634 var self = this;
3644 parser.addListener('tagstart', function(event){
3654 var attrs = event.attrs,
366 attr,
367 col = event.col + event.tagName.length + 1;
3684 for(var i=0, l=attrs.length;i<l;i++){
3696 attr = attrs[i];
3706 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3712 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
372 }
373 }
374 });
375 }
376});
377/**
378 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
379 * MIT Licensed
380 */
3811HTMLHint.addRule({
382 id: 'attr-value-not-empty',
383 description: 'Attribute must set value.',
384 init: function(parser, reporter){
3853 var self = this;
3863 parser.addListener('tagstart', function(event){
3873 var attrs = event.attrs,
388 attr,
389 col = event.col + event.tagName.length + 1;
3903 for(var i=0, l=attrs.length;i<l;i++){
3913 attr = attrs[i];
3923 if(attr.quote === '' && attr.value === ''){
3931 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
394 }
395 }
396 });
397 }
398});
399/**
400 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
401 * MIT Licensed
402 */
4031HTMLHint.addRule({
404 id: 'csslint',
405 description: 'Scan css with csslint.',
406 init: function(parser, reporter, options){
4071 var self = this;
4081 parser.addListener('cdata', function(event){
4091 if(event.tagName.toLowerCase() === 'style'){
410
4111 var cssVerify;
412
4131 if(typeof exports === 'object' && require){
4141 cssVerify = require("csslint").CSSLint.verify;
415 }
416 else{
4170 cssVerify = CSSLint.verify;
418 }
419
4201 if(options !== undefined){
4211 var styleLine = event.line - 1,
422 styleCol = event.col - 1;
4231 var messages = cssVerify(event.raw, options).messages;
4241 messages.forEach(function(error){
4252 var line = error.line;
4262 reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence);
427 });
428 }
429
430 }
431 });
432 }
433});
434/**
435 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
436 * MIT Licensed
437 */
4381HTMLHint.addRule({
439 id: 'doctype-first',
440 description: 'Doctype must be first.',
441 init: function(parser, reporter){
4423 var self = this;
4433 var allEvent = function(event){
4446 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4453 return;
446 }
4473 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4482 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
449 }
4503 parser.removeListener('all', allEvent);
451 };
4523 parser.addListener('all', allEvent);
453 }
454});
455/**
456 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
457 * MIT Licensed
458 */
4591HTMLHint.addRule({
460 id: 'doctype-html5',
461 description: 'Doctype must be html5.',
462 init: function(parser, reporter){
4632 var self = this;
4642 function onComment(event){
4659 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4661 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
467 }
468 }
4692 function onTagStart(){
4702 parser.removeListener('comment', onComment);
4712 parser.removeListener('tagstart', onTagStart);
472 }
4732 parser.addListener('all', onComment);
4742 parser.addListener('tagstart', onTagStart);
475 }
476});
477/**
478 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
479 * MIT Licensed
480 */
4811HTMLHint.addRule({
482 id: 'head-script-disabled',
483 description: 'The script tag can not be used in head.',
484 init: function(parser, reporter){
4853 var self = this;
4863 function onTagStart(event){
4875 if(event.tagName.toLowerCase() === 'script'){
4882 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
489 }
490 }
4913 function onTagEnd(event){
4927 if(event.tagName.toLowerCase() === 'head'){
4933 parser.removeListener('tagstart', onTagStart);
4943 parser.removeListener('tagstart', onTagEnd);
495 }
496 }
4973 parser.addListener('tagstart', onTagStart);
4983 parser.addListener('tagend', onTagEnd);
499 }
500});
501/**
502 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
503 * MIT Licensed
504 */
5051HTMLHint.addRule({
506 id: 'id-class-value',
507 description: 'Id and class value must meet some rules.',
508 init: function(parser, reporter, options){
5098 var self = this;
5108 var arrRules = {
511 'underline': {
512 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
513 'message': 'Id and class value must lower case and split by underline.'
514 },
515 'dash': {
516 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
517 'message': 'Id and class value must lower case and split by dash.'
518 },
519 'hump': {
520 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
521 'message': 'Id and class value must meet hump style.'
522 }
523 }, rule;
5248 if(typeof options === 'string'){
5256 rule = arrRules[options];
526 }
527 else{
5282 rule = options;
529 }
5308 if(rule && rule.regId){
5318 var regId = rule.regId,
532 message = rule.message;
5338 parser.addListener('tagstart', function(event){
5348 var attrs = event.attrs,
535 attr,
536 col = event.col + event.tagName.length + 1;
5378 for(var i=0, l1=attrs.length;i<l1;i++){
53816 attr = attrs[i];
53916 if(attr.name.toLowerCase() === 'id'){
5408 if(regId.test(attr.value) === false){
5414 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
542 }
543 }
54416 if(attr.name.toLowerCase() === 'class'){
5458 var arrClass = attr.value.split(/\s+/g), classValue;
5468 for(var j=0, l2=arrClass.length;j<l2;j++){
5478 classValue = arrClass[j];
5488 if(classValue && regId.test(classValue) === false){
5494 reporter.warn(message, event.line, col + attr.index, self, classValue);
550 }
551 }
552 }
553 }
554 });
555 }
556 }
557});
558/**
559 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
560 * MIT Licensed
561 */
5621HTMLHint.addRule({
563 id: 'img-alt-require',
564 description: 'Alt of img tag must be set value.',
565 init: function(parser, reporter){
5663 var self = this;
5673 parser.addListener('tagstart', function(event){
5683 if(event.tagName.toLowerCase() === 'img'){
5693 var attrs = event.attrs;
5703 var haveAlt = false;
5713 for(var i=0, l=attrs.length;i<l;i++){
5728 if(attrs[i].name.toLowerCase() === 'alt'){
5732 haveAlt = true;
5742 break;
575 }
576 }
5773 if(haveAlt === false){
5781 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
579 }
580 }
581 });
582 }
583});
584/**
585 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
586 * MIT Licensed
587 */
5881HTMLHint.addRule({
589 id: 'jshint',
590 description: 'Scan script with jshint.',
591 init: function(parser, reporter, options){
5921 var self = this;
5931 parser.addListener('cdata', function(event){
5941 if(event.tagName.toLowerCase() === 'script'){
595
5961 var jsVerify;
597
5981 if(typeof exports === 'object' && require){
5991 jsVerify = require('jshint').JSHINT;
600 }
601 else{
6020 jsVerify = JSHINT;
603 }
604
6051 if(options !== undefined){
6061 var styleLine = event.line - 1,
607 styleCol = event.col - 1;
6081 var code = event.raw.replace(/\t/g,' ');
6091 var status = jsVerify(code, options);
6101 if(status === false){
6111 jsVerify.errors.forEach(function(error){
6124 var line = error.line;
6134 reporter.warn(error.reason, styleLine + line, (line === 1 ? styleCol : 0) + error.character, self, error.evidence);
614 });
615 }
616 }
617
618 }
619 });
620 }
621});
622/**
623 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
624 * MIT Licensed
625 */
6261HTMLHint.addRule({
627 id: 'spec-char-escape',
628 description: 'Special characters must be escaped.',
629 init: function(parser, reporter){
6303 var self = this;
6313 parser.addListener('text', function(event){
6323 var raw = event.raw,
633 reSpecChar = /[<>]/g,
634 match;
6353 while((match = reSpecChar.exec(raw))){
6363 var fixedPos = parser.fixPos(event, match.index);
6373 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
638 }
639 });
640 }
641});
642/**
643 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
644 * MIT Licensed
645 */
6461HTMLHint.addRule({
647 id: 'style-disabled',
648 description: 'Style tag can not be use.',
649 init: function(parser, reporter){
6502 var self = this;
6512 parser.addListener('tagstart', function(event){
6524 if(event.tagName.toLowerCase() === 'style'){
6531 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
654 }
655 });
656 }
657});
658/**
659 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
660 * MIT Licensed
661 */
6621HTMLHint.addRule({
663 id: 'tag-pair',
664 description: 'Tag must be paired.',
665 init: function(parser, reporter){
6664 var self = this;
6674 var stack=[],
668 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6694 parser.addListener('tagstart', function(event){
6705 var tagName = event.tagName.toLowerCase();
6715 if (mapEmptyTags[tagName] === undefined && !event.close){
6725 stack.push(tagName);
673 }
674 });
6754 parser.addListener('tagend', function(event){
6763 var tagName = event.tagName.toLowerCase();
677 //向上寻找匹配的开始标签
6783 for(var pos = stack.length-1;pos >= 0; pos--){
6793 if(stack[pos] === tagName){
6802 break;
681 }
682 }
6833 if(pos >= 0){
6842 var arrTags = [];
6852 for(var i=stack.length-1;i>pos;i--){
6861 arrTags.push('</'+stack[i]+'>');
687 }
6882 if(arrTags.length > 0){
6891 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
690 }
6912 stack.length=pos;
692 }
693 else{
6941 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
695 }
696 });
6974 parser.addListener('end', function(event){
6984 var arrTags = [];
6994 for(var i=stack.length-1;i>=0;i--){
7002 arrTags.push('</'+stack[i]+'>');
701 }
7024 if(arrTags.length > 0){
7032 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
704 }
705 });
706 }
707});
708/**
709 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
710 * MIT Licensed
711 */
7121HTMLHint.addRule({
713 id: 'tag-self-close',
714 description: 'The empty tag must closed by self.',
715 init: function(parser, reporter){
7162 var self = this;
7172 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
7182 parser.addListener('tagstart', function(event){
7194 var tagName = event.tagName.toLowerCase();
7204 if(mapEmptyTags[tagName] !== undefined){
7214 if(!event.close){
7222 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
723 }
724 }
725 });
726 }
727});
728/**
729 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
730 * MIT Licensed
731 */
7321HTMLHint.addRule({
733 id: 'tagname-lowercase',
734 description: 'Tagname must be lowercase.',
735 init: function(parser, reporter){
7363 var self = this;
7373 parser.addListener('tagstart,tagend', function(event){
7389 var tagName = event.tagName;
7399 if(tagName !== tagName.toLowerCase()){
7404 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
741 }
742 });
743 }
744});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 4d3f9ca37..143ed5e47 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -5,4 +5,4 @@ * (c) 2013 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(e){var t=[],a={};return a.version="0.9.2",a.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0},a.addRule=function(e){t[e.id]=e},a.verify=function(n,r){r===e&&(r=a.defaultRuleset);var i,s=new HTMLParser,o=new a.Reporter(n.split(/\r?\n/),r);for(var l in r)i=t[l],i!==e&&i.init(s,o,r[l]);return s.parse(n),o.messages},a}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,n,r){this.report("error",e,t,a,n,r)},warn:function(e,t,a,n,r){this.report("warning",e,t,a,n,r)},info:function(e,t,a,n,r){this.report("info",e,t,a,n,r)},report:function(e,t,a,n,r,i){var s=this;s.messages.push({type:e,message:t,raw:i,evidence:s.lines[a-1],line:a,col:n,rule:r})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,r){var i=n-h+1;r===e&&(r={}),r.raw=a,r.pos=n,r.line=w,r.col=i,L.push(r),c.fire(t,r);for(var s;s=p.exec(a);)w++,h=n+p.lastIndex}var n,r,i,s,o,l,d,c=this,u=c._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,f=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,p=/\r?\n/g,m=0,v=0,h=0,w=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(r=n.index,r>m&&(d=t.substring(m,r),o?l.push(d):a("text",d,m)),m=g.lastIndex,!(i=n[1])||(o&&i===o&&(d=l.join(""),a("cdata",d,v,{tagName:o}),o=null,l=null),o))if(o)l.push(n[0]);else if(i=n[4]){s=[];for(var b,H=n[5],y=0;b=f.exec(H);){var T=b[1],x=b[2]?b[2]:b[4]?b[4]:"",M=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:T,value:M,quote:x,index:b.index,raw:b[0]}),y+=b[0].length}y===H.length?(a("tagstart",n[0],r,{tagName:i,attrs:s,close:n[6]}),u[i]&&(o=i,l=[],v=m)):a("text",n[0],r)}else(n[2]||n[3])&&a("comment",n[0],r,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],r,{tagName:i});t.length>m&&(d=t.substring(m,t.length),a("text",d,m)),c.fire("end",{pos:m,line:w,col:m-h+1})},addListener:function(t,a){for(var n,r=this._listeners,i=t.split(/[,\s]/),s=0,o=i.length;o>s;s++)n=i[s],r[n]===e&&(r[n]=[]),r[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,r=[],i=n._listeners[t],s=n._listeners.all;i!==e&&(r=r.concat(i)),s!==e&&(r=r.concat(s));for(var o=0,l=r.length;l>o;o++)r[o].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var r=0,i=n.length;i>r;r++)if(n[r]===a){n.splice(r,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),r=n.split(/\r?\n/),i=r.length-1,s=e.line;return i>0?(s+=i,a=r[i].length+1):a=e.col+t,{line:s,col:a}}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++){n=r[s];var l=n.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,i+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],'"'===n.quote||""===n.value&&""!==(n.value===n.quote)||t.error("The value of attribute [ "+n.name+" ] must closed by double quotes.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,r=e.attrs,i=e.col+e.tagName.length+1,s=0,o=r.length;o>s;s++)n=r[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must set value.",e.line,i+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=r(e.raw,i).messages;l.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,s+a,(1===a?o:0)+e.col,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,n=function(r){"start"===r.type||"text"===r.type&&/^\s*$/.test(r.raw)||(("comment"!==r.type&&r.long===!1||/^DOCTYPE\s+/i.test(r.content)===!1)&&t.error("Doctype must be first.",r.line,r.col,a,r.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,r,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var r=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,r,e.raw)}function n(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",n))}var r=this;e.addListener("tagstart",a),e.addListener("tagend",n)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var n,r=this,i={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(n="string"==typeof a?i[a]:a,n&&n.regId){var s=n.regId,o=n.message;e.addListener("tagstart",function(e){for(var a,n=e.attrs,i=e.col+e.tagName.length+1,l=0,d=n.length;d>l;l++)if(a=n[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,i+a.index,r,a.raw),"class"===a.name.toLowerCase())for(var c,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)c=u[g],c&&s.test(c)===!1&&t.warn(o,e.line,i+a.index,r,c)})}}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var n=e.attrs,r=!1,i=0,s=n.length;s>i;i++)if("alt"===n[i].name.toLowerCase()){r=!0;break}r===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var r=a.verify,i=a.options;if(void 0!==r&&void 0!==i){var s=e.line-1,o=e.col-1,l=e.raw.replace(/\t/g," "),d=r(l,i);d===!1&&r.errors.forEach(function(e){var a=e.line;t.warn(e.reason,s+a,(1===a?o:0)+e.character,n,e.evidence)})}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(n){for(var r,i=n.raw,s=/[<>]/g;r=s.exec(i);){var o=e.fixPos(n,r.index);t.error("Special characters must be escaped : [ "+r[0]+" ].",o.line,o.col,a,n.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,n=[],r=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var t=e.tagName.toLowerCase();void 0!==r[t]||e.close||n.push(t)}),e.addListener("tagend",function(e){for(var r=e.tagName.toLowerCase(),i=n.length-1;i>=0&&n[i]!==r;i--);if(i>=0){for(var s=[],o=n.length-1;o>i;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),n.length=i}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var r=[],i=n.length-1;i>=0;i--)r.push("");r.length>0&&t.error("Tag must be paired, Missing: [ "+r.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,n=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var r=e.tagName.toLowerCase();void 0!==n[r]&&(e.close||t.warn("The empty tag : [ "+r+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var n=e.tagName;n!==n.toLowerCase()&&t.error("Tagname [ "+n+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file +var HTMLHint=function(e){var t=[],a={};return a.version="0.9.2",a.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0},a.addRule=function(e){t[e.id]=e},a.verify=function(r,n){n===e&&(n=a.defaultRuleset);var i,s=new HTMLParser,o=new a.Reporter(r.split(/\r?\n/),n);for(var l in n)i=t[l],i!==e&&i.init(s,o,n[l]);return s.parse(r),o.messages},a}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,r,n){this.report("error",e,t,a,r,n)},warn:function(e,t,a,r,n){this.report("warning",e,t,a,r,n)},info:function(e,t,a,r,n){this.report("info",e,t,a,r,n)},report:function(e,t,a,r,n,i){var s=this;s.messages.push({type:e,message:t,raw:i,evidence:s.lines[a-1],line:a,col:r,rule:n})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[]},makeMap:function(e){for(var t={},a=e.split(","),r=0;a.length>r;r++)t[a[r]]=!0;return t},parse:function(t){function a(t,a,r,n){var i=r-h+1;n===e&&(n={}),n.raw=a,n.pos=r,n.line=w,n.col=i,L.push(n),d.fire(t,n);for(var s;s=p.exec(a);)w++,h=r+p.lastIndex}var r,n,i,s,o,l,c,d=this,u=d._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,f=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,p=/\r?\n/g,m=0,v=0,h=0,w=1,L=d._arrBlocks;for(d.fire("start",{pos:0,line:1,col:1});r=g.exec(t);)if(n=r.index,n>m&&(c=t.substring(m,n),o?l.push(c):a("text",c,m)),m=g.lastIndex,!(i=r[1])||(o&&i===o&&(c=l.join(""),a("cdata",c,v,{tagName:o}),o=null,l=null),o))if(o)l.push(r[0]);else if(i=r[4]){s=[];for(var b,H=r[5],y=0;b=f.exec(H);){var T=b[1],x=b[2]?b[2]:b[4]?b[4]:"",M=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:T,value:M,quote:x,index:b.index,raw:b[0]}),y+=b[0].length}y===H.length?(a("tagstart",r[0],n,{tagName:i,attrs:s,close:r[6]}),u[i]&&(o=i,l=[],v=m)):a("text",r[0],n)}else(r[2]||r[3])&&a("comment",r[0],n,{content:r[2]||r[3],"long":r[2]?!0:!1});else a("tagend",r[0],n,{tagName:i});t.length>m&&(c=t.substring(m,t.length),a("text",c,m)),d.fire("end",{pos:m,line:w,col:m-h+1})},addListener:function(t,a){for(var r,n=this._listeners,i=t.split(/[,\s]/),s=0,o=i.length;o>s;s++)r=i[s],n[r]===e&&(n[r]=[]),n[r].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var r=this,n=[],i=r._listeners[t],s=r._listeners.all;i!==e&&(n=n.concat(i)),s!==e&&(n=n.concat(s));for(var o=0,l=n.length;l>o;o++)n[o].call(r,a)},removeListener:function(t,a){var r=this._listeners[t];if(r!==e)for(var n=0,i=r.length;i>n;n++)if(r[n]===a){r.splice(n,1);break}},fixPos:function(e,t){var a,r=e.raw.substr(0,t),n=r.split(/\r?\n/),i=n.length-1,s=e.line;return i>0?(s+=i,a=n[i].length+1):a=e.col+t,{line:s,col:a}}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"attr-lowercase",description:"Attribute name must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++){r=n[s];var l=r.name;l!==l.toLowerCase()&&t.error("Attribute name [ "+l+" ] must be lower case.",e.line,i+r.index,a,r.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute value must closed by double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++)r=n[s],'"'===r.quote||""===r.value&&""!==(r.value===r.quote)||t.error("The value of attribute [ "+r.name+" ] must closed by double quotes.",e.line,i+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"Attribute must set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var r,n=e.attrs,i=e.col+e.tagName.length+1,s=0,o=n.length;o>s;s++)r=n[s],""===r.quote&&""===r.value&&t.warn("The attribute [ "+r.name+" ] must set value.",e.line,i+r.index,a,r.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var n;if(n="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var i=e.line-1,s=e.col-1,o=n(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"](e.message,i+a,(1===a?s:0)+e.col,r,e.evidence)})}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be first.",init:function(e,t){var a=this,r=function(n){"start"===n.type||"text"===n.type&&/^\s*$/.test(n.raw)||(("comment"!==n.type&&n.long===!1||/^DOCTYPE\s+/i.test(n.content)===!1)&&t.error("Doctype must be first.",n.line,n.col,a,n.raw),e.removeListener("all",r))};e.addListener("all",r)}}),HTMLHint.addRule({id:"doctype-html5",description:"Doctype must be html5.",init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn("Doctype must be html5.",e.line,e.col,n,e.raw)}function r(){e.removeListener("comment",a),e.removeListener("tagstart",r)}var n=this;e.addListener("all",a),e.addListener("tagstart",r)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The script tag can not be used in head.",init:function(e,t){function a(e){"script"===e.tagName.toLowerCase()&&t.warn("The script tag can not be used in head.",e.line,e.col,n,e.raw)}function r(t){"head"===t.tagName.toLowerCase()&&(e.removeListener("tagstart",a),e.removeListener("tagstart",r))}var n=this;e.addListener("tagstart",a),e.addListener("tagend",r)}}),HTMLHint.addRule({id:"id-class-value",description:"Id and class value must meet some rules.",init:function(e,t,a){var r,n=this,i={underline:{regId:/^[a-z\d]+(_[a-z\d]+)*$/,message:"Id and class value must lower case and split by underline."},dash:{regId:/^[a-z\d]+(-[a-z\d]+)*$/,message:"Id and class value must lower case and split by dash."},hump:{regId:/^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,message:"Id and class value must meet hump style."}};if(r="string"==typeof a?i[a]:a,r&&r.regId){var s=r.regId,o=r.message;e.addListener("tagstart",function(e){for(var a,r=e.attrs,i=e.col+e.tagName.length+1,l=0,c=r.length;c>l;l++)if(a=r[l],"id"===a.name.toLowerCase()&&s.test(a.value)===!1&&t.warn(o,e.line,i+a.index,n,a.raw),"class"===a.name.toLowerCase())for(var d,u=a.value.split(/\s+/g),g=0,f=u.length;f>g;g++)d=u[g],d&&s.test(d)===!1&&t.warn(o,e.line,i+a.index,n,d)})}}}),HTMLHint.addRule({id:"img-alt-require",description:"Alt of img tag must be set value.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){if("img"===e.tagName.toLowerCase()){for(var r=e.attrs,n=!1,i=0,s=r.length;s>i;i++)if("alt"===r[i].name.toLowerCase()){n=!0;break}n===!1&&t.warn("Alt of img tag must be set value.",e.line,e.col,a,e.raw)}})}}),HTMLHint.addRule({id:"jshint",description:"Scan script with jshint.",init:function(e,t,a){var r=this;e.addListener("cdata",function(e){if("script"===e.tagName.toLowerCase()){var n;if(n="object"==typeof exports&&require?require("jshint").JSHINT:JSHINT,void 0!==a){var i=e.line-1,s=e.col-1,o=e.raw.replace(/\t/g," "),l=n(o,a);l===!1&&n.errors.forEach(function(e){var a=e.line;t.warn(e.reason,i+a,(1===a?s:0)+e.character,r,e.evidence)})}}})}}),HTMLHint.addRule({id:"spec-char-escape",description:"Special characters must be escaped.",init:function(e,t){var a=this;e.addListener("text",function(r){for(var n,i=r.raw,s=/[<>]/g;n=s.exec(i);){var o=e.fixPos(r,n.index);t.error("Special characters must be escaped : [ "+n[0]+" ].",o.line,o.col,a,r.raw)}})}}),HTMLHint.addRule({id:"style-disabled",description:"Style tag can not be use.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){"style"===e.tagName.toLowerCase()&&t.warn("Style tag can not be use.",e.line,e.col,a,e.raw)})}}),HTMLHint.addRule({id:"tag-pair",description:"Tag must be paired.",init:function(e,t){var a=this,r=[],n=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!==n[t]||e.close||r.push(t)}),e.addListener("tagend",function(e){for(var n=e.tagName.toLowerCase(),i=r.length-1;i>=0&&r[i]!==n;i--);if(i>=0){for(var s=[],o=r.length-1;o>i;o--)s.push("");s.length>0&&t.error("Tag must be paired, Missing: [ "+s.join("")+" ]",e.line,e.col,a,e.raw),r.length=i}else t.error("Tag must be paired, No start tag: [ "+e.raw+" ]",e.line,e.col,a,e.raw)}),e.addListener("end",function(e){for(var n=[],i=r.length-1;i>=0;i--)n.push("");n.length>0&&t.error("Tag must be paired, Missing: [ "+n.join("")+" ]",e.line,e.col,a,"")})}}),HTMLHint.addRule({id:"tag-self-close",description:"The empty tag must closed by self.",init:function(e,t){var a=this,r=e.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");e.addListener("tagstart",function(e){var n=e.tagName.toLowerCase();void 0!==r[n]&&(e.close||t.warn("The empty tag : [ "+n+" ] must closed by self.",e.line,e.col,a,e.raw))})}}),HTMLHint.addRule({id:"tagname-lowercase",description:"Tagname must be lowercase.",init:function(e,t){var a=this;e.addListener("tagstart,tagend",function(e){var r=e.tagName;r!==r.toLowerCase()&&t.error("Tagname [ "+r+" ] must be lower case.",e.line,e.col,a,e.raw)})}}); \ No newline at end of file diff --git a/package.json b/package.json index 2517c98b3..2524bb941 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,11 @@ "version": "0.9.2", "description": "A Static Code Analysis Tool for HTML", "main": "./index", - "dependencies": {}, + "dependencies": { + "commander": "~1.1.1", + "jshint": "~1.1.0", + "csslint": "~0.9.10" + }, "devDependencies": { "grunt-cli": "~0.1.6", "grunt": "~0.4.1", @@ -17,9 +21,10 @@ "grunt-exec": "~0.4.0", "mocha": "~1.8.2", "expect.js": "~0.2.0", - "jscover": "~0.2.4", - "jshint": "~1.1.0", - "csslint": "~0.9.10" + "jscover": "~0.2.4" + }, + "bin": { + "htmlhint": "./bin/htmlhint" }, "repository": { "type": "git", diff --git a/src/rules/csslint.js b/src/rules/csslint.js index d0b9bbf63..e686ab6fc 100644 --- a/src/rules/csslint.js +++ b/src/rules/csslint.js @@ -10,13 +10,19 @@ HTMLHint.addRule({ parser.addListener('cdata', function(event){ if(event.tagName.toLowerCase() === 'style'){ - var cssVerify = options.verify, - cssOptions = options.options; + var cssVerify; - if(cssVerify !== undefined && cssOptions !== undefined){ + if(typeof exports === 'object' && require){ + cssVerify = require("csslint").CSSLint.verify; + } + else{ + cssVerify = CSSLint.verify; + } + + if(options !== undefined){ var styleLine = event.line - 1, styleCol = event.col - 1; - var messages = cssVerify(event.raw, cssOptions).messages; + var messages = cssVerify(event.raw, options).messages; messages.forEach(function(error){ var line = error.line; reporter[error.type==='warning'?'warn':'error'](error.message, styleLine + line, (line === 1 ? styleCol : 0) + error.col, self, error.evidence); diff --git a/src/rules/jshint.js b/src/rules/jshint.js index a62396f1e..633be7622 100644 --- a/src/rules/jshint.js +++ b/src/rules/jshint.js @@ -10,14 +10,20 @@ HTMLHint.addRule({ parser.addListener('cdata', function(event){ if(event.tagName.toLowerCase() === 'script'){ - var jsVerify = options.verify, - jsOptions = options.options; + var jsVerify; - if(jsVerify !== undefined && jsOptions !== undefined){ + if(typeof exports === 'object' && require){ + jsVerify = require('jshint').JSHINT; + } + else{ + jsVerify = JSHINT; + } + + if(options !== undefined){ var styleLine = event.line - 1, styleCol = event.col - 1; var code = event.raw.replace(/\t/g,' '); - var status = jsVerify(code, jsOptions); + var status = jsVerify(code, options); if(status === false){ jsVerify.errors.forEach(function(error){ var line = error.line; diff --git a/test/htmlparser.spec.js b/test/htmlparser.spec.js index aa41b23fd..47d97b327 100644 --- a/test/htmlparser.spec.js +++ b/test/htmlparser.spec.js @@ -327,7 +327,7 @@ describe('HTMLParser: Object parse', function(){ expect(arrEvents[2]).to.event('cdata', { tagName: 'script', - raw: 'alert(1);\r\nalert(2);' + raw: 'alert(1);\r\nalert("");' }); expect(arrEvents[3]).to.event('tagend', { @@ -335,7 +335,7 @@ describe('HTMLParser: Object parse', function(){ }); done(); }); - parser.parse(''); + parser.parse(''); }); diff --git a/test/rules/csslint.js b/test/rules/csslint.js index 7f044d62e..2c55de216 100644 --- a/test/rules/csslint.js +++ b/test/rules/csslint.js @@ -5,8 +5,6 @@ var expect = require("expect.js"); -var CSSLint = require("csslint").CSSLint; - var HTMLHint = require("../../index").HTMLHint; describe('Rules: csslint', function(){ @@ -14,11 +12,8 @@ describe('Rules: csslint', function(){ it('should result in an error', function(){ var code = 'ab'; var messages = HTMLHint.verify(code, {'csslint': { - verify: CSSLint.verify, - options:{ - "display-property-grouping": true, - "known-properties": true - } + "display-property-grouping": true, + "known-properties": true }}); expect(messages.length).to.be(2); expect(messages[0].rule.id).to.be('csslint'); diff --git a/test/rules/default.spec.js b/test/rules/default.spec.js new file mode 100644 index 000000000..b9208497c --- /dev/null +++ b/test/rules/default.spec.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2013, Yanis Wang + * MIT Licensed + */ + +var expect = require("expect.js"); + +var HTMLHint = require("../../index").HTMLHint; + +describe('Rules: default', function(){ + + it('should result 3 errors', function(){ + var code = '

'; + var messages = HTMLHint.verify(code); + expect(messages.length).to.be(3); + }); + +}); \ No newline at end of file diff --git a/test/rules/id-class-value.spec.js b/test/rules/id-class-value.spec.js index 59686c0ad..eb06cadc8 100644 --- a/test/rules/id-class-value.spec.js +++ b/test/rules/id-class-value.spec.js @@ -65,4 +65,28 @@ describe('Rules: id-class-value', function(){ expect(messages.length).to.be(0); }); + it('Id and class value be not meet regexp should result in an error', function(){ + var code = '

'; + var messages = HTMLHint.verify(code, {'id-class-value': { + 'regId': /^_[a-z\d]+(-[a-z\d]+)*$/, + 'message': 'Id and class value must meet regexp' + }}); + expect(messages.length).to.be(2); + expect(messages[0].rule.id).to.be('id-class-value'); + expect(messages[0].line).to.be(1); + expect(messages[0].col).to.be(5); + expect(messages[1].rule.id).to.be('id-class-value'); + expect(messages[1].line).to.be(1); + expect(messages[1].col).to.be(16); + }); + + it('Id and class value be meet regexp should not result in an error', function(){ + var code = '
'; + var messages = HTMLHint.verify(code, {'id-class-value': { + 'regId': /^_[a-z\d]+(-[a-z\d]+)*$/, + 'message': 'Id and class value must meet regexp' + }}); + expect(messages.length).to.be(0); + }); + }); \ No newline at end of file diff --git a/test/rules/jshint.js b/test/rules/jshint.js index 66be7397b..8be4e78f6 100644 --- a/test/rules/jshint.js +++ b/test/rules/jshint.js @@ -5,8 +5,6 @@ var expect = require("expect.js"); -var JSHINT = require('jshint').JSHINT; - var HTMLHint = require("../../index").HTMLHint; describe('Rules: jshint', function(){ @@ -14,11 +12,8 @@ describe('Rules: jshint', function(){ it('should result in an error', function(){ var code = 'ab'; var messages = HTMLHint.verify(code, {'jshint': { - verify: JSHINT, - options: { - "undef": true, - "unused": true - } + "undef": true, + "unused": true }}); expect(messages.length).to.be(4); expect(messages[0].rule.id).to.be('jshint');