From f4b4eec4cd954d4b13a702b0350a6de60529c592 Mon Sep 17 00:00:00 2001 From: Yanis Wang Date: Sun, 11 Oct 2015 19:40:03 +0800 Subject: [PATCH] support format as custom: json, junit, checkstyle --- CHANGE.md | 1 + bin/htmlhint | 214 +++++++++++++++++++++++++++++++++++------------- lib/htmlhint.js | 2 +- package.json | 3 +- src/core.js | 57 +++++++++++++ 5 files changed, 216 insertions(+), 61 deletions(-) diff --git a/CHANGE.md b/CHANGE.md index b64df674e..d5b6d49d9 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -7,6 +7,7 @@ add: 1. attr-unsafe-chars(rule): show unsafe code in message 2. support glob pattern for cli +3. support format as custom: json, junit, checkstyle fix: diff --git a/bin/htmlhint b/bin/htmlhint index 6e898e5a2..5608d446d 100755 --- a/bin/htmlhint +++ b/bin/htmlhint @@ -7,6 +7,7 @@ var stripJsonComments = require('strip-json-comments'); var async = require('async'); var glob = require("glob"); var parseGlob = require('parse-glob'); +var xml = require('xml'); var HTMLHint = require("../index").HTMLHint; var pkg = require('../package.json'); @@ -29,6 +30,7 @@ program.on('--help', function(){ console.log(' htmlhint www'); console.log(' htmlhint www/test.html'); console.log(' htmlhint www/**/*.xhtml'); + console.log(' htmlhint www/**/*.{htm,html}'); console.log(' htmlhint --list'); console.log(' htmlhint --rules tag-pair,id-class-value=underline test.html'); console.log(' htmlhint --config .htmlhintrc test.html'); @@ -39,11 +41,12 @@ program.on('--help', function(){ program .version(pkg.version) .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) - .option('-j, --json', 'output messages as raw JSON') - .option('-i, --ignore ', 'Add pattern to exclude matches') + .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) + .option('-p, --plugin ', 'load custom rules from file or folder') + .option('-f, --format ', 'output messages as custom format') + .option('-i, --ignore ', 'add pattern to exclude matches') .parse(process.argv); if(program.list){ @@ -57,8 +60,9 @@ if(arrTargets.length === 0){ } hintTargets(arrTargets, { + plugin: program.plugin, ruleset: program.rules, - json: program.json, + format: program.format, ignore: program.ignore }); @@ -80,11 +84,11 @@ function hintTargets(arrTargets, options){ var allHintCount = 0; var startTime = new Date().getTime(); - // json mode - var json = options.json; - var arrJson = []; + // custom format + var format = options.format; + var arrAllMessages = []; - if(!json){ + if(!format){ console.log(''); } var arrTasks = []; @@ -94,17 +98,22 @@ function hintTargets(arrTargets, options){ allFileCount += result.targetFileCount; allHintFileCount += result.targetHintFileCount; allHintCount += result.targetHintCount; - arrJson = arrJson.concat(result.arrJson); + arrAllMessages = arrAllMessages.concat(result.arrTargetMessages); next(); }); }); }); async.series(arrTasks, function(){ - if(json){ - console.log(JSON.stringify(arrJson)); + var spendTime = new Date().getTime() - startTime; + // output as custom format + if(format){ + formatResult({ + arrAllMessages: arrAllMessages, + allFileCount: allFileCount, + time: spendTime + }, format); } else{ - var spendTime = new Date().getTime() - startTime; if(allHintCount > 0){ console.log('Scan %d files, found %d errors in %d files (%d ms)'.red, allFileCount, allHintCount, allHintFileCount, spendTime); } @@ -116,6 +125,117 @@ function hintTargets(arrTargets, options){ }); } +// output as custom format +function formatResult(hintInfo, format){ + switch(format){ + case 'json': + console.log(JSON.stringify(hintInfo.arrAllMessages)); + break; + case 'junit': + formatJunit(hintInfo); + break; + case 'checkstyle': + formatCheckstyle(hintInfo); + break; + default: + console.log('No supported format, supported format:json, junit, checkstyle.'.red); + process.exit(1); + } +} + +// format as junit +function formatJunit(hintInfo){ + var arrTestcase = []; + var arrAllMessages = hintInfo.arrAllMessages; + arrAllMessages.forEach(function(fileInfo){ + var arrMessages = fileInfo.messages; + var arrLogs = HTMLHint.format(arrMessages); + arrTestcase.push({ + testcase: [ + { + _attr: { + name: fileInfo.file, + time: (fileInfo.time / 1000).toFixed(3) + } + }, + { + failure: { + _attr: { + message: 'Found '+arrMessages.length+' errors' + }, + _cdata: arrLogs.join('\r\n') + } + } + ] + }); + }); + var objXml = { + testsuites: [ + { + testsuite: [ + { + _attr: { + name: 'HTMLHint Tests', + time: (hintInfo.time / 1000).toFixed(3), + tests: hintInfo.allFileCount, + failures: arrAllMessages.length + } + } + ].concat(arrTestcase) + } + ] + }; + console.log(xml(objXml, { + declaration: true, + indent: ' ' + })); +} + +// format as checkstyle +function formatCheckstyle(hintInfo){ + var arrFiles = []; + var arrAllMessages = hintInfo.arrAllMessages; + arrAllMessages.forEach(function(fileInfo){ + var arrMessages = fileInfo.messages; + var arrErrors = []; + arrMessages.forEach(function(message){ + arrErrors.push({ + error: { + _attr: { + line: message.line, + column: message.col, + severity: message.type, + message: message.message, + source: 'htmlhint.'+message.rule.id + } + } + }); + }); + arrFiles.push({ + file: [ + { + _attr: { + name: fileInfo.file + } + } + ].concat(arrErrors) + }); + }); + var objXml = { + checkstyle: [ + { + _attr: { + version: '4.3' + } + } + ].concat(arrFiles) + }; + console.log(xml(objXml, { + declaration: true, + indent: ' ' + })); +} + // hint all files function hintAllFiles(target, options, onFinised){ var globInfo = getGlobInfo(target); @@ -126,57 +246,38 @@ function hintAllFiles(target, options, onFinised){ var targetHintFileCount = 0; var targetHintCount = 0; - // json mode - var json = options.json; - var arrJson = []; + // custom format + var format = options.format; + var arrTargetMessages = []; // init ruleset var ruleset = options.ruleset; if(ruleset === undefined){ - ruleset = getConfig(program.config, globInfo.base, json); + ruleset = getConfig(program.config, globInfo.base, format); } // hint queue var hintQueue = async.queue(function (filepath, next) { + var startTime = new Date().getTime(); var messages = hintFile(filepath, ruleset); + var spendTime = new Date().getTime() - startTime; var hintCount = messages.length; if(hintCount > 0){ - if(json){ - arrJson.push({'file': filepath, 'messages': messages}); + if(format){ + arrTargetMessages.push({ + 'file': filepath, + 'messages': messages, + 'time': spendTime + }); } else{ console.log(' '+filepath.white); - messages.forEach(function(hint){ - var leftWindow = 40; - var rightWindow = leftWindow + 20; - var evidence = hint.evidence; - var line = hint.line; - var col = hint.col; - var evidenceCount = evidence.length; - var leftCol = col > leftWindow + 1 ? col - leftWindow : 1; - var rightCol = evidence.length > col + rightWindow ? col + rightWindow : evidenceCount; - if(col < leftWindow + 1){ - rightCol += leftWindow - col + 1; - } - evidence = evidence.replace(/\t/g, ' ').substring(leftCol - 1, rightCol); - // add ... - if(leftCol > 1){ - evidence = '...' + evidence; - leftCol -= 3; - } - if(rightCol < evidenceCount){ - evidence += '...'; - } - // show evidence - console.log(' L%d |%s'.white, line, evidence.gray); - // show pointer & message - var pointCol = col - leftCol; - // add double byte character - var match = evidence.substring(0, pointCol).match(/[^\u0000-\u00ff]/g); - if(match !== null){ - pointCol += match.length; - } - console.log(' %s^ %s'.white, repeatStr(String(line).length + 3 + pointCol), (hint.message + ' (' + hint.rule.id+')')[hint.type === 'error'?'red':'yellow']); + var arrLogs = HTMLHint.format(messages, { + colors: true, + indent: 6 + }); + arrLogs.forEach(function(str){ + console.log(str); }); console.log(''); } @@ -206,7 +307,7 @@ function hintAllFiles(target, options, onFinised){ targetFileCount: targetFileCount, targetHintFileCount: targetHintFileCount, targetHintCount: targetHintCount, - arrJson: arrJson + arrTargetMessages: arrTargetMessages }); } } @@ -246,7 +347,7 @@ function getGlobInfo(target){ } // search and load config -function getConfig(configFile, base, json){ +function getConfig(configFile, base, format){ if(configFile === undefined && fs.existsSync(base)){ // find default config file in parent directory if(fs.statSync(base).isDirectory() === false){ @@ -267,7 +368,7 @@ function getConfig(configFile, base, json){ ruleset; try{ ruleset = JSON.parse(stripJsonComments(config)); - if(!json){ + if(!format){ console.log(' Config loaded: %s', configFile.cyan); console.log(''); } @@ -309,8 +410,3 @@ function hintFile(filepath, ruleset){ var content = fs.readFileSync(filepath, 'utf-8'); return HTMLHint.verify(content, ruleset); } - -// repeat string -function repeatStr(n, str){ - return new Array(n + 1).join(str || ' '); -} diff --git a/lib/htmlhint.js b/lib/htmlhint.js index a39bffe30..06ab5e395 100644 --- a/lib/htmlhint.js +++ b/lib/htmlhint.js @@ -5,4 +5,4 @@ * (c) 2014-2015 Yanis Wang . * MIT Licensed */ -var HTMLHint=function(e){var t={};return t.version="0.9.10",t.rules={},t.defaultRuleset={"tagname-lowercase":!0,"attr-lowercase":!0,"attr-value-double-quotes":!0,"doctype-first":!0,"tag-pair":!0,"spec-char-escape":!0,"id-unique":!0,"src-not-empty":!0,"attr-no-duplication":!0,"title-require":!0},t.addRule=function(e){t.rules[e.id]=e},t.verify=function(a,n){a=a.replace(/^\s*/i,function(t,a){return n===e&&(n={}),a.replace(/(?:^|,)\s*([^:,]+)\s*(?:\:\s*([^,\s]+))?/g,function(t,a,i){"false"===i?i=!1:"true"===i&&(i=!0),n[a]=i===e?!0:i}),""}),(n===e||0===Object.keys(n).length)&&(n=t.defaultRuleset);var i,r=new HTMLParser,s=new t.Reporter(a.split(/\r?\n/),n),o=t.rules;for(var l in n)i=o[l],i!==e&&n[l]!==!1&&i.init(r,s,n[l]);return r.parse(a),s.messages},t}();"object"==typeof exports&&exports&&(exports.HTMLHint=HTMLHint),function(e){var t=function(){var e=this;e._init.apply(e,arguments)};t.prototype={_init:function(e,t){var a=this;a.lines=e,a.ruleset=t,a.messages=[]},error:function(e,t,a,n,i){this.report("error",e,t,a,n,i)},warn:function(e,t,a,n,i){this.report("warning",e,t,a,n,i)},info:function(e,t,a,n,i){this.report("info",e,t,a,n,i)},report:function(e,t,a,n,i,r){var s=this;s.messages.push({type:e,message:t,raw:r,evidence:s.lines[a-1],line:a,col:n,rule:{id:i.id,description:i.description,link:"https://github.com/yaniswang/HTMLHint/wiki/"+i.id}})}},e.Reporter=t}(HTMLHint);var HTMLParser=function(e){var t=function(){var e=this;e._init.apply(e,arguments)};return t.prototype={_init:function(){var e=this;e._listeners={},e._mapCdataTags=e.makeMap("script,style"),e._arrBlocks=[],e.lastEvent=null},makeMap:function(e){for(var t={},a=e.split(","),n=0;a.length>n;n++)t[a[n]]=!0;return t},parse:function(t){function a(t,a,n,i){var r=n-b+1;i===e&&(i={}),i.raw=a,i.pos=n,i.line=w,i.col=r,L.push(i),c.fire(t,i);for(var s;s=m.exec(a);)w++,b=n+m.lastIndex}var n,i,r,s,o,l,u,d,c=this,f=c._mapCdataTags,g=/<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*?)\s*(\/?))>/g,h=/\s*([\w\-:]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"'>]*)))?/g,m=/\r?\n/g,p=0,v=0,b=0,w=1,L=c._arrBlocks;for(c.fire("start",{pos:0,line:1,col:1});n=g.exec(t);)if(i=n.index,i>p&&(d=t.substring(p,i),o?u.push(d):a("text",d,p)),p=g.lastIndex,!(r=n[1])||(o&&r===o&&(d=u.join(""),a("cdata",d,v,{tagName:o,attrs:l}),o=null,l=null,u=null),o))if(o)u.push(n[0]);else if(r=n[4]){s=[];for(var T,H=n[5],y=0;T=h.exec(H);){var x=T[1],M=T[2]?T[2]:T[4]?T[4]:"",N=T[3]?T[3]:T[5]?T[5]:T[6]?T[6]:"";s.push({name:x,value:N,quote:M,index:T.index,raw:T[0]}),y+=T[0].length}y===H.length?(a("tagstart",n[0],i,{tagName:r,attrs:s,close:n[6]}),f[r]&&(o=r,l=s.concat(),u=[],v=p)):a("text",n[0],i)}else(n[2]||n[3])&&a("comment",n[0],i,{content:n[2]||n[3],"long":n[2]?!0:!1});else a("tagend",n[0],i,{tagName:r});t.length>p&&(d=t.substring(p,t.length),a("text",d,p)),c.fire("end",{pos:p,line:w,col:t.length-b+1})},addListener:function(t,a){for(var n,i=this._listeners,r=t.split(/[,\s]/),s=0,o=r.length;o>s;s++)n=r[s],i[n]===e&&(i[n]=[]),i[n].push(a)},fire:function(t,a){a===e&&(a={}),a.type=t;var n=this,i=[],r=n._listeners[t],s=n._listeners.all;r!==e&&(i=i.concat(r)),s!==e&&(i=i.concat(s));var o=n.lastEvent;null!==o&&(delete o.lastEvent,a.lastEvent=o),n.lastEvent=a;for(var l=0,u=i.length;u>l;l++)i[l].call(n,a)},removeListener:function(t,a){var n=this._listeners[t];if(n!==e)for(var i=0,r=n.length;r>i;i++)if(n[i]===a){n.splice(i,1);break}},fixPos:function(e,t){var a,n=e.raw.substr(0,t),i=n.split(/\r?\n/),r=i.length-1,s=e.line;return r>0?(s+=r,a=i[r].length+1):a=e.col+t,{line:s,col:a}},getMapAttrs:function(e){for(var t,a={},n=0,i=e.length;i>n;n++)t=e[n],a[t.name]=t.value;return a}},t}();"object"==typeof exports&&exports&&(exports.HTMLParser=HTMLParser),HTMLHint.addRule({id:"alt-require",description:"The alt attribute of an element must be present and alt attribute of area[href] and input[type=image] must have a value.",init:function(e,t){var a=this;e.addListener("tagstart",function(n){var i,r=n.tagName.toLowerCase(),s=e.getMapAttrs(n.attrs),o=n.col+r.length+1;"img"!==r||"alt"in s?("area"===r&&"href"in s||"input"===r&&"image"===s.type)&&("alt"in s&&""!==s.alt||(i="area"===r?"area[href]":"input[type=image]",t.warn("The alt attribute of "+i+" must have a value.",n.line,o,a,n.raw))):t.warn("An alt attribute must be present on elements.",n.line,o,a,n.raw)})}}),HTMLHint.addRule({id:"attr-lowercase",description:"All attribute names must be in lowercase.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++){n=i[s];var l=n.name;l!==l.toLowerCase()&&t.error("The attribute name of [ "+l+" ] must be in lowercase.",e.line,r+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-no-duplication",description:"Elements cannot have duplicate attributes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o={},l=0,u=r.length;u>l;l++)n=r[l],i=n.name,o[i]===!0&&t.error("Duplicate of attribute name [ "+n.name+" ] was found.",e.line,s+n.index,a,n.raw),o[i]=!0})}}),HTMLHint.addRule({id:"attr-unsafe-chars",description:"Attribute values cannot contain unsafe chars.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i,r=e.attrs,s=e.col+e.tagName.length+1,o=/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/,l=0,u=r.length;u>l;l++)if(n=r[l],i=n.value.match(o),null!==i){var d=escape(i[0]).replace(/%u/,"\\u").replace(/%/,"\\x");t.warn("The value of attribute [ "+n.name+" ] cannot contain an unsafe char [ "+d+" ].",e.line,s+n.index,a,n.raw)}})}}),HTMLHint.addRule({id:"attr-value-double-quotes",description:"Attribute values must be in double quotes.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],(""!==n.value&&'"'!==n.quote||""===n.value&&"'"===n.quote)&&t.error("The value of attribute [ "+n.name+" ] must be in double quotes.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"attr-value-not-empty",description:"All attributes must have values.",init:function(e,t){var a=this;e.addListener("tagstart",function(e){for(var n,i=e.attrs,r=e.col+e.tagName.length+1,s=0,o=i.length;o>s;s++)n=i[s],""===n.quote&&""===n.value&&t.warn("The attribute [ "+n.name+" ] must have a value.",e.line,r+n.index,a,n.raw)})}}),HTMLHint.addRule({id:"csslint",description:"Scan css with csslint.",init:function(e,t,a){var n=this;e.addListener("cdata",function(e){if("style"===e.tagName.toLowerCase()){var i;if(i="object"==typeof exports&&require?require("csslint").CSSLint.verify:CSSLint.verify,void 0!==a){var r=e.line-1,s=e.col-1;try{var o=i(e.raw,a).messages;o.forEach(function(e){var a=e.line;t["warning"===e.type?"warn":"error"]("["+e.rule.id+"] "+e.message,r+a,(1===a?s:0)+e.col,n,e.evidence)})}catch(l){}}}})}}),HTMLHint.addRule({id:"doctype-first",description:"Doctype must be declared first.",init:function(e,t){var a=this,n=function(i){"start"===i.type||"text"===i.type&&/^\s*$/.test(i.raw)||(("comment"!==i.type&&i.long===!1||/^DOCTYPE\s+/i.test(i.content)===!1)&&t.error("Doctype must be declared first.",i.line,i.col,a,i.raw),e.removeListener("all",n))};e.addListener("all",n)}}),HTMLHint.addRule({id:"doctype-html5",description:'Invalid doctype. Use: ""',init:function(e,t){function a(e){e.long===!1&&"doctype html"!==e.content.toLowerCase()&&t.warn('Invalid doctype. Use: ""',e.line,e.col,i,e.raw)}function n(){e.removeListener("comment",a),e.removeListener("tagstart",n)}var i=this;e.addListener("all",a),e.addListener("tagstart",n)}}),HTMLHint.addRule({id:"head-script-disabled",description:"The