diff --git a/hooks.md b/hooks.md new file mode 100644 index 0000000..257b787 --- /dev/null +++ b/hooks.md @@ -0,0 +1,33 @@ +# apiDoc Hooks + + +## parser-find-elements + +Called on each found element. Returns a new list of elements (replace elements). +Used to inject annotationes from an external schema. + +Parameter: `(elements, element, block, filename)` + * {array} elements Found elements in a block without the current element. + * {array} element Contains the source, name (lowercase), sourceName (original), content. + * {string} block Current source block. + * {string} filename Current filename. + +File: `parser.js` +Function: `_findElements` + + + +## parser-find-element-{name} + +Called on each found element and returns the modified element. +Used to modify a specific element. + +{name} is the found element.name (lowercase). + +Parameter: `(element, block, filename)` + * {array} element Contains the source, name (lowercase), sourceName (original), content. + * {string} block Current source block. + * {string} filename Current filename. + +File: `parser.js` +Function: `_findElements` diff --git a/lib/filter.js b/lib/filter.js index c55ff03..a861b70 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -21,9 +21,14 @@ function Filter(_app) { // load filters var filters = Object.keys(app.filters); filters.forEach(function(filter) { - var filename = app.filters[filter]; - app.log.debug('load filter: ' + filter + ', ' + filename); - self.addFilter(filter, require(filename)); + if (_.isObject( app.filters[filter] )) { + app.log.debug('inject filter: ' + parser); + self.addFilter(worker, app.filters[filter] ); + } else { + var filename = app.filters[filter]; + app.log.debug('load filter: ' + filter + ', ' + filename); + self.addFilter(filter, require(filename)); + } }); } diff --git a/lib/index.js b/lib/index.js index 16f8860..a154281 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,6 +9,8 @@ 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'); @@ -90,7 +92,10 @@ var app = { apisuccessstructure : './workers/api_success_structure.js', apisuccesstitle : './workers/api_success_title.js', apiuse : './workers/api_use.js' - } + }, + hooks: {}, + addHook: addHook, + hook: applyHook }; var defaultGenerator = { @@ -159,6 +164,7 @@ function parse(options) { 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; @@ -181,10 +187,17 @@ function parse(options) { 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'); @@ -345,6 +358,55 @@ 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, diff --git a/lib/parser.js b/lib/parser.js index 234e853..e0243b5 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -27,17 +27,27 @@ function Parser(_app) { // load languages var languages = Object.keys(app.languages); languages.forEach(function(language) { - var filename = app.languages[language]; - app.log.debug('load parser language: ' + language + ', ' + filename); - self.addLanguage(language, require(filename)); + if (_.isObject( app.languages[language] )) { + app.log.debug('inject parser language: ' + language); + self.addLanguage(language, app.languages[language] ); + } else { + var filename = app.languages[language]; + app.log.debug('load parser language: ' + language + ', ' + filename); + self.addLanguage(language, require(filename)); + } }); // load parser var parsers = Object.keys(app.parsers); parsers.forEach(function(parser) { - var filename = app.parsers[parser]; - app.log.debug('load parser: ' + parser + ', ' + filename); - self.addParser(parser, require(filename)); + if (_.isObject( app.parsers[parser] )) { + app.log.debug('inject parser: ' + parser); + self.addParser(parser, app.parsers[parser] ); + } else { + var filename = app.parsers[parser]; + app.log.debug('load parser: ' + parser + ', ' + filename); + self.addParser(parser, require(filename)); + } }); } @@ -127,7 +137,7 @@ Parser.prototype.parseFile = function(filename, encoding) { // determine elements in blocks self.elements = self.blocks.map(function(block, i) { - var elements = self._findElements(block); + var elements = self.findElements(block, filename); app.log.debug('count elements in block ' + i + ': ' + elements.length); return elements; }); @@ -420,7 +430,7 @@ Parser.prototype._findBlockWithApiGetIndex = function(blocks) { /** * Get Elements of Blocks */ -Parser.prototype._findElements = function(block) { +Parser.prototype.findElements = function(block, filename) { var elements = []; // Replace Linebreak with Unicode @@ -441,8 +451,12 @@ Parser.prototype._findElements = function(block) { element.content = element.content.replace(/\uffff/g, '\n'); element.source = element.source.replace(/\uffff/g, '\n'); + app.hook('parser-find-element-' + element.name, element, block, filename); + elements.push(element); + app.hook('parser-find-elements', elements, element, block, filename); + // next Match matches = elementsRegExp.exec(block); } diff --git a/lib/plugin_loader.js b/lib/plugin_loader.js new file mode 100644 index 0000000..e1bb6fb --- /dev/null +++ b/lib/plugin_loader.js @@ -0,0 +1,92 @@ +var _ = require('lodash'); +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var glob = require('glob'); + +var app = {}; + +function PluginLoader(_app) { + var self = this; + + // global variables + app = _app; + + // class variables + self.plugins = {}; + + // Try to load global apidoc-plugins (if apidoc is installed locally it tries only local) + this.detectPugins(__dirname); + + // Try to load local apidoc-plugins + this.detectPugins( path.join(process.cwd(), '/node_modules') ); + + if (Object.keys(this.plugins).length === 0) + app.log.debug('No plugins found.'); + + this.loadPlugins(); +} +/** + * Inherit + */ +util.inherits(PluginLoader, Object); + +/** + * Exports + */ +module.exports = PluginLoader; + +/** + * Detect modules start with "apidoc-plugin-". + * Search up to root until found a plugin. + */ +PluginLoader.prototype.detectPugins = function(dir) { + var self = this; + + // Search from the given dir up to root. + // Every dir start with "apidoc-plugin-", because for the tests of apidoc-plugin-test. + var plugins = glob.sync(dir + '/apidoc-plugin-*'); + if (plugins.length === 0) { + dir = path.join(dir, '..'); + if (dir === '/') + return; + return this.detectPugins(dir); + } + + var offset = dir.length + 1; + plugins.forEach( function(plugin) { + var name = plugin.substr(offset); + var filename = path.relative(__dirname, plugin); + app.log.debug('add plugin: ' + name + ', ' + filename); + self.addPlugin(name, plugin); + }); +}; + +/** + * Add Plugin to plugin list. + */ +PluginLoader.prototype.addPlugin = function(name, filename) { + if (this.plugins[name]) + app.log.debug('overwrite plugin: ' + name + ', ' + this.plugins[name]); + + this.plugins[name] = filename; +}; + +/** + * Load and initialize Plugins. + */ +PluginLoader.prototype.loadPlugins = function() { + _.forEach(this.plugins, function(filename, name) { + app.log.debug('load plugin: ' + name + ', ' + filename); + var plugin; + try { + plugin = require(filename); + } catch(e) { + } + if (plugin && plugin.init) { + plugin.init(app); + } else { + app.log.debug('Ignored, no init function found.'); + } + }); +}; diff --git a/lib/worker.js b/lib/worker.js index f74c19e..cac372c 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -26,9 +26,14 @@ function Worker(_app) { // load worker var workers = Object.keys(app.workers); workers.forEach(function(worker) { - var filename = app.workers[worker]; - app.log.debug('load worker: ' + worker + ', ' + filename); - self.addWorker(worker, require(filename)); + if (_.isObject( app.workers[worker] )) { + app.log.debug('inject worker: ' + parser); + self.addWorker(worker, app.workers[worker] ); + } else { + var filename = app.workers[worker]; + app.log.debug('load worker: ' + worker + ', ' + filename); + self.addWorker(worker, require(filename)); + } }); } diff --git a/package.json b/package.json index 555daad..4cfecb9 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "node": ">= 0.10.0" }, "dependencies": { + "glob": "^7.0.3", "iconv-lite": "^0.4.13", "lodash": "~4.5.0", "semver": "~5.1.0",