var _ = require('lodash'); var fs = require('fs'); var os = require('os'); var path = require('path'); var semver = require('semver'); /*jshint -W079 */ var Filter = require('./filter'); var Parser = require('./parser'); var Worker = require('./worker'); var PluginLoader = require('./plugin_loader'); var FileError = require('./errors/file_error'); var ParserError = require('./errors/parser_error'); var WorkerError = require('./errors/worker_error'); // const var SPECIFICATION_VERSION = '0.3.0'; var defaults = { excludeFilters: [], includeFilters: [ '.*\\.(clj|cls|coffee|cpp|cs|dart|erl|exs?|go|groovy|ino?|java|js|jsx|litcoffee|lua|p|php?|pl|pm|py|rb|scala|ts|vue)$' ], src: path.join(__dirname, '../example/'), filters: {}, languages: {}, parsers: {}, workers: {}, lineEnding: detectLineEnding(), encoding: 'utf8' }; var app = { options : {}, // see defaults log : logger, generator : {}, packageInfos: {}, markdownParser: false, filters: { apierror : './filters/api_error.js', apiheader : './filters/api_header.js', apiparam : './filters/api_param.js', apisuccess : './filters/api_success.js' }, languages: { '.clj' : './languages/clj.js', '.coffee' : './languages/coffee.js', '.erl' : './languages/erl.js', '.ex' : './languages/ex.js', '.exs' : './languages/ex.js', '.litcoffee' : './languages/coffee.js', '.lua' : './languages/lua.js', '.pl' : './languages/pm.js', '.pm' : './languages/pm.js', '.py' : './languages/py.js', '.rb' : './languages/rb.js', 'default' : './languages/default.js' }, parsers: { api : './parsers/api.js', apidefine : './parsers/api_define.js', apidescription : './parsers/api_description.js', apierror : './parsers/api_error.js', apierrorexample : './parsers/api_error_example.js', apiexample : './parsers/api_example.js', apiheader : './parsers/api_header.js', apiheaderexample : './parsers/api_header_example.js', apigroup : './parsers/api_group.js', apiname : './parsers/api_name.js', apiparam : './parsers/api_param.js', apiparamexample : './parsers/api_param_example.js', apipermission : './parsers/api_permission.js', apisuccess : './parsers/api_success.js', apisuccessexample : './parsers/api_success_example.js', apiuse : './parsers/api_use.js', apiversion : './parsers/api_version.js', apisamplerequest : './parsers/api_sample_request.js', apideprecated : './parsers/api_deprecated.js' }, workers: { apierrorstructure : './workers/api_error_structure.js', apierrortitle : './workers/api_error_title.js', apigroup : './workers/api_group.js', apiheaderstructure : './workers/api_header_structure.js', apiheadertitle : './workers/api_header_title.js', apiname : './workers/api_name.js', apiparamtitle : './workers/api_param_title.js', apipermission : './workers/api_permission.js', apisamplerequest : './workers/api_sample_request.js', apistructure : './workers/api_structure.js', apisuccessstructure : './workers/api_success_structure.js', apisuccesstitle : './workers/api_success_title.js', apiuse : './workers/api_use.js' }, hooks: {}, addHook: addHook, hook: applyHook }; var defaultGenerator = { name : 'apidoc', time : new Date(), url : 'http://apidocjs.com', version: '0.0.0' }; // TODO: find abetter name for PackageInfos (-> apidoc-conf) var defaultPackageInfos = { description: '', name : '', sampleUrl : false, version : '0.0.0', defaultVersion: '0.0.0' }; // Simple logger interace var logger = { debug : function() { console.log(arguments); }, verbose: function() { console.log(arguments); }, info : function() { console.log(arguments); }, warn : function() { console.log(arguments); }, error : function() { console.log(arguments); } }; /** * Return the used specification version * * @returns {String} */ function getSpecificationVersion() { return SPECIFICATION_VERSION; } /** * Detect and return OS specific line ending. * * @returns {String} */ function detectLineEnding() { if ( os.platform() === 'win32' ) return '\r\n'; if ( os.platform() === 'darwin' ) return '\r'; return '\n'; } /** * Parser * * @param {Object} options Overwrite default options. * @param {Object} logger Logger (with methods: debug, verbose, info, warn and error is necessary). * @returns {Mixed} true = ok, but nothing todo | false = error | Object with parsed data and project-informations. * { * data : { ... } * project: { ... } * } */ function parse(options) { options = _.defaults({}, options, defaults); // extend with custom functions app.filters = _.defaults({}, options.filters, app.filters); app.languages = _.defaults({}, options.languages, app.languages); app.parsers = _.defaults({}, options.parsers, app.parsers); app.workers = _.defaults({}, options.workers, app.workers); app.hooks = _.defaults({}, options.hooks, app.hooks); // options app.options = options; // generator app.generator = _.defaults({}, app.generator, defaultGenerator); // packageInfos app.packageInfos = _.defaults({}, app.packageInfos, defaultPackageInfos); var parsedFiles = []; var parsedFilenames = []; try { // Log version information var filename = path.join(__dirname, '../', './package.json'); var packageJson = JSON.parse( fs.readFileSync( filename , 'utf8') ); app.log.verbose('apidoc-generator name: ' + app.generator.name); app.log.verbose('apidoc-generator version: ' + app.generator.version); app.log.verbose('apidoc-core version: ' + packageJson.version); app.log.verbose('apidoc-spec version: ' + getSpecificationVersion()); new PluginLoader(app); var parser = new Parser(app); var worker = new Worker(app); var filter = new Filter(app); // Make them available for plugins app.parser = parser; app.worker = worker; app.filter = filter; // if input option for source is an array of folders, // parse each folder in the order provided. app.log.verbose('run parser'); if (options.src instanceof Array) { options.src.forEach(function(folder) { // Keep same options for each folder, but ensure the 'src' of options // is the folder currently being processed. var folderOptions = options; folderOptions.src = path.join(folder, './'); parser.parseFiles(folderOptions, parsedFiles, parsedFilenames); }); } else { // if the input option for source is a single folder, parse as usual. options.src = path.join(options.src, './'); parser.parseFiles(options, parsedFiles, parsedFilenames); } if (parsedFiles.length > 0) { // process transformations and assignments app.log.verbose('run worker'); worker.process(parsedFiles, parsedFilenames, app.packageInfos); // cleanup app.log.verbose('run filter'); var blocks = filter.process(parsedFiles, parsedFilenames); // sort by group ASC, name ASC, version DESC blocks.sort(function(a, b) { var nameA = a.group + a.name; var nameB = b.group + b.name; if (nameA === nameB) { if (a.version === b.version) return 0; return (semver.gte(a.version, b.version)) ? -1 : 1; } return (nameA < nameB) ? -1 : 1; }); // add apiDoc specification version app.packageInfos.apidoc = SPECIFICATION_VERSION; // add apiDoc specification version app.packageInfos.generator = app.generator; // api_data var apiData = JSON.stringify(blocks, null, 2); apiData = apiData.replace(/(\r\n|\n|\r)/g, app.options.lineEnding); // api_project var apiProject = JSON.stringify(app.packageInfos, null, 2); apiProject = apiProject.replace(/(\r\n|\n|\r)/g, app.options.lineEnding); return { data : apiData, project: apiProject }; } return true; } catch(e) { // display error by instance var extra; var meta = {}; if (e instanceof FileError) { meta = { 'Path': e.path }; app.log.error(e.message, meta); } else if (e instanceof ParserError) { extra = e.extra; if (e.source) extra.unshift({ 'Source': e.source }); if (e.element) extra.unshift({ 'Element': '@' + e.element }); if (e.block) extra.unshift({ 'Block': e.block }); if (e.file) extra.unshift({ 'File': e.file }); extra.forEach(function(obj) { var key = Object.keys(obj)[0]; meta[key] = obj[key]; }); app.log.error(e.message, meta); } else if (e instanceof WorkerError) { extra = e.extra; if (e.definition) extra.push({ 'Definition': e.definition }); if (e.example) extra.push({ 'Example': e.example }); extra.unshift({ 'Element': '@' + e.element }); extra.unshift({ 'Block': e.block }); extra.unshift({ 'File': e.file }); extra.forEach(function(obj) { var key = Object.keys(obj)[0]; meta[key] = obj[key]; }); app.log.error(e.message, meta); } else { app.log.error(e.message); if (e.stack) app.log.debug(e.stack); } return false; } } /** * Set generator informations. * * @param {Object} [generator] Generator informations. * @param {String} [generator.name] Generator name (UI-Name). * @param {String} [generator.time] Time for the generated doc * @param {String} [generator.version] Version (semver) of the generator, e.g. 1.2.3 * @param {String} [generator.url] Url to the generators homepage */ function setGeneratorInfos(generator) { app.generator = generator; } /** * Set a logger. * * @param {Object} logger A Logger (@see https://github.com/flatiron/winston for details) * Interface: * debug(msg, meta) * verbose(msg, meta) * info(msg, meta) * warn(msg, meta) * error(msg, meta) */ function setLogger(logger) { app.log = logger; } /** * Set the markdown parser. * * @param {Object} [markdownParser] Markdown parser. */ function setMarkdownParser(markdownParser) { app.markdownParser = markdownParser; } /** * Set package infos. * * @param {Object} [packageInfos] Collected from apidoc.json / package.json. * @param {String} [packageInfos.name] Project name. * @param {String} [packageInfos.version] Version (semver) of the project, e.g. 1.0.27 * @param {String} [packageInfos.description] A short description. * @param {String} [packageInfos.sampleUrl] @see http://apidocjs.com/#param-api-sample-request */ function setPackageInfos(packageInfos) { app.packageInfos = packageInfos; } /** * Register a hook function. * * @param {String} name Name of the hook. Hook overview: https://github.com/apidoc/apidoc-core/hooks.md * @param {Function} func Callback function. * @param {Integer} [priority=100] Hook priority. Lower value will be executed first. * Same value overwrite a previously defined hook. */ function addHook(name, func, priority) { priority = priority || 100; if ( ! app.hooks[name]) app.hooks[name] = []; app.log.debug('add hook: ' + name + ' [' + priority + ']'); // Find position and overwrite same priority var replace = 0; var pos = 0; app.hooks[name].forEach( function(entry, index) { if (priority === entry.priority) { pos = index; replace = 1; } else if (priority > entry.priority) { pos = index + 1; } }); app.hooks[name].splice(pos, replace, { func: func, priority: priority }); } /** * Execute a hook. */ function applyHook(name /* , ...args */) { if ( ! app.hooks[name]) return Array.prototype.slice.call(arguments, 1, 2)[0]; var args = Array.prototype.slice.call(arguments, 1); app.hooks[name].forEach( function(hook) { hook.func.apply(this, args); }); return args[0]; } module.exports = { getSpecificationVersion: getSpecificationVersion, parse : parse, setGeneratorInfos : setGeneratorInfos, setLogger : setLogger, setMarkdownParser : setMarkdownParser, setPackageInfos : setPackageInfos };