diff --git a/.npmignore b/.npmignore index 0bbc5b50e..8cefadb96 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ node_modules coverage -npm-debug.log \ No newline at end of file +npm-debug.log +src +test \ No newline at end of file diff --git a/CHANGE.md b/CHANGE.md index 5fce98cf8..b64df674e 100644 --- a/CHANGE.md +++ b/CHANGE.md @@ -1,16 +1,17 @@ HTMLHint change log ==================== -## ver 0.9.10 (2015-10-9) +## ver 0.9.10 (2015-10-10) add: 1. attr-unsafe-chars(rule): show unsafe code in message +2. support glob pattern for cli fix: 1. title-require(rule): report error when `test` -1. title-require(rule): report error when `` +2. title-require(rule): report error when `` ## ver 0.9.9 (2015-10-9) diff --git a/README.md b/README.md index 2cdcae2af..0d52507be 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ Quick start npm install htmlhint -g htmlhint -V htmlhint --help - htmlhint test.html + htmlhint www + htmlhint www/test.html + htmlhint www/**/*.xhtml 2. results diff --git a/bin/htmlhint b/bin/htmlhint index 8f7f4c1be..e2662ddc1 100755 --- a/bin/htmlhint +++ b/bin/htmlhint @@ -5,6 +5,8 @@ var fs = require('fs'); var path = require('path'); var stripJsonComments = require('strip-json-comments'); var async = require('async'); +var glob = require("glob"); +var parseGlob = require('parse-glob'); var HTMLHint = require("../index").HTMLHint; var pkg = require('../package.json'); @@ -14,8 +16,8 @@ require('colors'); function map(val) { var objMap = {}; val.split(',').forEach(function(item){ - var arrItem = item.split(/\s*=\s*/); - objMap[arrItem[0]] = arrItem[1]?arrItem[1]:true; + var arrItem = item.split(/\s*=\s*/); + objMap[arrItem[0]] = arrItem[1]?arrItem[1]:true; }); return objMap; } @@ -23,19 +25,25 @@ function map(val) { program.on('--help', function(){ console.log(' Examples:'); console.log(''); + console.log(' htmlhint'); + console.log(' htmlhint www'); + console.log(' htmlhint www/test.html'); + console.log(' htmlhint www/**/*.xhtml'); console.log(' htmlhint --list'); console.log(' htmlhint --rules tag-pair,id-class-value=underline test.html'); console.log(' htmlhint --config .htmlhintrc test.html'); + console.log(' htmlhint --ignore **/build/**,**/test/**'); console.log(''); }); program .version(pkg.version) - .usage(' [options]') + .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') .parse(process.argv); if(program.list){ @@ -45,18 +53,19 @@ if(program.list){ var arrTargets = program.args; if(arrTargets.length === 0){ - arrTargets.push(process.cwd()); + arrTargets.push('.'); } hintTargets(arrTargets, { ruleset: program.rules, - json: program.json + json: program.json, + ignore: program.ignore }); // list all rules function listRules(){ var rules = HTMLHint.rules; - var rule; + var rule; console.log(' All rules:'); console.log(' =================================================='); for (var id in rules){ @@ -67,7 +76,9 @@ function listRules(){ function hintTargets(arrTargets, options){ var allFileCount = 0; + var allHintFileCount = 0; var allHintCount = 0; + var startTime = new Date().getTime(); // json mode var json = options.json; @@ -78,28 +89,27 @@ function hintTargets(arrTargets, options){ } var arrTasks = []; arrTargets.forEach(function(target){ - target = path.resolve(target); - if(fs.existsSync(target)){ - arrTasks.push(function(next){ - hintAllFiles(target, options, function(result){ - allFileCount += result.targetFileCount; - allHintCount += result.targetHintCount; - arrJson = arrJson.concat(result.arrJson); - next(); - }); + arrTasks.push(function(next){ + hintAllFiles(target, options, function(result){ + allFileCount += result.targetFileCount; + allHintFileCount += result.targetHintFileCount; + allHintCount += result.targetHintCount; + arrJson = arrJson.concat(result.arrJson); + next(); }); - } + }); }); async.series(arrTasks, function(){ if(json){ console.log(JSON.stringify(arrJson)); } else{ + var spendTime = new Date().getTime() - startTime; if(allHintCount > 0){ - console.log('%d errors in %d files'.red, allHintCount, allFileCount); + console.log('Scan %d files, found %d errors in %d files (%d ms)'.red, allFileCount, allHintCount, allHintFileCount, spendTime); } else{ - console.log('Done, without errors.'.green); + console.log('Scan %d files, without errors (%d ms).'.green, allFileCount, spendTime); } } process.exit(allHintCount > 0 ? 1: 0); @@ -108,8 +118,12 @@ function hintTargets(arrTargets, options){ // hint all files function hintAllFiles(target, options, onFinised){ + var globInfo = getGlobInfo(target); + globInfo.ignore = options.ignore; + // hint count var targetFileCount = 0; + var targetHintFileCount = 0; var targetHintCount = 0; // json mode @@ -119,7 +133,7 @@ function hintAllFiles(target, options, onFinised){ // init ruleset var ruleset = options.ruleset; if(ruleset === undefined){ - ruleset = getConfig(program.config, target, json); + ruleset = getConfig(program.config, globInfo.base, json); } // hint queue @@ -131,7 +145,7 @@ function hintAllFiles(target, options, onFinised){ arrJson.push({'file': filepath, 'messages': messages}); } else{ - console.log(' '+path.relative(process.cwd(), filepath).white); + console.log(' '+filepath.white); messages.forEach(function(hint){ var leftWindow = 40; var rightWindow = leftWindow + 20; @@ -166,28 +180,22 @@ function hintAllFiles(target, options, onFinised){ }); console.log(''); } - targetFileCount ++; + targetHintFileCount ++; targetHintCount += hintCount; } + targetFileCount ++; setImmediate(next); }, 10); // start hint var isWalkDone = false; var isHintDone = true; - var stats = fs.statSync(target); - if(stats.isDirectory()){ - walkPath(target, function onPath(filepath){ - isHintDone = false; - hintQueue.push(filepath); - }, function onFinised(){ - isWalkDone = true; - checkAllHinted(); - }); - } - else{ + walkPath(globInfo, function(filepath){ + isHintDone = false; + hintQueue.push(filepath); + }, function(){ isWalkDone = true; - hintQueue.push(target); - } + checkAllHinted(); + }); hintQueue.drain = function() { isHintDone = true; checkAllHinted(); @@ -196,6 +204,7 @@ function hintAllFiles(target, options, onFinised){ if(isWalkDone && isHintDone){ onFinised({ targetFileCount: targetFileCount, + targetHintFileCount: targetHintFileCount, targetHintCount: targetHintCount, arrJson: arrJson }); @@ -203,20 +212,53 @@ function hintAllFiles(target, options, onFinised){ } } +// split target to base & glob +function getGlobInfo(target){ + // fix windows sep + target = target.replace(/\\/g, '/'); + var globInfo = parseGlob(target); + var base = globInfo.base; + base += /\/$/.test(base) ? '' : '/'; + var pattern = globInfo.glob; + var globPath = globInfo.path; + var defaultGlob = '*.{htm,html}'; + if(globInfo.is.glob === true){ + // no basename + if(globPath.basename === ''){ + pattern += defaultGlob; + } + } + else{ + // no basename + if(globPath.basename === ''){ + pattern += '**/' + defaultGlob; + } + // detect directory + else if(fs.existsSync(target) && fs.statSync(target).isDirectory()){ + base += globPath.basename + '/'; + pattern = '**/' + defaultGlob; + } + } + return { + base: base, + pattern: pattern + }; +} + // search and load config -function getConfig(configFile, target, json){ - if(configFile === undefined){ +function getConfig(configFile, base, json){ + if(configFile === undefined && fs.existsSync(base)){ // find default config file in parent directory - if(fs.statSync(target).isDirectory() === false){ - target = path.dirname(target); + if(fs.statSync(base).isDirectory() === false){ + base = path.dirname(base); } - while(target){ - var tmpConfigFile = path.resolve(target+path.sep, '.htmlhintrc'); + while(base){ + var tmpConfigFile = path.resolve(base+path.sep, '.htmlhintrc'); if(fs.existsSync(tmpConfigFile)){ configFile = tmpConfigFile; break; } - target = target.substring(0,target.lastIndexOf(path.sep)); + base = base.substring(0,base.lastIndexOf(path.sep)); } } @@ -236,37 +278,29 @@ function getConfig(configFile, target, json){ } // walk path -function walkPath(dir, callback, onFinish) { - fs.readdir(dir, function (err, files) { - var arrTasks = []; - files.forEach(function(file){ - arrTasks.push(function(next){ - var pathname = path.join(dir, file); - fs.stat(pathname, function (err, stats) { - if(stats){ - if (stats.isDirectory()) { - if(/^(\.svn|\.git|\.build|node_modules)$/i.test(file) === false){ - walkPath(pathname, callback, next); - } - else{ - next(); - } - } else { - if(/\.html?$/i.test(file)){ - callback(path.normalize(pathname)); - } - next(); - } - } - else{ - next(); - } - }); - }); - }); - async.series(arrTasks, function(){ - onFinish && onFinish(); +function walkPath(globInfo, callback, onFinish) { + var base = globInfo.base; + var pattern = globInfo.pattern; + var ignore = globInfo.ignore; + var arrIgnores = ['**/node_modules/**']; + if(ignore){ + ignore.split(',').forEach(function(pattern){ + arrIgnores.push(pattern); }); + } + var walk = glob(pattern, { + 'cwd': base, + 'dot': false, + 'ignore': arrIgnores, + 'nodir': true, + 'strict': false, + 'silent': true + },function() { + onFinish(); + }); + walk.on('match', function(file){ + base = base.replace(/^.\//, ''); + callback(base + file); }); } diff --git a/package.json b/package.json index 3581a8103..36b6a3703 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "colors": "1.0.3", "commander": "2.6.0", "csslint": "0.10.0", + "glob": "5.0.15", "jshint": "2.8.0", + "parse-glob": "3.0.4", "strip-json-comments": "1.0.4" }, "devDependencies": {