apidoc
Version:
RESTful web API Documentation Generator
426 lines (374 loc) • 10.7 kB
JavaScript
var fs = require("fs");
var path = require("path");
var util = require("util");
var _ = require("underscore");
var markdown = require("marked");
var app = {};
function Parser(_app)
{
var self = this;
// Global Variables
app = _app;
// Class Variables
self.parsers = {};
self.parsedFileElements = [];
self.parsedFiles = [];
// Markdown settings
markdown.setOptions(app.options.marked);
// Parser laden
var parsers = Object.keys(app.parsers);
parsers.forEach(function(parser) {
var filename = app.parsers[parser];
app.debug("load parser: " + parser + ", " + filename);
self.addParser(parser, require(filename));
});
} // Parser
/**
* Inherit
*/
util.inherits(Parser, Object);
/**
* Exports
*/
module.exports = Parser;
/**
* Add a Parser
*/
Parser.prototype.addParser = function(name, parser)
{
this.parsers[name] = parser;
}; // addParser
/**
* Execute Fileparsing
*/
Parser.prototype.parseFile = function(filename)
{
var self = this;
app.debug("inspect file: " + filename);
self.filename = filename;
self.extension = path.extname(filename).toLowerCase();
self.src = fs.readFileSync(filename, "utf8").toString();
app.debug("size: " + self.src.length);
// Unify Linebreaks
self.src = self.src.replace(/\r\n/g, "\n");
self.blocks = [];
self.elements = [];
self.indexApiBlocks = [];
// Determine Blocks
self.blocks = self._findBlocks();
if(self.blocks.length === 0) return;
app.debug("count blocks: " + self.blocks.length);
// Determine Elements of Blocks
for(var i = 0; i < self.blocks.length; i += 1)
{
var elements = self._findElements(self.blocks[i]);
self.elements.push(elements);
app.debug("count elements in block " + i + ": " + self.elements[i].length);
} // for
if(self.elements.length === 0) return;
// Determine List of Blocks with API-Elements
self.indexApiBlocks = self._findBlockWithApiGetIndex(self.elements);
if(self.indexApiBlocks.length === 0) return;
return self._parseBlockElements(self.indexApiBlocks, self.elements);
}; // parseFile
/**
* Parse API Elements with Plugins
*
* @param indexApiBlocks
* @param detectedElements
* @returns {___anonymous2249_2250}
*/
Parser.prototype._parseBlockElements = function(indexApiBlocks, detectedElements)
{
var self = this;
var parsedBlocks = [];
for(var i = 0; i < indexApiBlocks.length; i += 1)
{
var blockIndex = indexApiBlocks[i];
var elements = detectedElements[blockIndex];
var blockData = {
global: {},
local: {}
};
var preventGlobal = false;
var countAllowedMultiple = 0;
for(var j = 0; j < elements.length; j += 1)
{
var element = elements[j];
if(self.parsers[element.name])
{
// Determine Fieldvalues
var result = null;
try {
result = self.parsers[element.name].parse(element.content, element.source);
// Markdown
// TODO: Evaluate if better add a function in specific worker_{name}.js
if(app.options.marked.gfm &&
self.parsers[element.name].markdownFields &&
self.parsers[element.name].markdownFields.length > 0
)
{
for(var markdownIndex = 0; markdownIndex < self.parsers[element.name].markdownFields.length; markdownIndex += 1)
{
var markdownField = self.parsers[element.name].markdownFields[markdownIndex];
if(result[markdownField])
{
result[markdownField] = markdown(result[markdownField]);
// remove line breaks.
result[markdownField] = result[markdownField].replace(/(\r\n|\n|\r)/g, "");
}
} // for
}
}
catch(e)
{
throw new Error("\"@" + element.sourceName + "\" in file \"" + self.filename +
"\" block number " + (blockIndex + 1) + " " + e
);
}
if( ! result)
{
throw new Error("Empty result for \"@" + element.sourceName + "\" in file \"" + self.filename +
"\" block number " + (blockIndex + 1) + "."
);
}
// Check if it is allowed to add to global namespace.
if(self.parsers[element.name].preventGlobal && self.parsers[element.name].preventGlobal === true)
{
preventGlobal = true;
// Check if count global namespace entries > count allowed
// (e.g. @successTitle is global, but should co-exist with @apiErrorStructure)
if(Object.keys(blockData.global).length > countAllowedMultiple)
{
throw new Error("Can't set \"@" + element.sourceName + "\" in file \"" + self.filename +
"\" block number " + (blockIndex + 1) + ", only one definition or use is allowed in the same block."
);
}
}
if(self.parsers[element.name].pushTo)
{
// Fieldvalues will be inserted into subpath
var pushTo = self.parsers[element.name].pushTo();
// Only one global allowed per block
if(pushTo === "global" || pushTo.substr(0, 7) === "global.")
{
var allowMultiple = self.parsers[element.name].allowMultiple || false;
if(allowMultiple)
{
countAllowedMultiple += 1;
}
else
{
if(Object.keys(blockData.global).length > 0)
{
throw new Error("Can't set \"@" + element.sourceName + "\" in file \"" + self.filename +
"\" block number " + (blockIndex + 1) + ", only one definition per block allowed."
);
}
if(preventGlobal === true)
{
throw new Error("Can't set \"@" + element.sourceName + "\" in file \"" + self.filename +
"\" block number " + (blockIndex + 1) + ", only one definition or use is allowed in the same block."
);
}
}
}
if( ! blockData[pushTo])
{
// Create path
self._createObjectPath(blockData, pushTo);
}
var blockDataPath = _pathToObject(pushTo, blockData);
// Insert Fieldvalues in Path-Array
if(typeof(blockData[pushTo]) === "object")
{
_.extend(blockData[pushTo], result);
}
else
{
blockDataPath.push(result);
}
if(self.parsers[element.name].extendRoot === true)
{
// Insert Fieldvalues in Mainpath
_.extend(blockData, result);
}
}
else
{
// Insert Fieldvalues in Mainpath
_.extend(blockData, result);
}
blockData.index = blockIndex + 1;
}
else
{
app.logWarn("parser plugin \"" + element.name + "\" not found.");
}
} // for
parsedBlocks.push(blockData);
} // for
return parsedBlocks;
};
/**
* Return Path to Object.
*/
function _pathToObject(path, src) {
if( ! path) return src;
var pathParts = path.split(".");
var current = src;
for(var i = 0; i < pathParts.length; i += 1)
{
var part = pathParts[i];
current = current[part];
} // for
return current;
} // _pathToObject
/**
* Create a not existing Path in an Object.
*
* @param src
* @param path
* @returns {Object}
*/
Parser.prototype._createObjectPath = function(src, path)
{
if( ! path) return src;
var pathParts = path.split(".");
var current = src;
for(var i = 0; i < pathParts.length; i += 1)
{
var part = pathParts[i];
if( ! current[part])
{
if(i === (pathParts.length - 1) ) current[part] = [];
else current[part] = {};
}
current = current[part];
} // for
return current;
}; // _createObjectPath
/**
* Determine Blocks
*/
Parser.prototype._findBlocks = function()
{
var self = this;
var blocks = [];
var src = self.src;
var inlineRegExp;
var docBlocksRegExp;
// Replace Linebreak with Unicode
src = src.replace(/\n/g, "\uffff");
// TODO: make this more flexible with a plugin
// Blocksearch depending on file-extension
switch(self.extension)
{
case ".coffee":
// Find document blocks between "###" and "###"
docBlocksRegExp = /###\uffff?(.+?)###/g;
// Remove not needed " " (tabs) at the beginning
inlineRegExp = /^(\t+)?[ ]?/gm;
break;
case ".erl":
// Find document blocks between "%{" and "%}"
docBlocksRegExp = /\%\{\uffff?(.+?)\%\}/g;
// Remove not needed " % " and " " (tabs) at the beginning
// HINT: Not sure if erlang developer use the %, but i think it should be no problem
inlineRegExp = /^(\t+)?(\%)[ ]?/gm;
break;
case ".py":
// Find document blocks between """ and """
docBlocksRegExp = /\"\"\"\uffff?(.+?)\"\"\"/g;
// Remove not needed " " (tabs) at the beginning
inlineRegExp = /^(\t+)?[ ]?/gm;
break;
case ".rb":
// Find document blocks between "=begin" and "=end"
docBlocksRegExp = /\=begin\uffff?(.+?)\=end/g;
// Remove not needed " " (tabs) at the beginning
inlineRegExp = /^(\t+)?[ ]?/gm;
break;
case ".pm":
// Find document blocks between "#**" and "#*"
docBlocksRegExp = /#\*\*\uffff?(.+?)#\*/g;
// Remove not needed " # " and " " (tabs) at the beginning
inlineRegExp = /^(\s+)?(#)[ ]?/gm;
break;
default:
// Find document blocks between "/**" and "*/"
docBlocksRegExp = /\/\*\*\uffff?(.+?)\*\//g;
// Remove not needed " * " and " " (tabs) at the beginning
inlineRegExp = /^(\s+)?(\*)[ ]?/gm;
} // switch
var matches = docBlocksRegExp.exec(src);
while(matches)
{
var block = matches[1];
// Reverse Unicode Linebreaks
block = block.replace(/\uffff/g, "\n");
block = block.replace(inlineRegExp, "");
blocks.push(block);
// Find next
matches = docBlocksRegExp.exec(src);
} // while
return blocks;
}; // _findBlocks
/**
* Check if block has API elements and return block indexes.
*/
Parser.prototype._findBlockWithApiGetIndex = function(elements)
{
var foundIndexes = [];
for(var i = 0; i < elements.length; i += 1)
{
var foundIndex = -1;
for(var j = 0; j < elements[i].length; j += 1)
{
if(elements[i][j].name.substr(0, 3) === "api")
{
foundIndex = j;
}
if(elements[i][j].name.substr(0, 9) === "apiignore")
{
foundIndex = -1;
break;
}
} // for
if(foundIndex >= 0)
{
foundIndexes.push(i);
app.debug("api found in block: " + i);
}
} // for
return foundIndexes;
}; // _findBlockWithApiGetIndex
/**
* Get Elements of Blocks.
*/
Parser.prototype._findElements = function(block)
{
var self = this;
var elements = [];
// Replace Linebreak with Unicode
block = block.replace(/\n/g, "\uffff");
// Elements start with @
var elementsRegExp = /(@(\w*)\s?(.+?)(?=\uffff[\s\*]*@|$))/gm;
var matches = elementsRegExp.exec(block);
while(matches)
{
var element = {
source: matches[1],
name: matches[2].toLowerCase(),
sourceName: matches[2],
content: matches[3]
};
// Reverse Unicode Linebreaks
element.content = element.content.replace(/\uffff/g, "\n");
element.source = element.source.replace(/\uffff/g, "\n");
elements.push(element);
// Next Match
matches = elementsRegExp.exec(block);
}
return elements;
}; // _findElements