diff --git a/.jscsrc b/.jscsrc index 83287a3..8b12ca9 100644 --- a/.jscsrc +++ b/.jscsrc @@ -68,5 +68,7 @@ "requireDotNotation": true, "disallowYodaConditions": true, "disallowNewlineBeforeBlockStatements": true, - "validateLineBreaks": "LF" + "validateLineBreaks": "LF", + "requireSpaceAfterLineComment": true, + "safeContextKeyword": "self" } diff --git a/jsdox.js b/jsdox.js index 1f746a5..61c096b 100644 --- a/jsdox.js +++ b/jsdox.js @@ -15,42 +15,16 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER I OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'); var fs = require('fs'); var path = require('path'); var q = require('q'); +var mkdirp = require('mkdirp'); + var packageJson = require('./package.json'); -var jsdocParser = require('jsdoc3-parser'); -var analyze = require('./lib/analyze'); +var generateForDir = require('./lib/generateForDir'); +var generateForFile = require('./lib/generateForFile'); var generateMD = require('./lib/generateMD'); -var index = { - classes: [], - functions: [] -}; - -/** - * Whether or not to print debug information. - * Global to this module. - * - * @type {Boolean} - */ -var debug = false; - -/** - * Cache of the optimist arguments list - * - * @type {Object} - */ -var argv; - -/** - * Pretty print utility - * @param {Object} ast [description] - * @return {String} - */ -function inspect(ast) { - return util.inspect(ast, false, 20); -} +var util = require('./lib/util'); function printHelp() { console.log('Usage:\tjsdox [options] '); @@ -76,172 +50,11 @@ function printVersion() { } /** - * @param {String} filename - * @param {String} destination - * @param {String} templateDir - * @param {Function} cb - * @param {Function} fileCb - */ -function generateForDir(filename, destination, templateDir, cb, fileCb) { - var waiting = 0; - var touched = 0; - var error = null; - - var readdirSyncRec = function(dir, filelist) { - var files = fs.readdirSync(dir); - filelist = filelist || []; - files.forEach(function(file) { - if (fs.statSync(path.join(dir, file)).isDirectory()) { - filelist = readdirSyncRec(path.join(dir, file), filelist); - } else { - filelist.push(path.join(dir, file)); - } - }); - return filelist; - }; - - function oneFile(directory, file, cb) { - var fullpath; - if (argv.rr) { - fullpath = path.join(path.join(destination, path.dirname(file)), path.basename(file)); - } else { - fullpath = path.join(destination, file); - } - fullpath = fullpath.replace(/\.js$/, '.md'); - - if (debug) { - console.log('Generating', fullpath); - } - - waiting++; - - jsdocParser(path.join(directory, path.basename(file)), function(err, result) { - if (err) { - console.error('Error generating docs for file', file, err); - waiting--; - if (!waiting) { - return cb(err); - } else { - error = err; - } - } - - if (debug) { - console.log(file + ' AST: ', util.inspect(result, false, 20)); - console.log(file + ' Analyzed: ', util.inspect(analyze(result), false, 20)); - } - - var data = analyze(result, argv); - var output = generateMD(data, templateDir); - - if (argv.index) { - for (var i = 0; i < data.functions.length; i++) { - if (data.functions[i].className === undefined) { - var toAddFct = data.functions[i]; - toAddFct.file = path.relative(destination, fullpath); - toAddFct.sourcePath = path.relative(destination, path.join(directory, path.basename(file))); - index.functions.push(toAddFct); - } - } - for (var j = 0; j < data.classes.length; j++) { - if (data.functions[j].className === undefined) { - var toAddClass = data.classes[j]; - toAddClass.file = path.relative(destination, fullpath); - toAddClass.sourcePath = path.relative(destination, path.join(directory, path.basename(file))); - index.classes.push(toAddClass); - } - } - } - - if (output) { - fileCb && fileCb(file, data); - fs.writeFile(fullpath, output, function(err) { - waiting--; - if (err) { - console.error('Error generating docs for file', file, err); - error = err; - } - if (!waiting) { - return cb(error); - } - }); - - } else { - waiting--; - if (!waiting) { - return cb(error); - } - } - }); - } - - if (filename.match(/\.js$/)) { - oneFile(path.dirname(filename), path.basename(filename), cb); - - } else { - if (argv.recursive || argv.rr) { - fs.stat(filename, function (err, s) { - if (!err && s.isDirectory()) { - var contentList = readdirSyncRec(filename); - contentList.forEach(function(fileFullPath) { - if (argv.rr) { - //create the sub-directories - try { - fs.mkdirSync(path.join(destination, path.dirname(fileFullPath))); - } catch(err) {} //lazy way: if the file already exists, everything is alright. - try { - oneFile(path.dirname(fileFullPath), fileFullPath, cb), touched++; - } catch(err) { - console.error('Error generating docs for files', path.basename(fileFullPath), err); - return cb(err); - } - } else { - try { - oneFile(path.dirname(fileFullPath), path.basename(fileFullPath), cb), touched++; - } catch(err) { - console.error('Error generating docs for files', path.basename(fileFullPath), err); - return cb(err); - } - } - }); - if (!touched) { - cb(); - } - - } else { - cb(); - } - }); - } else { - fs.stat(filename, function (err, s) { - if (!err && s.isDirectory()) { - fs.readdir(filename, function (err, files) { - if (err) { - console.error('Error generating docs for files', filename, err); - return cb(err); - } - files.forEach(function (file) { - if (file.match(/\.js$/)) { - oneFile(filename, file, cb), touched++; - } - }); - if (!touched) { - cb(); - } - }); - } else { - cb(); - } - }); - } - } -} - -/** - * @param {String} file + * @param {Object} argv * @param {Function} callback */ -function loadConfigFile(file, argv, callback) { +function loadConfigFile(argv, callback) { + var file = argv.config; var config; // Check to see if file exists @@ -273,53 +86,51 @@ function loadConfigFile(file, argv, callback) { } function main(argv) { - if (typeof argv._[0] !== 'undefined') { - fs.mkdir(argv.output, function() { - q.all(argv._.map(function(file) { - var deferred = q.defer(); - - generateForDir(file, argv.output, argv.templateDir, function(err) { - if (err) { - console.error(err); - throw err; - } - deferred.resolve(); - }); + console.log(argv) - return deferred.promise; - })) - .then(function() { - //create index - if (argv.index) { - var fileName; - if (argv.index === true) { - fileName = 'index'; - } else { - fileName = argv.index; - } - if (typeof argv.output === 'string') { - fileName = path.join(argv.output, fileName); - } else { - fileName = path.join('output', fileName); - } - fs.writeFileSync(fileName + '.md', generateMD(index, argv.templateDir, true)); - } - }) - .then(function () { - console.log('jsdox completed'); - }); - }); - } else { + if (!argv._.length) { console.error('Error missing input file or directory.'); printHelp(); + return; + } + + // @todo: support input being a directory and output being a directory + // @todo: support input being a file and output being a file + + if (util.isDirectoryPath(argv.output)) { + try { + mkdirp.sync(argv.output); + } catch (err) {} } -} -function jsdox(args) { - argv = args; - debug = !!argv.debug; + q.all(argv._.map(function(filename) { + var options = { + filename: filename, + argv: argv + }; + + return util.isDirectory(filename) ? + generateForDir(options) : + generateForFile(options); + })) + .then(function() { + // Create index + if (argv.index) { + var fileName = argv.index === true ? 'index' : argv.index; + fileName = typeof argv.output === 'string' ? + path.join(argv.output, fileName) : + path.join('output', fileName); + + fs.writeFileSync(fileName + '.md', generateMD(generateForDir.index, argv.templateDir, true)); + } + }) + .then(function () { + console.log('jsdox completed'); + }); +} +function jsdox(argv) { if (argv.help) { printHelp(); } @@ -329,14 +140,14 @@ function jsdox(args) { } if (argv.config) { - // @todo: refactor to not rely on argv - loadConfigFile(argv.config, argv, main); + loadConfigFile(argv, main); } else { main(argv); } } -exports.analyze = analyze; -exports.generateMD = generateMD; -exports.generateForDir = generateForDir; +exports.analyze = require('./lib/analyze'); +exports.generateMD = require('./lib/generateMD'); +exports.generateForDir = require('./lib/generateForDir'); +exports.generateForFile = require('./lib/generateForFile'); exports.jsdox = jsdox; diff --git a/lib/generateForDir.js b/lib/generateForDir.js new file mode 100644 index 0000000..7ab4776 --- /dev/null +++ b/lib/generateForDir.js @@ -0,0 +1,83 @@ +var util = require('./util'); +var path = require('path'); +var fs = require('fs'); +var q = require('q'); +var dir = require('node-dir'); +var mkdirp = require('mkdirp'); + +var generateForFile = require('./generateForFile'); + +/** + * @param {Object} options + * @param {String} options.filename + * @param {String} options.destination + * @param {String} options.templateDir + * @param {Function} options.fileCb + * @returns {Promise} + * + * @todo: Support options instead of separate params. Be sure to update grunt-jsdox + */ +module.exports = function(options) { + var deferred = q.defer(); + + var filename = options.filename; + var fileDir = path.dirname(filename); + var output = options.output; + var templateDir = options.templateDir; + var fileCb = options.fileCb; + var argv = options.argv; + + var self = this; + + // Aggregated index data about the directory + this.index = { + classes: [], + functions: [] + }; + + var aggregateIndexData = function(file, data) { + var index; + if (argv.index) { + index = generateIndexData(data); + self.index.classes = self.index.classes.concat(index.classes); + self.index.functions = self.index.functions.concat(index.functions); + } + } + + // If it's just a file and not a directory + if (filename.match(/\.js$/)) { + generateForFile({ + directory: fileDir, + file: path.basename(filename), + argv: argv + }) + .then(aggregateIndexData) + .then(deferred.resolve.bind(deferred)); + + } else { + dir.readFiles(directory, + { + match: /.js$/, + exclude: /^\./, + }, + function (err, content, filename, next) { + next(); + }, + function(err, files) { + q.all(files.map(function(filename) { + mkdirp.sync(path.join(output, path.dirname(filename))); + + return generateForFile({ + directory: fileDir, + file: filename, + argv: argv, + templateDir: templateDir + }).then(aggregateIndexData); + })) + .then(deferred.resolve.bind(deferred)); + }); + } + + return deferred.promise; +}; + diff --git a/lib/generateForFile.js b/lib/generateForFile.js new file mode 100644 index 0000000..747c81d --- /dev/null +++ b/lib/generateForFile.js @@ -0,0 +1,85 @@ +var path = require('path'); +var jsdocParser = require('jsdoc3-parser'); +var fs = require('fs'); +var q = require('q'); + +var util = require('./util'); +var analyze = require('./analyze'); +var generateMD = require('./generateMD'); + +/** + * Generates the markdown output for a single JS file + * + * @param {Object} options + * @param {String} options.directory + * @param {String} options.file + * @param {Object} options.argv + * @param {String} options.templateDir + * @return {Promise} Resolves with the file {String} and the analyzed ast {Object} + */ +module.exports = function(options) { + console.log(options) + + var deferred = q.defer(); + var filename = options.filename; + var fileDir = path.dirname(filename); + var fileBase = path.basename(filename); + var argv = options.argv; + var outputPath; + var outputDir; + + // Lazy way to check if it's a directory without + // hitting the filesystem, in case the output dir doesn't exist yet + if (path.dirname(argv.output) === argv.output) { + // Markdown should be in the proper subdirectory of the output dir + if (argv.recursive) { + outputPath = path.join(path.join(argv.output, fileDir), fileBase); + + // Just dump the markdown into the output folder + } else { + outputPath = path.join(argv.output, fileBase); + } + + // We're storing the markdown in a separate file + } else { + outputPath = argv.output; + } + + console.log('fullpath: ', outputPath) + outputPath = outputPath.replace(/\.js$/, '.md'); + + if (argv.debug) { + console.log('Generating', outputPath); + } + + jsdocParser(filename, function(err, result) { + if (err) { + console.error('Error generating docs for file', filename, err); + deferred.reject(err); + return; + } + + if (options.debug) { + console.log(file + ' AST: ', util.inspect(result)); + console.log(file + ' Analyzed: ', util.inspect(analyze(result))); + } + + var data = analyze(result, argv); + var output = generateMD(data, options.templateDir); + + try { + if (output) { + fs.writeFileSync(outputPath, output); + } + + deferred.resolve(filename, data); + return; + + } catch (err) { + console.error('Error generating docs for file', filename, err); + deferred.reject(err); + } + }); + + return deferred.promise; +}; diff --git a/lib/generateIndex.js b/lib/generateIndex.js new file mode 100644 index 0000000..381f4ec --- /dev/null +++ b/lib/generateIndex.js @@ -0,0 +1,29 @@ +/** + * Aggregates functions and classes for an index page + * @param {Object} data - Analyzed ast + * @return {Object} Index data + */ +module.exports = function(data) { + var index = { + classes: [], + functions: [] + }; + + data.functions.forEach(function(toAddFct) { + if (toAddFct.className === undefined) { + toAddFct.file = path.relative(destination, fullpath); + toAddFct.sourcePath = path.relative(destination, path.join(directory, path.basename(file))); + index.functions.push(toAddFct); + } + }); + + data.classes.forEach(function(toAddClass) { + if (toAddClass.className === undefined) { + toAddClass.file = path.relative(destination, fullpath); + toAddClass.sourcePath = path.relative(destination, path.join(directory, path.basename(file))); + index.classes.push(toAddClass); + } + }); + + return index; +}; diff --git a/lib/generateMD.js b/lib/generateMD.js index cc63236..37e1def 100644 --- a/lib/generateMD.js +++ b/lib/generateMD.js @@ -1,6 +1,5 @@ /** * Copyright (c) 2012-2014 Sutoiku - * */ var Mustache = require('mustache'); @@ -9,7 +8,7 @@ var path = require('path'); /** * Renders markdown from the given analyzed AST - * @param {Object} ast - output from analyze() + * @param {Object} ast * @param {String} templateDir - templates directory (optional) * @return {String} Markdown output */ @@ -24,38 +23,33 @@ module.exports = function(ast, templateDir, isIndex) { templateDir = templateDir.replace(/\\/g, '/'); } - //if ast is an index file, we need to sort the contents and to use the right templates; + // If ast is an index file, we need to sort the contents and to use the right templates if (isIndex) { console.log('Now generating index'); + ast.classes.sort(function(a, b) { - if (a.name < b.name) { - return -1; - } else { - return 1; - } + return a.name < b.name ? -1 : 1; }); + ast.functions.sort(function(a, b) { - if (a.name < b.name) { - return -1; - } else { - return 1; - } + return a.name < b.name ? -1 : 1; }); templates = { index: fs.readFileSync(templateDir + '/index.mustache', 'utf8'), - class: fs.readFileSync(templateDir + '/overview.mustache', 'utf8'),//do we need different overview templates for functions or classes here ? + class: fs.readFileSync(templateDir + '/overview.mustache', 'utf8'), function: fs.readFileSync(templateDir + '/overview.mustache', 'utf8') }; - return Mustache.render(templates.index, ast, templates); - }else { + return Mustache.render(templates.index, ast, templates); + } else { templates = { file: fs.readFileSync(templateDir + '/file.mustache', 'utf8'), class: fs.readFileSync(templateDir + '/class.mustache', 'utf8'), function: fs.readFileSync(templateDir + '/function.mustache', 'utf8') }; + return Mustache.render(templates.file, ast, templates); } }; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..95723bb --- /dev/null +++ b/lib/util.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2012-2014 Sutoiku + */ + +var fs = require('fs'); +var util = require('util'); +var path = require('path'); + +/** + * Recursive readdirSync + * @param {String} dir + * @param {String[]} filelist + * @return {String[]} + */ +module.exports.readdirSyncRec = function(dir, filelist) { + var files = fs.readdirSync(dir); + var self = this; + + filelist = filelist || []; + files.forEach(function(file) { + if (fs.statSync(path.join(dir, file)).isDirectory()) { + filelist = self.readdirSyncRec(path.join(dir, file), filelist); + } else { + filelist.push(path.join(dir, file)); + } + }); + return filelist; +}; + +/** + * Pretty print utility + * + * @param {Object} ast [description] + * @return {String} + */ +module.exports.inspect = function(ast) { + return util.inspect(ast, false, 20); +}; + +/** + * Shallow copy + * @param {Object} obj1 + * @param {Object} obj2 + */ +module.exports.extend = function(obj1, obj2) { + Object.keys(obj2).forEach(function(prop) { + obj1[prop] = obj2[prop]; + }); +}; + +/** + * Whether or not the given path represents a directory name + * The directory name does not have to be of a directory on the filesystem + * @param {String} path + * @return {Boolean} + */ +module.exports.isDirectoryPath = function(path) { + return path.dirname(path) === path; +}; + +/** + * Whether or not the given resource is a directory on the filesystem + * @param {String} path + * @return {Boolean} + */ +module.exports.isDirectory = function(path) { + return fs.lstatSync(path).isDirectory(); +}; \ No newline at end of file diff --git a/package.json b/package.json index 6d05505..86488ce 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "jsdoc3-parser": "^1.0.0", "mustache": "^0.8.2", "optimist": "~0.3.4", - "q": "^1.0.1" + "q": "^1.0.1", + "node-dir": "^0.1.6", + "mkdirp": "^0.5.0" }, "devDependencies": { "expect.js": "^0.3.1", diff --git a/test/jsdox.js b/test/jsdox.js index dfed0fe..eae0126 100644 --- a/test/jsdox.js +++ b/test/jsdox.js @@ -28,7 +28,7 @@ describe('jsdox', function() { it('generates non-empty output markdown files from the fixtures/ and the fixtures/under files', function(done) { var cmd = bin + ' fixtures/ -o sample_output -r'; - //in case an old index.md is here + // In case an old index.md is here try { fs.unlinkSync('sample_output/index.md'); } catch(err) {} @@ -64,7 +64,7 @@ describe('jsdox', function() { var content = fs.readFileSync('sample_output/fixtures/' + outputFile).toString(); expect(content).not.to.be.empty(); nbFilesA += 1; - //clean for future tests + // Clean for future tests fs.unlinkSync('sample_output/fixtures/' + outputFile); } } @@ -103,7 +103,7 @@ describe('jsdox', function() { }); expect(nbFiles).to.be(7); expect(hasIndex).to.be(true); - //clean index for other tests + // Clean index for other tests fs.unlinkSync('sample_output/index.md'); done();