index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. var _ = require('lodash');
  2. var fs = require('fs');
  3. var os = require('os');
  4. var path = require('path');
  5. var semver = require('semver');
  6. /*jshint -W079 */
  7. var Filter = require('./filter');
  8. var Parser = require('./parser');
  9. var Worker = require('./worker');
  10. var PluginLoader = require('./plugin_loader');
  11. var FileError = require('./errors/file_error');
  12. var ParserError = require('./errors/parser_error');
  13. var WorkerError = require('./errors/worker_error');
  14. // const
  15. var SPECIFICATION_VERSION = '0.3.0';
  16. var defaults = {
  17. excludeFilters: [],
  18. 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)$' ],
  19. src: path.join(__dirname, '../example/'),
  20. filters: {},
  21. languages: {},
  22. parsers: {},
  23. workers: {},
  24. lineEnding: detectLineEnding(),
  25. encoding: 'utf8'
  26. };
  27. var app = {
  28. options : {}, // see defaults
  29. log : logger,
  30. generator : {},
  31. packageInfos: {},
  32. markdownParser: false,
  33. filters: {
  34. apierror : './filters/api_error.js',
  35. apiheader : './filters/api_header.js',
  36. apiparam : './filters/api_param.js',
  37. apisuccess : './filters/api_success.js'
  38. },
  39. languages: {
  40. '.clj' : './languages/clj.js',
  41. '.coffee' : './languages/coffee.js',
  42. '.erl' : './languages/erl.js',
  43. '.ex' : './languages/ex.js',
  44. '.exs' : './languages/ex.js',
  45. '.litcoffee' : './languages/coffee.js',
  46. '.lua' : './languages/lua.js',
  47. '.pl' : './languages/pm.js',
  48. '.pm' : './languages/pm.js',
  49. '.py' : './languages/py.js',
  50. '.rb' : './languages/rb.js',
  51. 'default' : './languages/default.js'
  52. },
  53. parsers: {
  54. api : './parsers/api.js',
  55. apidefine : './parsers/api_define.js',
  56. apidescription : './parsers/api_description.js',
  57. apierror : './parsers/api_error.js',
  58. apierrorexample : './parsers/api_error_example.js',
  59. apiexample : './parsers/api_example.js',
  60. apiheader : './parsers/api_header.js',
  61. apiheaderexample : './parsers/api_header_example.js',
  62. apigroup : './parsers/api_group.js',
  63. apiname : './parsers/api_name.js',
  64. apiparam : './parsers/api_param.js',
  65. apiparamexample : './parsers/api_param_example.js',
  66. apipermission : './parsers/api_permission.js',
  67. apisuccess : './parsers/api_success.js',
  68. apisuccessexample : './parsers/api_success_example.js',
  69. apiuse : './parsers/api_use.js',
  70. apiversion : './parsers/api_version.js',
  71. apisamplerequest : './parsers/api_sample_request.js',
  72. apideprecated : './parsers/api_deprecated.js'
  73. },
  74. workers: {
  75. apierrorstructure : './workers/api_error_structure.js',
  76. apierrortitle : './workers/api_error_title.js',
  77. apigroup : './workers/api_group.js',
  78. apiheaderstructure : './workers/api_header_structure.js',
  79. apiheadertitle : './workers/api_header_title.js',
  80. apiname : './workers/api_name.js',
  81. apiparamtitle : './workers/api_param_title.js',
  82. apipermission : './workers/api_permission.js',
  83. apisamplerequest : './workers/api_sample_request.js',
  84. apistructure : './workers/api_structure.js',
  85. apisuccessstructure : './workers/api_success_structure.js',
  86. apisuccesstitle : './workers/api_success_title.js',
  87. apiuse : './workers/api_use.js'
  88. },
  89. hooks: {},
  90. addHook: addHook,
  91. hook: applyHook
  92. };
  93. var defaultGenerator = {
  94. name : 'apidoc',
  95. time : new Date(),
  96. url : 'http://apidocjs.com',
  97. version: '0.0.0'
  98. };
  99. // TODO: find abetter name for PackageInfos (-> apidoc-conf)
  100. var defaultPackageInfos = {
  101. description: '',
  102. name : '',
  103. sampleUrl : false,
  104. version : '0.0.0',
  105. defaultVersion: '0.0.0'
  106. };
  107. // Simple logger interace
  108. var logger = {
  109. debug : function() { console.log(arguments); },
  110. verbose: function() { console.log(arguments); },
  111. info : function() { console.log(arguments); },
  112. warn : function() { console.log(arguments); },
  113. error : function() { console.log(arguments); }
  114. };
  115. /**
  116. * Return the used specification version
  117. *
  118. * @returns {String}
  119. */
  120. function getSpecificationVersion() {
  121. return SPECIFICATION_VERSION;
  122. }
  123. /**
  124. * Detect and return OS specific line ending.
  125. *
  126. * @returns {String}
  127. */
  128. function detectLineEnding() {
  129. if ( os.platform() === 'win32' )
  130. return '\r\n';
  131. if ( os.platform() === 'darwin' )
  132. return '\r';
  133. return '\n';
  134. }
  135. /**
  136. * Parser
  137. *
  138. * @param {Object} options Overwrite default options.
  139. * @param {Object} logger Logger (with methods: debug, verbose, info, warn and error is necessary).
  140. * @returns {Mixed} true = ok, but nothing todo | false = error | Object with parsed data and project-informations.
  141. * {
  142. * data : { ... }
  143. * project: { ... }
  144. * }
  145. */
  146. function parse(options) {
  147. options = _.defaults({}, options, defaults);
  148. // extend with custom functions
  149. app.filters = _.defaults({}, options.filters, app.filters);
  150. app.languages = _.defaults({}, options.languages, app.languages);
  151. app.parsers = _.defaults({}, options.parsers, app.parsers);
  152. app.workers = _.defaults({}, options.workers, app.workers);
  153. app.hooks = _.defaults({}, options.hooks, app.hooks);
  154. // options
  155. app.options = options;
  156. // generator
  157. app.generator = _.defaults({}, app.generator, defaultGenerator);
  158. // packageInfos
  159. app.packageInfos = _.defaults({}, app.packageInfos, defaultPackageInfos);
  160. var parsedFiles = [];
  161. var parsedFilenames = [];
  162. try {
  163. // Log version information
  164. var filename = path.join(__dirname, '../', './package.json');
  165. var packageJson = JSON.parse( fs.readFileSync( filename , 'utf8') );
  166. app.log.verbose('apidoc-generator name: ' + app.generator.name);
  167. app.log.verbose('apidoc-generator version: ' + app.generator.version);
  168. app.log.verbose('apidoc-core version: ' + packageJson.version);
  169. app.log.verbose('apidoc-spec version: ' + getSpecificationVersion());
  170. new PluginLoader(app);
  171. var parser = new Parser(app);
  172. var worker = new Worker(app);
  173. var filter = new Filter(app);
  174. // Make them available for plugins
  175. app.parser = parser;
  176. app.worker = worker;
  177. app.filter = filter;
  178. // if input option for source is an array of folders,
  179. // parse each folder in the order provided.
  180. app.log.verbose('run parser');
  181. if (options.src instanceof Array) {
  182. options.src.forEach(function(folder) {
  183. // Keep same options for each folder, but ensure the 'src' of options
  184. // is the folder currently being processed.
  185. var folderOptions = options;
  186. folderOptions.src = path.join(folder, './');
  187. parser.parseFiles(folderOptions, parsedFiles, parsedFilenames);
  188. });
  189. }
  190. else {
  191. // if the input option for source is a single folder, parse as usual.
  192. options.src = path.join(options.src, './');
  193. parser.parseFiles(options, parsedFiles, parsedFilenames);
  194. }
  195. if (parsedFiles.length > 0) {
  196. // process transformations and assignments
  197. app.log.verbose('run worker');
  198. worker.process(parsedFiles, parsedFilenames, app.packageInfos);
  199. // cleanup
  200. app.log.verbose('run filter');
  201. var blocks = filter.process(parsedFiles, parsedFilenames);
  202. // sort by group ASC, name ASC, version DESC
  203. blocks.sort(function(a, b) {
  204. var nameA = a.group + a.name;
  205. var nameB = b.group + b.name;
  206. if (nameA === nameB) {
  207. if (a.version === b.version)
  208. return 0;
  209. return (semver.gte(a.version, b.version)) ? -1 : 1;
  210. }
  211. return (nameA < nameB) ? -1 : 1;
  212. });
  213. // add apiDoc specification version
  214. app.packageInfos.apidoc = SPECIFICATION_VERSION;
  215. // add apiDoc specification version
  216. app.packageInfos.generator = app.generator;
  217. // api_data
  218. var apiData = JSON.stringify(blocks, null, 2);
  219. apiData = apiData.replace(/(\r\n|\n|\r)/g, app.options.lineEnding);
  220. // api_project
  221. var apiProject = JSON.stringify(app.packageInfos, null, 2);
  222. apiProject = apiProject.replace(/(\r\n|\n|\r)/g, app.options.lineEnding);
  223. return {
  224. data : apiData,
  225. project: apiProject
  226. };
  227. }
  228. return true;
  229. } catch(e) {
  230. // display error by instance
  231. var extra;
  232. var meta = {};
  233. if (e instanceof FileError) {
  234. meta = { 'Path': e.path };
  235. app.log.error(e.message, meta);
  236. } else if (e instanceof ParserError) {
  237. extra = e.extra;
  238. if (e.source)
  239. extra.unshift({ 'Source': e.source });
  240. if (e.element)
  241. extra.unshift({ 'Element': '@' + e.element });
  242. if (e.block)
  243. extra.unshift({ 'Block': e.block });
  244. if (e.file)
  245. extra.unshift({ 'File': e.file });
  246. extra.forEach(function(obj) {
  247. var key = Object.keys(obj)[0];
  248. meta[key] = obj[key];
  249. });
  250. app.log.error(e.message, meta);
  251. }
  252. else if (e instanceof WorkerError) {
  253. extra = e.extra;
  254. if (e.definition)
  255. extra.push({ 'Definition': e.definition });
  256. if (e.example)
  257. extra.push({ 'Example': e.example });
  258. extra.unshift({ 'Element': '@' + e.element });
  259. extra.unshift({ 'Block': e.block });
  260. extra.unshift({ 'File': e.file });
  261. extra.forEach(function(obj) {
  262. var key = Object.keys(obj)[0];
  263. meta[key] = obj[key];
  264. });
  265. app.log.error(e.message, meta);
  266. }
  267. else {
  268. app.log.error(e.message);
  269. if (e.stack)
  270. app.log.debug(e.stack);
  271. }
  272. return false;
  273. }
  274. }
  275. /**
  276. * Set generator informations.
  277. *
  278. * @param {Object} [generator] Generator informations.
  279. * @param {String} [generator.name] Generator name (UI-Name).
  280. * @param {String} [generator.time] Time for the generated doc
  281. * @param {String} [generator.version] Version (semver) of the generator, e.g. 1.2.3
  282. * @param {String} [generator.url] Url to the generators homepage
  283. */
  284. function setGeneratorInfos(generator) {
  285. app.generator = generator;
  286. }
  287. /**
  288. * Set a logger.
  289. *
  290. * @param {Object} logger A Logger (@see https://github.com/flatiron/winston for details)
  291. * Interface:
  292. * debug(msg, meta)
  293. * verbose(msg, meta)
  294. * info(msg, meta)
  295. * warn(msg, meta)
  296. * error(msg, meta)
  297. */
  298. function setLogger(logger) {
  299. app.log = logger;
  300. }
  301. /**
  302. * Set the markdown parser.
  303. *
  304. * @param {Object} [markdownParser] Markdown parser.
  305. */
  306. function setMarkdownParser(markdownParser) {
  307. app.markdownParser = markdownParser;
  308. }
  309. /**
  310. * Set package infos.
  311. *
  312. * @param {Object} [packageInfos] Collected from apidoc.json / package.json.
  313. * @param {String} [packageInfos.name] Project name.
  314. * @param {String} [packageInfos.version] Version (semver) of the project, e.g. 1.0.27
  315. * @param {String} [packageInfos.description] A short description.
  316. * @param {String} [packageInfos.sampleUrl] @see http://apidocjs.com/#param-api-sample-request
  317. */
  318. function setPackageInfos(packageInfos) {
  319. app.packageInfos = packageInfos;
  320. }
  321. /**
  322. * Register a hook function.
  323. *
  324. * @param {String} name Name of the hook. Hook overview: https://github.com/apidoc/apidoc-core/hooks.md
  325. * @param {Function} func Callback function.
  326. * @param {Integer} [priority=100] Hook priority. Lower value will be executed first.
  327. * Same value overwrite a previously defined hook.
  328. */
  329. function addHook(name, func, priority) {
  330. priority = priority || 100;
  331. if ( ! app.hooks[name])
  332. app.hooks[name] = [];
  333. app.log.debug('add hook: ' + name + ' [' + priority + ']');
  334. // Find position and overwrite same priority
  335. var replace = 0;
  336. var pos = 0;
  337. app.hooks[name].forEach( function(entry, index) {
  338. if (priority === entry.priority) {
  339. pos = index;
  340. replace = 1;
  341. } else if (priority > entry.priority) {
  342. pos = index + 1;
  343. }
  344. });
  345. app.hooks[name].splice(pos, replace, {
  346. func: func,
  347. priority: priority
  348. });
  349. }
  350. /**
  351. * Execute a hook.
  352. */
  353. function applyHook(name /* , ...args */) {
  354. if ( ! app.hooks[name])
  355. return Array.prototype.slice.call(arguments, 1, 2)[0];
  356. var args = Array.prototype.slice.call(arguments, 1);
  357. app.hooks[name].forEach( function(hook) {
  358. hook.func.apply(this, args);
  359. });
  360. return args[0];
  361. }
  362. module.exports = {
  363. getSpecificationVersion: getSpecificationVersion,
  364. parse : parse,
  365. setGeneratorInfos : setGeneratorInfos,
  366. setLogger : setLogger,
  367. setMarkdownParser : setMarkdownParser,
  368. setPackageInfos : setPackageInfos
  369. };