diff --git a/coverage.html b/coverage.html index fae1af0f0..1e892229e 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%
278
273
5

htmlhint.js

98%
278
273
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){
2413 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2835 if(ruleset === undefined){
290 ruleset = HTMLHint.defaultRuleset;
30 }
3135 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3435 var rule;
3535 for (var id in ruleset){
3635 rule = rules[id];
3735 if (rule !== undefined){
3835 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4235 parser.parse(html);
43
4435 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(){
6135 var self = this;
6235 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6735 var self = this;
6835 self.lines = lines;
6935 self.ruleset = ruleset;
7035 self.messages = [];
71 },
72 //错误
73 error: function(message, line, col, rule, raw){
7413 this.report('error', message, line, col, rule, raw);
75 },
76 //警告
77 warn: function(message, line, col, rule, raw){
7814 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){
8627 var self = this;
8727 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(){
10954 var self = this;
11054 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11554 var self = this;
11654 self._listeners = {};
11754 self._mapCdataTags = self.makeMap("script,style");
11854 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12259 var obj = {}, items = str.split(",");
12359 for ( var i = 0; i < items.length; i++ ){
124178 obj[ items[i] ] = true;
125 }
12659 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13254 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13554 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13954 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, arrCDATA, lastCDATAIndex = 0, text;
14054 var lastLineIndex = 0, line = 1;
14154 var arrBlocks = self._arrBlocks;
142
14354 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14954 while((match = regTag.exec(html))){
150107 matchIndex = match.index;
151107 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15214 text = html.substring(lastIndex, matchIndex);
15314 if(tagCDATA){
1544 arrCDATA.push(text);
155 }
156 else{//文本
15710 saveBlock('text', text, lastIndex);
158 }
159 }
160107 lastIndex = regTag.lastIndex;
161
162107 if((tagName = match[1])){
16333 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
1646 text = arrCDATA.join('');
1656 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA
167 });
1686 tagCDATA = null;
1696 arrCDATA = null;
170 }
17133 if(!tagCDATA){
172 //标签结束
17333 saveBlock('tagend', match[0], matchIndex, {
174 'tagName': tagName
175 });
17633 continue;
177 }
178 }
179
18074 if(tagCDATA){
1810 arrCDATA.push(match[0]);
182 }
183 else{
18474 if((tagName = match[4])){//标签开始
18561 arrAttrs = [];
18661 var attrs = match[5], attrMatch;
18761 while((attrMatch = regAttr.exec(attrs))){
18844 var name = attrMatch[1],
189 quote = attrMatch[2] ? attrMatch[2] :
190 attrMatch[4] ? attrMatch[4] : '',
191 value = attrMatch[3] ? attrMatch[3] :
192 attrMatch[5] ? attrMatch[5] :
193 attrMatch[6] ? attrMatch[6] : '';
19444 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
195 }
19661 saveBlock('tagstart', match[0], matchIndex, {
197 'tagName': tagName,
198 'attrs': arrAttrs,
199 'close': match[6]
200 });
20161 if(mapCdataTags[tagName]){
2026 tagCDATA = tagName;
2036 arrCDATA = [];
2046 lastCDATAIndex = lastIndex;
205 }
206 }
20713 else if(match[2] || match[3]){//注释标签
20813 saveBlock('comment', match[0], matchIndex, {
209 'content': match[2] || match[3],
210 'long': match[2]?true:false
211 });
212 }
213 }
214 }
215
21654 if(html.length > lastIndex){
217 //结尾文本
2182 text = html.substring(lastIndex, html.length);
2192 saveBlock('text', text, lastIndex);
220 }
221
22254 self.fire('end', {
223 pos: lastIndex,
224 line: line,
225 col: lastIndex - lastLineIndex + 1
226 });
227
228 //存储区块
22954 function saveBlock(type, raw, pos, data){
230125 var col = pos - lastLineIndex + 1;
231125 if(data === undefined){
23212 data = {};
233 }
234125 data.raw = raw;
235125 data.pos = pos;
236125 data.line = line;
237125 data.col = col;
238125 arrBlocks.push(data);
239125 self.fire(type, data);
240125 var lineMatch;
241125 while((lineMatch = regLine.exec(raw))){
2424 line ++;
2434 lastLineIndex = pos + regLine.lastIndex;
244 }
245 }
246
247 },
248
249 // add event
250 addListener: function(types, listener){
25165 var _listeners = this._listeners;
25265 var arrTypes = types.split(/[,\s]/), type;
25365 for(var i=0, l = arrTypes.length;i<l;i++){
25467 type = arrTypes[i];
25567 if (_listeners[type] === undefined){
25667 _listeners[type] = [];
257 }
25867 _listeners[type].push(listener);
259 }
260 },
261
262 // fire event
263 fire: function(type, data){
264233 if (data === undefined){
2650 data = {};
266 }
267233 data.type = type;
268233 var self = this,
269 listeners = [],
270 listenersType = self._listeners[type],
271 listenersAll = self._listeners['all'];
272233 if (listenersType !== undefined){
27362 listeners = listeners.concat(listenersType);
274 }
275233 if (listenersAll !== undefined){
276100 listeners = listeners.concat(listenersAll);
277 }
278233 for (var i = 0, l = listeners.length; i < l; i++){
279156 listeners[i].call(self, data);
280 }
281 },
282
283 // remove event
284 removeListener: function(type, listener){
28512 var listenersType = this._listeners[type];
28612 if(listenersType !== undefined){
28710 for (var i = 0, l = listenersType.length; i < l; i++){
2887 if (listenersType[i] === listener){
2897 listenersType.splice(i, 1);
2907 break;
291 }
292 }
293 }
294 },
295
296 //fix pos if event.raw have \n
297 fixPos: function(event, index){
2983 var text = event.raw.substr(0, index);
2993 var arrLines = text.split(/\r?\n/),
300 lineCount = arrLines.length - 1,
301 line = event.line, col;
3023 if(lineCount > 0){
3031 line += lineCount;
3041 col = arrLines[lineCount].length + 1;
305 }
306 else{
3072 col = event.col + index;
308 }
3093 return {
310 line: line,
311 col: col
312 };
313
314 }
315 };
316
3171 return HTMLParser;
318
319})();
320
3211if (typeof exports === 'object' && exports){
3221 exports.HTMLParser = HTMLParser;
323}
324/**
325 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
326 * MIT Licensed
327 */
3281HTMLHint.addRule({
329 id: 'attr-lowercase',
330 description: 'Attribute name must be lowercase.',
331 init: function(parser, reporter){
3322 var self = this;
3332 parser.addListener('tagstart', function(event){
3342 var attrs = event.attrs,
335 attr,
336 col = event.col + event.tagName.length + 1;
3372 for(var i=0, l=attrs.length;i<l;i++){
3382 attr = attrs[i];
3392 var attrName = attr.name;
3402 if(attrName !== attrName.toLowerCase()){
3411 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
342 }
343 }
344 });
345 }
346});
347/**
348 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
349 * MIT Licensed
350 */
3511HTMLHint.addRule({
352 id: 'attr-value-double-quotes',
353 description: 'Attribute value must closed by double quotes.',
354 init: function(parser, reporter){
3553 var self = this;
3563 parser.addListener('tagstart', function(event){
3573 var attrs = event.attrs,
358 attr,
359 col = event.col + event.tagName.length + 1;
3603 for(var i=0, l=attrs.length;i<l;i++){
3613 attr = attrs[i];
3623 if(attr.quote !== '' && attr.quote !== '"'){
3631 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
364 }
365 }
366 });
367 }
368});
369/**
370 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
371 * MIT Licensed
372 */
3731HTMLHint.addRule({
374 id: 'attr-value-not-empty',
375 description: 'Attribute must set value.',
376 init: function(parser, reporter){
3773 var self = this;
3783 parser.addListener('tagstart', function(event){
3793 var attrs = event.attrs,
380 attr,
381 col = event.col + event.tagName.length + 1;
3823 for(var i=0, l=attrs.length;i<l;i++){
3833 attr = attrs[i];
3843 if(attr.quote === '' && attr.value === ''){
3851 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
386 }
387 }
388 });
389 }
390});
391/**
392 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
393 * MIT Licensed
394 */
3951HTMLHint.addRule({
396 id: 'doctype-first',
397 description: 'Doctype must be first.',
398 init: function(parser, reporter){
3992 var self = this;
4002 var allEvent = function(event){
4014 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4022 return;
403 }
4042 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4051 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
406 }
4072 parser.removeListener('all', allEvent);
408 };
4092 parser.addListener('all', allEvent);
410 }
411});
412/**
413 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
414 * MIT Licensed
415 */
4161HTMLHint.addRule({
417 id: 'doctype-html5',
418 description: 'Doctype must be html5.',
419 init: function(parser, reporter){
4202 var self = this;
4212 function onComment(event){
4229 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4231 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
424 }
425 }
4262 function onTagStart(){
4272 parser.removeListener('comment', onComment);
4282 parser.removeListener('tagstart', onTagStart);
429 }
4302 parser.addListener('all', onComment);
4312 parser.addListener('tagstart', onTagStart);
432 }
433});
434/**
435 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
436 * MIT Licensed
437 */
4381HTMLHint.addRule({
439 id: 'head-script-disabled',
440 description: 'The script tag can not be used in head.',
441 init: function(parser, reporter){
4423 var self = this;
4433 function onTagStart(event){
4445 if(event.tagName.toLowerCase() === 'script'){
4452 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
446 }
447 }
4483 function onTagEnd(event){
4497 if(event.tagName.toLowerCase() === 'head'){
4503 parser.removeListener('tagstart', onTagStart);
4513 parser.removeListener('tagstart', onTagEnd);
452 }
453 }
4543 parser.addListener('tagstart', onTagStart);
4553 parser.addListener('tagend', onTagEnd);
456 }
457});
458/**
459 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
460 * MIT Licensed
461 */
4621HTMLHint.addRule({
463 id: 'id-class-value',
464 description: 'Id and class value must meet some rules.',
465 init: function(parser, reporter, options){
4666 var self = this;
4676 var arrRules = {
468 'underline': {
469 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
470 'message': 'Id and class value must lower case and split by underline.'
471 },
472 'dash': {
473 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
474 'message': 'Id and class value must lower case and split by dash.'
475 },
476 'hump': {
477 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
478 'message': 'Id and class value must meet hump style.'
479 }
480 }, rule;
4816 if(typeof options === 'string'){
4826 rule = arrRules[options];
483 }
484 else{
4850 rule = options;
486 }
4876 if(rule && rule.regId){
4886 var regId = rule.regId,
489 message = rule.message;
4906 parser.addListener('tagstart', function(event){
4916 var attrs = event.attrs,
492 attr,
493 col = event.col + event.tagName.length + 1;
4946 for(var i=0, l1=attrs.length;i<l1;i++){
49512 attr = attrs[i];
49612 if(attr.name.toLowerCase() === 'id'){
4976 if(regId.test(attr.value) === false){
4983 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
499 }
500 }
50112 if(attr.name.toLowerCase() === 'class'){
5026 var arrClass = attr.value.split(/\s+/g), classValue;
5036 for(var j=0, l2=arrClass.length;j<l2;j++){
5046 classValue = arrClass[j];
5056 if(classValue && regId.test(classValue) === false){
5063 reporter.warn(message, event.line, col + attr.index, self, classValue);
507 }
508 }
509 }
510 }
511 });
512 }
513 }
514});
515/**
516 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
517 * MIT Licensed
518 */
5191HTMLHint.addRule({
520 id: 'img-alt-require',
521 description: 'Alt of img tag must be set value.',
522 init: function(parser, reporter){
5233 var self = this;
5243 parser.addListener('tagstart', function(event){
5253 if(event.tagName.toLowerCase() === 'img'){
5263 var attrs = event.attrs;
5273 var haveAlt = false;
5283 for(var i=0, l=attrs.length;i<l;i++){
5298 if(attrs[i].name.toLowerCase() === 'alt'){
5302 haveAlt = true;
5312 break;
532 }
533 }
5343 if(haveAlt === false){
5351 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
536 }
537 }
538 });
539 }
540});
541/**
542 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
543 * MIT Licensed
544 */
5451HTMLHint.addRule({
546 id: 'spec-char-escape',
547 description: 'Special characters must be escaped.',
548 init: function(parser, reporter){
5492 var self = this;
5502 parser.addListener('text', function(event){
5513 var raw = event.raw,
552 reSpecChar = /[<>]/g,
553 match;
5543 while((match = reSpecChar.exec(raw))){
5553 var fixedPos = parser.fixPos(event, match.index);
5563 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
557 }
558 });
559 }
560});
561/**
562 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
563 * MIT Licensed
564 */
5651HTMLHint.addRule({
566 id: 'style-disabled',
567 description: 'Style tag can not be use.',
568 init: function(parser, reporter){
5692 var self = this;
5702 parser.addListener('tagstart', function(event){
5714 if(event.tagName.toLowerCase() === 'style'){
5721 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
573 }
574 });
575 }
576});
577/**
578 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
579 * MIT Licensed
580 */
5811HTMLHint.addRule({
582 id: 'tag-pair',
583 description: 'Tag must be paired.',
584 init: function(parser, reporter){
5853 var self = this;
5863 var stack=[],
587 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
5883 parser.addListener('tagstart', function(event){
5894 var tagName = event.tagName.toLowerCase();
5904 if (mapEmptyTags[tagName] === undefined && !event.close){
5914 stack.push(tagName);
592 }
593 });
5943 parser.addListener('tagend', function(event){
5953 var tagName = event.tagName.toLowerCase();
596 //向上寻找匹配的开始标签
5973 for(var pos = stack.length-1;pos >= 0; pos--){
5983 if(stack[pos] === tagName){
5992 break;
600 }
601 }
6023 if(pos >= 0){
6032 var arrTags = [];
6042 for(var i=stack.length-1;i>pos;i--){
6051 arrTags.push('</'+stack[i]+'>');
606 }
6072 if(arrTags.length > 0){
6081 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
609 }
6102 stack.length=pos;
611 }
612 else{
6131 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
614 }
615 });
6163 parser.addListener('end', function(event){
6173 var arrTags = [];
6183 for(var i=stack.length-1;i>=0;i--){
6191 arrTags.push('</'+stack[i]+'>');
620 }
6213 if(arrTags.length > 0){
6221 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
623 }
624 });
625 }
626});
627/**
628 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
629 * MIT Licensed
630 */
6311HTMLHint.addRule({
632 id: 'tag-self-close',
633 description: 'The empty tag must closed by self.',
634 init: function(parser, reporter){
6352 var self = this;
6362 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6372 parser.addListener('tagstart', function(event){
6384 var tagName = event.tagName.toLowerCase();
6394 if(mapEmptyTags[tagName] !== undefined){
6404 if(!event.close){
6412 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
642 }
643 }
644 });
645 }
646});
647/**
648 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
649 * MIT Licensed
650 */
6511HTMLHint.addRule({
652 id: 'tagname-lowercase',
653 description: 'Tagname must be lowercase.',
654 init: function(parser, reporter){
6552 var self = this;
6562 parser.addListener('tagstart,tagend', function(event){
6578 var tagName = event.tagName;
6588 if(tagName !== tagName.toLowerCase()){
6594 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
660 }
661 });
662 }
663});
\ No newline at end of file +

Coverage

98%
278
273
5

htmlhint.js

98%
278
273
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){
2413 rules[rule.id] = rule;
25 };
26
271 HTMLHint.verify = function(html, ruleset){
2835 if(ruleset === undefined){
290 ruleset = HTMLHint.defaultRuleset;
30 }
3135 var parser = new HTMLParser(),
32 reporter = new HTMLHint.Reporter(html.split(/\r?\n/), ruleset);
33
3435 var rule;
3535 for (var id in ruleset){
3635 rule = rules[id];
3735 if (rule !== undefined){
3835 rule.init(parser, reporter, ruleset[id]);
39 }
40 }
41
4235 parser.parse(html);
43
4435 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(){
6135 var self = this;
6235 self._init.apply(self,arguments);
63 };
64
651 Reporter.prototype = {
66 _init: function(lines, ruleset){
6735 var self = this;
6835 self.lines = lines;
6935 self.ruleset = ruleset;
7035 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){
7814 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){
8628 var self = this;
8728 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(){
10954 var self = this;
11054 self._init.apply(self,arguments);
111 };
112
1131 HTMLParser.prototype = {
114 _init: function(){
11554 var self = this;
11654 self._listeners = {};
11754 self._mapCdataTags = self.makeMap("script,style");
11854 self._arrBlocks = [];
119 },
120
121 makeMap: function(str){
12259 var obj = {}, items = str.split(",");
12359 for ( var i = 0; i < items.length; i++ ){
124178 obj[ items[i] ] = true;
125 }
12659 return obj;
127 },
128
129 // parse html code
130 parse: function(html){
131
13254 var self = this,
133 mapCdataTags = self._mapCdataTags;
134
13554 var regTag=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,
136 regAttr = /\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,
137 regLine = /\r?\n/g;
138
13954 var match, matchIndex, lastIndex = 0, tagName, arrAttrs, tagCDATA, arrCDATA, lastCDATAIndex = 0, text;
14054 var lastLineIndex = 0, line = 1;
14154 var arrBlocks = self._arrBlocks;
142
14354 self.fire('start', {
144 pos: 0,
145 line: 1,
146 col: 1
147 });
148
14954 while((match = regTag.exec(html))){
150107 matchIndex = match.index;
151107 if(matchIndex > lastIndex){//保存前面的文本或者CDATA
15214 text = html.substring(lastIndex, matchIndex);
15314 if(tagCDATA){
1544 arrCDATA.push(text);
155 }
156 else{//文本
15710 saveBlock('text', text, lastIndex);
158 }
159 }
160107 lastIndex = regTag.lastIndex;
161
162107 if((tagName = match[1])){
16333 if(tagCDATA && tagName === tagCDATA){//结束标签前输出CDATA
1646 text = arrCDATA.join('');
1656 saveBlock('cdata', text, lastCDATAIndex, {
166 'tagName': tagCDATA
167 });
1686 tagCDATA = null;
1696 arrCDATA = null;
170 }
17133 if(!tagCDATA){
172 //标签结束
17333 saveBlock('tagend', match[0], matchIndex, {
174 'tagName': tagName
175 });
17633 continue;
177 }
178 }
179
18074 if(tagCDATA){
1810 arrCDATA.push(match[0]);
182 }
183 else{
18474 if((tagName = match[4])){//标签开始
18561 arrAttrs = [];
18661 var attrs = match[5], attrMatch;
18761 while((attrMatch = regAttr.exec(attrs))){
18846 var name = attrMatch[1],
189 quote = attrMatch[2] ? attrMatch[2] :
190 attrMatch[4] ? attrMatch[4] : '',
191 value = attrMatch[3] ? attrMatch[3] :
192 attrMatch[5] ? attrMatch[5] :
193 attrMatch[6] ? attrMatch[6] : '';
19446 arrAttrs.push({'name': name, 'value': value, 'quote': quote, 'index': attrMatch.index, 'raw': attrMatch[0]});
195 }
19661 saveBlock('tagstart', match[0], matchIndex, {
197 'tagName': tagName,
198 'attrs': arrAttrs,
199 'close': match[6]
200 });
20161 if(mapCdataTags[tagName]){
2026 tagCDATA = tagName;
2036 arrCDATA = [];
2046 lastCDATAIndex = lastIndex;
205 }
206 }
20713 else if(match[2] || match[3]){//注释标签
20813 saveBlock('comment', match[0], matchIndex, {
209 'content': match[2] || match[3],
210 'long': match[2]?true:false
211 });
212 }
213 }
214 }
215
21654 if(html.length > lastIndex){
217 //结尾文本
2182 text = html.substring(lastIndex, html.length);
2192 saveBlock('text', text, lastIndex);
220 }
221
22254 self.fire('end', {
223 pos: lastIndex,
224 line: line,
225 col: lastIndex - lastLineIndex + 1
226 });
227
228 //存储区块
22954 function saveBlock(type, raw, pos, data){
230125 var col = pos - lastLineIndex + 1;
231125 if(data === undefined){
23212 data = {};
233 }
234125 data.raw = raw;
235125 data.pos = pos;
236125 data.line = line;
237125 data.col = col;
238125 arrBlocks.push(data);
239125 self.fire(type, data);
240125 var lineMatch;
241125 while((lineMatch = regLine.exec(raw))){
2424 line ++;
2434 lastLineIndex = pos + regLine.lastIndex;
244 }
245 }
246
247 },
248
249 // add event
250 addListener: function(types, listener){
25165 var _listeners = this._listeners;
25265 var arrTypes = types.split(/[,\s]/), type;
25365 for(var i=0, l = arrTypes.length;i<l;i++){
25467 type = arrTypes[i];
25567 if (_listeners[type] === undefined){
25667 _listeners[type] = [];
257 }
25867 _listeners[type].push(listener);
259 }
260 },
261
262 // fire event
263 fire: function(type, data){
264233 if (data === undefined){
2650 data = {};
266 }
267233 data.type = type;
268233 var self = this,
269 listeners = [],
270 listenersType = self._listeners[type],
271 listenersAll = self._listeners['all'];
272233 if (listenersType !== undefined){
27362 listeners = listeners.concat(listenersType);
274 }
275233 if (listenersAll !== undefined){
276100 listeners = listeners.concat(listenersAll);
277 }
278233 for (var i = 0, l = listeners.length; i < l; i++){
279156 listeners[i].call(self, data);
280 }
281 },
282
283 // remove event
284 removeListener: function(type, listener){
28512 var listenersType = this._listeners[type];
28612 if(listenersType !== undefined){
28710 for (var i = 0, l = listenersType.length; i < l; i++){
2887 if (listenersType[i] === listener){
2897 listenersType.splice(i, 1);
2907 break;
291 }
292 }
293 }
294 },
295
296 //fix pos if event.raw have \n
297 fixPos: function(event, index){
2983 var text = event.raw.substr(0, index);
2993 var arrLines = text.split(/\r?\n/),
300 lineCount = arrLines.length - 1,
301 line = event.line, col;
3023 if(lineCount > 0){
3031 line += lineCount;
3041 col = arrLines[lineCount].length + 1;
305 }
306 else{
3072 col = event.col + index;
308 }
3093 return {
310 line: line,
311 col: col
312 };
313
314 }
315 };
316
3171 return HTMLParser;
318
319})();
320
3211if (typeof exports === 'object' && exports){
3221 exports.HTMLParser = HTMLParser;
323}
324/**
325 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
326 * MIT Licensed
327 */
3281HTMLHint.addRule({
329 id: 'attr-lowercase',
330 description: 'Attribute name must be lowercase.',
331 init: function(parser, reporter){
3322 var self = this;
3332 parser.addListener('tagstart', function(event){
3342 var attrs = event.attrs,
335 attr,
336 col = event.col + event.tagName.length + 1;
3372 for(var i=0, l=attrs.length;i<l;i++){
3382 attr = attrs[i];
3392 var attrName = attr.name;
3402 if(attrName !== attrName.toLowerCase()){
3411 reporter.error('Attribute name [ '+attrName+' ] must be lower case.', event.line, col + attr.index, self, attr.raw);
342 }
343 }
344 });
345 }
346});
347/**
348 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
349 * MIT Licensed
350 */
3511HTMLHint.addRule({
352 id: 'attr-value-double-quotes',
353 description: 'Attribute value must closed by double quotes.',
354 init: function(parser, reporter){
3553 var self = this;
3563 parser.addListener('tagstart', function(event){
3573 var attrs = event.attrs,
358 attr,
359 col = event.col + event.tagName.length + 1;
3603 for(var i=0, l=attrs.length;i<l;i++){
3615 attr = attrs[i];
3625 if(attr.quote !== '"' && (attr.value !== '' || (attr.value === attr.quote === ''))){
3632 reporter.error('The value of attribute [ '+attr.name+' ] must closed by double quotes.', event.line, col + attr.index, self, attr.raw);
364 }
365 }
366 });
367 }
368});
369/**
370 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
371 * MIT Licensed
372 */
3731HTMLHint.addRule({
374 id: 'attr-value-not-empty',
375 description: 'Attribute must set value.',
376 init: function(parser, reporter){
3773 var self = this;
3783 parser.addListener('tagstart', function(event){
3793 var attrs = event.attrs,
380 attr,
381 col = event.col + event.tagName.length + 1;
3823 for(var i=0, l=attrs.length;i<l;i++){
3833 attr = attrs[i];
3843 if(attr.quote === '' && attr.value === ''){
3851 reporter.warn('The attribute [ '+attr.name+' ] must set value.', event.line, col + attr.index, self, attr.raw);
386 }
387 }
388 });
389 }
390});
391/**
392 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
393 * MIT Licensed
394 */
3951HTMLHint.addRule({
396 id: 'doctype-first',
397 description: 'Doctype must be first.',
398 init: function(parser, reporter){
3992 var self = this;
4002 var allEvent = function(event){
4014 if(event.type === 'start' || (event.type === 'text' && /^\s*$/.test(event.raw))){
4022 return;
403 }
4042 if((event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false){
4051 reporter.error('Doctype must be first.', event.line, event.col, self, event.raw);
406 }
4072 parser.removeListener('all', allEvent);
408 };
4092 parser.addListener('all', allEvent);
410 }
411});
412/**
413 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
414 * MIT Licensed
415 */
4161HTMLHint.addRule({
417 id: 'doctype-html5',
418 description: 'Doctype must be html5.',
419 init: function(parser, reporter){
4202 var self = this;
4212 function onComment(event){
4229 if(event.long === false && event.content.toLowerCase() !== 'doctype html'){
4231 reporter.warn('Doctype must be html5.', event.line, event.col, self, event.raw);
424 }
425 }
4262 function onTagStart(){
4272 parser.removeListener('comment', onComment);
4282 parser.removeListener('tagstart', onTagStart);
429 }
4302 parser.addListener('all', onComment);
4312 parser.addListener('tagstart', onTagStart);
432 }
433});
434/**
435 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
436 * MIT Licensed
437 */
4381HTMLHint.addRule({
439 id: 'head-script-disabled',
440 description: 'The script tag can not be used in head.',
441 init: function(parser, reporter){
4423 var self = this;
4433 function onTagStart(event){
4445 if(event.tagName.toLowerCase() === 'script'){
4452 reporter.warn('The script tag can not be used in head.', event.line, event.col, self, event.raw);
446 }
447 }
4483 function onTagEnd(event){
4497 if(event.tagName.toLowerCase() === 'head'){
4503 parser.removeListener('tagstart', onTagStart);
4513 parser.removeListener('tagstart', onTagEnd);
452 }
453 }
4543 parser.addListener('tagstart', onTagStart);
4553 parser.addListener('tagend', onTagEnd);
456 }
457});
458/**
459 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
460 * MIT Licensed
461 */
4621HTMLHint.addRule({
463 id: 'id-class-value',
464 description: 'Id and class value must meet some rules.',
465 init: function(parser, reporter, options){
4666 var self = this;
4676 var arrRules = {
468 'underline': {
469 'regId': /^[a-z\d]+(_[a-z\d]+)*$/,
470 'message': 'Id and class value must lower case and split by underline.'
471 },
472 'dash': {
473 'regId': /^[a-z\d]+(-[a-z\d]+)*$/,
474 'message': 'Id and class value must lower case and split by dash.'
475 },
476 'hump': {
477 'regId': /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
478 'message': 'Id and class value must meet hump style.'
479 }
480 }, rule;
4816 if(typeof options === 'string'){
4826 rule = arrRules[options];
483 }
484 else{
4850 rule = options;
486 }
4876 if(rule && rule.regId){
4886 var regId = rule.regId,
489 message = rule.message;
4906 parser.addListener('tagstart', function(event){
4916 var attrs = event.attrs,
492 attr,
493 col = event.col + event.tagName.length + 1;
4946 for(var i=0, l1=attrs.length;i<l1;i++){
49512 attr = attrs[i];
49612 if(attr.name.toLowerCase() === 'id'){
4976 if(regId.test(attr.value) === false){
4983 reporter.warn(message, event.line, col + attr.index, self, attr.raw);
499 }
500 }
50112 if(attr.name.toLowerCase() === 'class'){
5026 var arrClass = attr.value.split(/\s+/g), classValue;
5036 for(var j=0, l2=arrClass.length;j<l2;j++){
5046 classValue = arrClass[j];
5056 if(classValue && regId.test(classValue) === false){
5063 reporter.warn(message, event.line, col + attr.index, self, classValue);
507 }
508 }
509 }
510 }
511 });
512 }
513 }
514});
515/**
516 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
517 * MIT Licensed
518 */
5191HTMLHint.addRule({
520 id: 'img-alt-require',
521 description: 'Alt of img tag must be set value.',
522 init: function(parser, reporter){
5233 var self = this;
5243 parser.addListener('tagstart', function(event){
5253 if(event.tagName.toLowerCase() === 'img'){
5263 var attrs = event.attrs;
5273 var haveAlt = false;
5283 for(var i=0, l=attrs.length;i<l;i++){
5298 if(attrs[i].name.toLowerCase() === 'alt'){
5302 haveAlt = true;
5312 break;
532 }
533 }
5343 if(haveAlt === false){
5351 reporter.warn('Alt of img tag must be set value.', event.line, event.col, self, event.raw);
536 }
537 }
538 });
539 }
540});
541/**
542 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
543 * MIT Licensed
544 */
5451HTMLHint.addRule({
546 id: 'spec-char-escape',
547 description: 'Special characters must be escaped.',
548 init: function(parser, reporter){
5492 var self = this;
5502 parser.addListener('text', function(event){
5513 var raw = event.raw,
552 reSpecChar = /[<>]/g,
553 match;
5543 while((match = reSpecChar.exec(raw))){
5553 var fixedPos = parser.fixPos(event, match.index);
5563 reporter.error('Special characters must be escaped : [ '+match[0]+' ].', fixedPos.line, fixedPos.col, self, event.raw);
557 }
558 });
559 }
560});
561/**
562 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
563 * MIT Licensed
564 */
5651HTMLHint.addRule({
566 id: 'style-disabled',
567 description: 'Style tag can not be use.',
568 init: function(parser, reporter){
5692 var self = this;
5702 parser.addListener('tagstart', function(event){
5714 if(event.tagName.toLowerCase() === 'style'){
5721 reporter.warn('Style tag can not be use.', event.line, event.col, self, event.raw);
573 }
574 });
575 }
576});
577/**
578 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
579 * MIT Licensed
580 */
5811HTMLHint.addRule({
582 id: 'tag-pair',
583 description: 'Tag must be paired.',
584 init: function(parser, reporter){
5853 var self = this;
5863 var stack=[],
587 mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
5883 parser.addListener('tagstart', function(event){
5894 var tagName = event.tagName.toLowerCase();
5904 if (mapEmptyTags[tagName] === undefined && !event.close){
5914 stack.push(tagName);
592 }
593 });
5943 parser.addListener('tagend', function(event){
5953 var tagName = event.tagName.toLowerCase();
596 //向上寻找匹配的开始标签
5973 for(var pos = stack.length-1;pos >= 0; pos--){
5983 if(stack[pos] === tagName){
5992 break;
600 }
601 }
6023 if(pos >= 0){
6032 var arrTags = [];
6042 for(var i=stack.length-1;i>pos;i--){
6051 arrTags.push('</'+stack[i]+'>');
606 }
6072 if(arrTags.length > 0){
6081 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, event.raw);
609 }
6102 stack.length=pos;
611 }
612 else{
6131 reporter.error('Tag must be paired, No start tag: [ ' + event.raw + ' ]', event.line, event.col, self, event.raw);
614 }
615 });
6163 parser.addListener('end', function(event){
6173 var arrTags = [];
6183 for(var i=stack.length-1;i>=0;i--){
6191 arrTags.push('</'+stack[i]+'>');
620 }
6213 if(arrTags.length > 0){
6221 reporter.error('Tag must be paired, Missing: [ '+ arrTags.join('') + ' ]', event.line, event.col, self, '');
623 }
624 });
625 }
626});
627/**
628 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
629 * MIT Licensed
630 */
6311HTMLHint.addRule({
632 id: 'tag-self-close',
633 description: 'The empty tag must closed by self.',
634 init: function(parser, reporter){
6352 var self = this;
6362 var mapEmptyTags = parser.makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed");//HTML 4.01
6372 parser.addListener('tagstart', function(event){
6384 var tagName = event.tagName.toLowerCase();
6394 if(mapEmptyTags[tagName] !== undefined){
6404 if(!event.close){
6412 reporter.warn('The empty tag : [ '+tagName+' ] must closed by self.', event.line, event.col, self, event.raw);
642 }
643 }
644 });
645 }
646});
647/**
648 * Copyright (c) 2013, Yanis Wang <yanis.wang@gmail.com>
649 * MIT Licensed
650 */
6511HTMLHint.addRule({
652 id: 'tagname-lowercase',
653 description: 'Tagname must be lowercase.',
654 init: function(parser, reporter){
6552 var self = this;
6562 parser.addListener('tagstart,tagend', function(event){
6578 var tagName = event.tagName;
6588 if(tagName !== tagName.toLowerCase()){
6594 reporter.error('Tagname [ '+tagName+' ] must be lower case.', event.line, event.col, self, event.raw);
660 }
661 });
662 }
663});
\ No newline at end of file diff --git a/lib/htmlhint.js b/lib/htmlhint.js index 635d830f7..5a521a2a4 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.1",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-v+1;n===e&&(n={}),n.raw=a,n.pos=r,n.line=w,n.col=i,L.push(n),u.fire(t,n);for(var s;s=f.exec(a);)w++,v=r+f.lastIndex}var r,n,i,s,o,l,d,u=this,c=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,m=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,f=/\r?\n/g,p=0,h=0,v=0,w=1,L=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});r=g.exec(t);)if(n=r.index,n>p&&(d=t.substring(p,n),o?l.push(d):a("text",d,p)),p=g.lastIndex,!(i=r[1])||(o&&i===o&&(d=l.join(""),a("cdata",d,h,{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];b=m.exec(H);){var y=b[1],T=b[2]?b[2]:b[4]?b[4]:"",x=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:y,value:x,quote:T,index:b.index,raw:b[0]})}a("tagstart",r[0],n,{tagName:i,attrs:s,close:r[6]}),c[i]&&(o=i,l=[],h=p)}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>p&&(d=t.substring(p,t.length),a("text",d,p)),u.fire("end",{pos:p,line:w,col:p-v+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.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:"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,d=r.length;d>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 u,c=a.value.split(/\s+/g),g=0,m=c.length;m>g;g++)u=c[g],u&&s.test(u)===!1&&t.warn(o,e.line,i+a.index,n,u)})}}}),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:"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 +var HTMLHint=function(e){var t=[],a={};return a.version="0.9.1",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),u.fire(t,n);for(var s;s=f.exec(a);)w++,h=r+f.lastIndex}var r,n,i,s,o,l,d,u=this,c=u._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:"[^"]*"|'[^']*'|[^"'<>])*?)\s*(\/?))>/g,m=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s]+)))?/g,f=/\r?\n/g,p=0,v=0,h=0,w=1,L=u._arrBlocks;for(u.fire("start",{pos:0,line:1,col:1});r=g.exec(t);)if(n=r.index,n>p&&(d=t.substring(p,n),o?l.push(d):a("text",d,p)),p=g.lastIndex,!(i=r[1])||(o&&i===o&&(d=l.join(""),a("cdata",d,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];b=m.exec(H);){var y=b[1],T=b[2]?b[2]:b[4]?b[4]:"",x=b[3]?b[3]:b[5]?b[5]:b[6]?b[6]:"";s.push({name:y,value:x,quote:T,index:b.index,raw:b[0]})}a("tagstart",r[0],n,{tagName:i,attrs:s,close:r[6]}),c[i]&&(o=i,l=[],v=p)}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>p&&(d=t.substring(p,t.length),a("text",d,p)),u.fire("end",{pos:p,line:w,col:p-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:"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,d=r.length;d>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 u,c=a.value.split(/\s+/g),g=0,m=c.length;m>g;g++)u=c[g],u&&s.test(u)===!1&&t.warn(o,e.line,i+a.index,n,u)})}}}),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:"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/src/rules/attr-value-double-quotes.js b/src/rules/attr-value-double-quotes.js index 4366cbda5..6429c37fe 100644 --- a/src/rules/attr-value-double-quotes.js +++ b/src/rules/attr-value-double-quotes.js @@ -13,7 +13,7 @@ HTMLHint.addRule({ col = event.col + event.tagName.length + 1; for(var i=0, l=attrs.length;i'; + var code = ''; var messages = HTMLHint.verify(code, {'attr-value-double-quotes': true}); - expect(messages.length).to.be(1); + expect(messages.length).to.be(2); expect(messages[0].rule.id).to.be('attr-value-double-quotes'); expect(messages[0].line).to.be(1); expect(messages[0].col).to.be(3); + expect(messages[1].rule.id).to.be('attr-value-double-quotes'); + expect(messages[1].line).to.be(1); + expect(messages[1].col).to.be(14); }); it('Attribute value no closed should not result in an error', function(){ - var code = ''; + var code = ''; var messages = HTMLHint.verify(code, {'attr-value-double-quotes': true}); expect(messages.length).to.be(0); });