/**
* Copyright (c) 2011, Yahoo! Inc. All rights reserved.
* Code licensed under the BSD License:
* https://github.com/yui/yuidoc/blob/master/LICENSE
*/
'use strict';
var fs = require('graceful-fs');
var rimraf = require('rimraf');
var path = require('path');
/**
This is the __module__ description for the `YUIDoc` module.
var options = {
paths: [ './lib' ],
outdir: './out'
};
var Y = require('yuidocjs');
var json = (new Y.YUIDoc(options)).run();
@class YUIDoc
@main yuidoc
*/
YUI.add('yuidoc', function (Y) {
/**
* The default list of configuration options
* @property OPTIONS
* @type Object
* @final
* @for YUIDoc
*/
var OPTIONS = {
quiet: false,
writeJSON: true,
outdir: path.join(process.cwd(), 'out'),
extension: '.js',
exclude: '.DS_Store,.svn,CVS,.git,build_rollup_tmp,build_tmp,node_modules',
norecurse: false,
version: '0.1.0',
paths: [],
themedir: path.join(__dirname, 'themes', 'default'),
syntaxtype: 'js'
};
/**
* YUIDoc main class
var options = {
paths: [ './lib' ],
outdir: './out'
};
var Y = require('yuidoc');
var json = (new Y.YUIDoc(options)).run();
* @class YUIDoc
* @module yuidoc
* @constructor
* @param config The config object
*/
Y.YUIDoc = function (config) {
/**
* Holds the number of files that we are processing.
* @property filecount
* @type Boolean
* @private
*/
this.filecount = 0;
/**
* Hash map of dirnames to selleck config options.
* @property selleck
* @type Object
* @private
*/
this.selleck = {};
/**
* Holder for the list of files we are processing.
* @property filemap
* @type Object
* @private
*/
this.filemap = {};
/**
* Holder for the list of directories we are processing.
* @property dirmap
* @type Object
* @private
*/
this.dirmap = {};
/**
* Internal holder for configuration options.
* @property options
* @type Object
* @private
*/
this.options = Y.merge(OPTIONS, config);
if (this.options.quiet) {
Y.applyConfig({
debug: false
});
}
};
Y.YUIDoc.prototype = {
/**
* Always exclude these directories
* @method _setDefaultExcludes
* @private
*/
_setDefaultExcludes: function () {
//These should always be excluded
var excludes = '.DS_Store,.svn,CVS,.git,build_rollup_tmp,build_tmp,node_modules'.split(','),
self = this;
excludes.forEach(function (item) {
self.options.excludes[item] = true;
});
},
/**
* Does post process on self.options.
* @method _processConfig
* @private
*/
_processConfig: function () {
this.options.extensions = Y.Array.hash(this.options.extension.split(','));
this.options.excludes = Y.Array.hash(this.options.exclude.split(','));
this._setDefaultExcludes();
},
/**
* Walks the paths and parses the directory contents
* @method walk
* @private
*/
walk: function () {
Y.each(this.options.paths, function (dir) {
this.parsedir(dir);
}, this);
},
/**
* Walks the passed directory and grabs all the files recursively.
* @method parsedir
* @param {String} dir The directory to parse the contents of.
* @private
*/
parsedir: function (dir) {
if (!Y.Files.isDirectory(dir)) {
throw ('Can not find directory: ' + dir);
}
var allfiles = fs.readdirSync(dir),
stats,
files = [],
fullpath, self = this;
if (dir in self.options.excludes) {
return;
}
allfiles = allfiles.sort();
Y.each(allfiles, function (filename) {
if (!(filename in self.options.excludes)) {
fullpath = path.join(dir, filename);
stats = fs.statSync(fullpath);
if (stats.isDirectory() && !self.options.norecurse) {
self.parsedir(fullpath);
} else {
files.push(filename);
}
}
});
if (!(dir in self.options.excludes)) {
this.parsefiles(dir, files);
}
},
/**
* Gathers all the file data and populates the filemap and dirmap hashes.
* @method parsefiles
* @param {String} dir The directory to start from.
* @param {Array} files List of files to parse.
* @private
*/
parsefiles: function (dir, files) {
var self = this;
files = files.sort();
Y.each(files, function (filename) {
var ext = path.extname(filename),
text,
fullpath;
if (ext) {
if (ext in self.options.extensions) {
fullpath = path.join(dir, filename);
if (Y.Files.exists(fullpath)) {
self.filecount++;
text = fs.readFileSync(fullpath, Y.charset);
self.filemap[fullpath] = text.replace(/\r?\n|\r/g, '\n');
self.dirmap[fullpath] = dir;
self.getSelleck(fullpath);
} else {
Y.log('File skipped: ' + fullpath, 'warn', 'yuidoc');
}
}
}
});
},
getSelleck: function (fullpath) {
var self = this,
base,
comp,
json;
if (self.options.selleck) {
base = path.dirname(fullpath);
comp = path.join(base, '../', 'docs', 'component.json');
//Y.log('Checking for Selleck data: ' + comp, 'info', 'yuidoc');
if (Y.Files.exists(comp)) {
try {
json = JSON.parse(fs.readFileSync(comp, 'utf8'));
delete json.examples; //Remove the selleck example data, we only want the comp info
self.selleck[fullpath] = json;
} catch (e) {
Y.log('JSON parse failed on Selleck component.json file: ' + comp, 'error', 'yuidoc');
}
}
}
},
/**
* Applies preprocessors to the data tree.
* This function first clones the data and operates on the clone.
* @method runPreprocessors
* @private
* @return {Object} The mutated data
*/
runPreprocessors: function (data) {
var self = this,
preprocessors,
preprocessorsRelativeTo;
// We will try to load the preprocessors as npm modules, but we will also
// search for them relative to the process working directory.
// The latter is consistent with how other paths are treated by yuidoc,
// such as the config options 'paths' and 'outdir'.
preprocessorsRelativeTo = process.cwd();
if (self.options.preprocessor) {
data = JSON.parse(JSON.stringify(data));
preprocessors = [].concat(self.options.preprocessor);
preprocessors.forEach(function (preprocessor) {
var preprocessorModule;
try {
preprocessorModule = require(preprocessor);
} catch (e) {
preprocessorModule = require(path.resolve(preprocessorsRelativeTo, preprocessor));
}
preprocessorModule(data, self.options);
});
}
return data;
},
/**
* Writes the parser JSON data to disk.
* Applies preprocessors, if any.
* @method writeJSON
* @param {Object} parser The DocParser instance to use
* @private
* @return {Object} The JSON data returned from the DocParser
*/
writeJSON: function (parser) {
var self = this,
data,
file,
out;
data = parser.data;
data.warnings = parser.warnings;
data = this.runPreprocessors(data);
if (self.selleck && self.options.selleck && data.files && data.modules) {
Object.keys(self.selleck).forEach(function (selleckFile) {
Object.keys(data.files).forEach(function (f) {
if (selleckFile === f) {
var mods = data.files[f].modules;
if (mods) {
Object.keys(mods).forEach(function (mod) {
if (data.modules[mod]) {
if (!data.modules[mod].extra) {
data.modules[mod].extra = {};
}
data.modules[mod].extra.selleck = self.selleck[selleckFile];
}
});
}
}
});
});
}
if (self.options.project) {
data.project = self.options.project;
}
if (self.options.writeJSON) {
// Y.log(Y.JSON.stringify(parser.data, null, 4));
file = path.join(self.options.outdir, 'data.json');
if (Y.Files.exists(self.options.outdir) && !self.options.nodeleteout) {
Y.log('Found out dir, deleting: ' + self.options.outdir, 'warn', 'yuidoc');
rimraf.sync(self.options.outdir);
}
if (!Y.Files.exists(self.options.outdir)) {
Y.log('Making out dir: ' + self.options.outdir, 'info', 'yuidoc');
try {
fs.mkdirSync(self.options.outdir, '0777');
} catch (e) {
Y.log('Outdir creation failed', 'warn', 'yuidoc');
}
}
out = fs.createWriteStream(file, {
flags: 'w',
encoding: Y.charset,
mode: '0644'
});
out.write(JSON.stringify(data, null, 4));
out.end();
}
return data;
},
lint: function (warnings) {
var code = 0,
count = 0;
if (warnings && warnings.length) {
code = 1;
console.log('YUIDoc found', warnings.length, 'lint errors in your docs');
warnings.forEach(function (item) {
count++;
console.log('#' + count, item.message, item.line + '\n');
});
process.exit(code);
}
},
/**
* Process the config, walk the file tree and write out the JSON data.
* @method run
* @return {Object} The JSON data returned from the DocParser
*/
run: function () {
/**
* Timestamp holder so we know when YUIDoc started the parse process.
* @property starttime
* @type Timestamp
*/
Y.log('YUIDoc Starting from: ' + this.options.paths.join(','), 'info', 'yuidoc');
this.starttime = new Date().getTime();
this._processConfig();
this.walk();
var json = this.writeJSON(new Y.DocParser({
syntaxtype: this.options.syntaxtype,
filemap: this.filemap,
dirmap: this.dirmap
}).parse());
if (this.options.lint) {
this.lint(json.warnings);
return null;
}
/**
* Timestamp holder so we know when YUIDoc has finished the parse process.
* @property endtime
* @type Timestamp
*/
this.endtime = new Date().getTime();
Y.log('Parsed ' + this.filecount + ' files in ' + ((this.endtime - this.starttime) / 1000) + ' seconds', 'info', 'yuidoc');
return json;
}
};
});