crojsdoc
Version:
A documentation generator for JavaScript and CoffeeScript
683 lines (655 loc) • 19.8 kB
JavaScript
// Generated by CoffeeScript 2.4.1
//#
// Module dependencies.
var markdown, markedOptions, renderer;
markdown = require('marked');
renderer = new markdown.Renderer();
//renderer.heading = (text, level) ->
// '<h' + level + '>' + text + '</h' + level + '>\n'
//renderer.paragraph = (text) ->
// '<p>' + text + '</p>'
//renderer.br = () ->
// '<br />'
markedOptions = {
renderer: renderer,
gfm: true,
tables: true,
// breaks: true
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
};
markdown.setOptions(markedOptions);
//#
// Parse comments in the given string of `js`.
// @param {String} js
// @param {Object} options
// @return {Array}
// @see exports.parseComment
// @api public
exports.parseComments = function(js, options = {}) {
var buf, code, comment, comments, i, ignore, len, lineNum, lineNumStarting, linterPrefixes, parentContext, ref, skipPattern, skipSingleStar, withinMultiline, withinSingle, withinString;
js = js.replace(/\r\n/gm, '\n');
comments = [];
skipSingleStar = options.skipSingleStar;
buf = '';
withinMultiline = false;
withinSingle = false;
withinString = false;
linterPrefixes = options.skipPrefixes || ['jslint', 'jshint', 'eshint'];
skipPattern = new RegExp('^' + ((ref = options.raw) != null ? ref : {
'': '<p>'
}) + '(' + linterPrefixes.join('|') + ')');
lineNum = 1;
lineNumStarting = 1;
i = 0;
len = js.length;
while (i < len) {
// start comment
if (!withinMultiline && !withinSingle && !withinString && '/' === js[i] && '*' === js[i + 1] && (!skipSingleStar || js[i + 2] === '*')) {
lineNumStarting = lineNum;
// code following the last comment
if (buf.trim().length) {
comment = comments[comments.length - 1];
if (comment) {
// Adjust codeStart for any vertical space between comment and code
comment.codeStart += buf.match(/^(\s*)/)[0].split('\n').length - 1;
comment.code = code = exports.trimIndentation(buf).trim();
comment.ctx = exports.parseCodeContext(code, parentContext);
if (comment.isConstructor && comment.ctx) {
comment.ctx.type = 'constructor';
}
// starting a new namespace
if (comment.ctx && (comment.ctx.type === 'prototype' || comment.ctx.type === 'class')) {
parentContext = comment.ctx;
// reasons to clear the namespace
// new property/method in a different constructor
} else if (!parentContext || !comment.ctx || !comment.ctx.constructor || !parentContext.constructor || parentContext.constructor !== comment.ctx.constructor) {
parentContext = null;
}
}
buf = '';
}
i += 2;
withinMultiline = true;
ignore = '!' === js[i];
// if the current character isn't whitespace and isn't an ignored comment,
// back up one character so we don't clip the contents
if (' ' !== js[i] && '\n' !== js[i] && '\t' !== js[i] && '!' !== js[i]) {
i--;
}
// end comment
} else if (withinMultiline && !withinSingle && '*' === js[i] && '/' === js[i + 1]) {
i += 2;
buf = buf.replace(/^[ \t]*\* ?/gm, '');
comment = exports.parseComment(buf, options);
comment.ignore = ignore;
comment.line = lineNumStarting;
comment.codeStart = lineNum + 1;
if (!comment.description.full.match(skipPattern)) {
comments.push(comment);
}
withinMultiline = ignore = false;
buf = '';
} else if (!withinSingle && !withinMultiline && !withinString && '/' === js[i] && '/' === js[i + 1]) {
withinSingle = true;
buf += js[i];
} else if (withinSingle && !withinMultiline && '\n' === js[i]) {
withinSingle = false;
buf += js[i];
} else if (!withinSingle && !withinMultiline && ('\'' === js[i] || '"' === js[i])) {
withinString = !withinString;
buf += js[i];
} else {
buf += js[i];
}
if ('\n' === js[i]) {
lineNum++;
}
i++;
}
if (comments.length === 0) {
comments.push({
tags: [],
description: {
full: '',
summary: '',
body: ''
},
isPrivate: false,
isConstructor: false,
line: lineNumStarting
});
}
// trailing code
if (buf.trim().length) {
comment = comments[comments.length - 1];
// Adjust codeStart for any vertical space between comment and code
comment.codeStart += buf.match(/^(\s*)/)[0].split('\n').length - 1;
comment.code = code = exports.trimIndentation(buf).trim();
comment.ctx = exports.parseCodeContext(code, parentContext);
}
return comments;
};
//#
// Removes excess indentation from string of code.
// @param {String} str
// @return {String}
// @api public
exports.trimIndentation = function(str) {
var indent;
// Find indentation from first line of code.
indent = str.match(/(?:^|\n)([ \t]*)[^\s]/);
if (indent) {
// Replace indentation on all lines.
str = str.replace(new RegExp('(^|\n)' + indent[1], 'g'), '$1');
}
return str;
};
//#
// Parse the given comment `str`.
// The comment object returned contains the following
// - `tags` array of tag objects
// - `description` the first line of the comment
// - `body` lines following the description
// - `content` both the description and the body
// - `isPrivate` true when "@api private" is used
// @param {String} str
// @param {Object} options
// @return {Object}
// @see exports.parseTag
// @api public
exports.parseComment = function(str, options = {}) {
var comment, description, raw, tags;
str = str.trim();
comment = {
tags: []
};
raw = options.raw;
description = {};
tags = str.split(/\n\s*@/);
// A comment has no description
if (tags[0].charAt(0) === '@') {
tags.unshift('');
}
// parse comment body
description.full = tags[0];
description.summary = description.full.split('\n\n')[0];
description.body = description.full.split('\n\n').slice(1).join('\n\n');
comment.description = description;
// parse tags
if (tags.length) {
comment.tags = tags.slice(1).map(exports.parseTag);
comment.isPrivate = comment.tags.some(function(tag) {
return 'private' === tag.visibility;
});
comment.isConstructor = comment.tags.some(function(tag) {
return 'constructor' === tag.type || 'augments' === tag.type;
});
comment.isClass = comment.tags.some(function(tag) {
return 'class' === tag.type;
});
comment.isEvent = comment.tags.some(function(tag) {
return 'event' === tag.type;
});
if (!description.full || !description.full.trim()) {
comment.tags.some(function(tag) {
if ('description' === tag.type) {
description.full = tag.full;
description.summary = tag.summary;
description.body = tag.body;
return true;
}
});
}
}
// markdown
if (!raw) {
description.full = markdown(description.full);
description.summary = markdown(description.summary);
description.body = markdown(description.body);
comment.tags.forEach(function(tag) {
if (tag.description) {
return tag.description = markdown(tag.description);
} else {
return tag.html = markdown(tag.string);
}
});
}
return comment;
};
//TODO: Find a smarter way to do this
//#
// Extracts different parts of a tag by splitting string into pieces separated by whitespace. If the white spaces are
// somewhere between curly braces (which is used to indicate param/return type in JSDoc) they will not be used to split
// the string. This allows to specify jsdoc tags without the need to eliminate all white spaces i.e. {number | string}
// @param str The tag line as a string that needs to be split into parts
// @returns {Array.<string>} An array of strings containing the parts
exports.extractTagParts = function(str) {
var extract, level, split;
level = 0;
extract = '';
split = [];
str.split('').forEach(function(c) {
if (c.match(/\s/) && level === 0) {
split.push(extract);
return extract = '';
} else {
if (c === '{') {
level++;
} else if (c === '}') {
level--;
}
return extract += c;
}
});
split.push(extract);
return split.filter(function(str) {
return str.length > 0;
});
};
//#
// Parse tag string "@param {Array} name description" etc.
// @param {String}
// @return {Object}
// @api public
exports.parseTag = function(str) {
var getMultilineDescription, lines, matchType, matchTypeStr, parts, tag, type, typeString;
tag = {};
lines = str.split('\n');
parts = exports.extractTagParts(lines[0]);
type = tag.type = parts.shift().replace('@', '').toLowerCase();
matchType = new RegExp('^@?' + type + ' *');
matchTypeStr = /^\{.+\}$/;
tag.string = str.replace(matchType, '');
getMultilineDescription = function() {
var description;
description = parts.join(' ');
if (lines.length > 1) {
if (description) {
description += '\n';
}
description += lines.slice(1).join('\n');
}
return description;
};
switch (type) {
case 'property':
case 'template':
case 'param':
typeString = matchTypeStr.test(parts[0]) ? parts.shift() : '';
tag.name = parts.shift() || '';
tag.description = getMultilineDescription();
exports.parseTagTypes(typeString, tag);
break;
case 'define':
case 'return':
case 'returns':
typeString = matchTypeStr.test(parts[0]) ? parts.shift() : '';
exports.parseTagTypes(typeString, tag);
tag.description = getMultilineDescription();
break;
case 'see':
if (~str.indexOf('http')) {
tag.title = parts.length > 1 ? parts.shift() : '';
tag.url = parts.join(' ');
} else {
tag.local = parts.join(' ');
}
break;
case 'api':
tag.visibility = parts.shift();
break;
case 'public':
case 'private':
case 'protected':
tag.visibility = type;
break;
case 'enum':
case 'typedef':
case 'type':
typeString = parts.shift();
if (!/{.*}/.test(typeString)) {
typeString = '{' + typeString + '}';
}
exports.parseTagTypes(typeString, tag);
break;
case 'lends':
case 'memberof':
tag.parent = parts.shift();
break;
case 'extends':
case 'implements':
case 'augments':
tag.otherClass = parts.shift();
break;
case 'borrows':
tag.otherMemberName = parts.join(' ').split(' as ')[0];
tag.thisMemberName = parts.join(' ').split(' as ')[1];
break;
case 'throws':
if (/{([^}]+)}\s*(.*)/.exec(str)) {
tag.message = RegExp.$1;
tag.description = RegExp.$2;
} else {
tag.message = '';
tag.description = str;
}
break;
case 'description':
tag.full = parts.join(' ').trim();
tag.summary = tag.full.split('\n\n')[0];
tag.body = tag.full.split('\n\n').slice(1).join('\n\n');
break;
default:
tag.string = getMultilineDescription().replace(/\s+$/, '');
}
return tag;
};
//#
// Parse tag type string "{Array|Object}" etc.
// This function also supports complex type descriptors like in jsDoc or even the enhanced syntax used by the
// [google closure compiler](https://developers.google.com/closure/compiler/docs/js-for-compiler#types)
// The resulting array from the type descriptor `{number|string|{name:string,age:number|date}}` would look like this:
// [
// 'number',
// 'string',
// {
// age: ['number', 'date'],
// name: ['string']
// }
// ]
// @param {String} str
// @return {Array}
// @api public
exports.parseTagTypes = function(str, tag) {
var NodeType, SyntaxType, optional, parse, publish, result, transform, types;
if (!str) {
if (tag) {
tag.types = [];
tag.optional = false;
}
return [];
}
({parse, publish, NodeType, SyntaxType} = require('jsdoctypeparser'));
result = parse(str.substr(1, str.length - 2));
optional = false;
if (result.type === NodeType.OPTIONAL) {
optional = true;
result = result.value;
}
transform = function(ast) {
var left, right;
if (ast.type === NodeType.NAME) {
return [ast.name];
} else if (ast.type === NodeType.UNION) {
left = transform(ast.left);
right = transform(ast.right);
[].push.apply(left, right);
return left;
} else if (ast.type === NodeType.RECORD) {
return [
ast.entries.reduce(function(obj,
entry) {
obj[entry.key] = transform(entry.value);
return obj;
},
{})
];
} else if (ast.type === NodeType.GENERIC && ast.meta.syntax === SyntaxType.GenericTypeSyntax.ANGLE_BRACKET_WITH_DOT) {
ast = {
...ast,
meta: {
...ast.meta,
syntax: SyntaxType.GenericTypeSyntax.ANGLE_BRACKET
}
};
return [publish(ast)];
} else {
return [publish(ast)];
}
};
types = transform(result);
if (tag) {
tag.types = types;
tag.optional = (tag.name && tag.name.slice(0, 1) === '[') || optional;
}
return types;
};
//#
// Parse the context from the given `str` of js.
// This method attempts to discover the context
// for the comment based on it's code. Currently
// supports:
// - classes
// - class constructors
// - class methods
// - function statements
// - function expressions
// - prototype methods
// - prototype properties
// - methods
// - properties
// - declarations
// @param {String} str
// @param {Object=} parentContext An indication if we are already in something. Like a namespace or an inline declaration.
// @return {Object}
// @api public
exports.parseCodeContext = function(str, parentContext) {
var ctx;
if (!parentContext) {
parentContext = {};
}
ctx = void 0;
// loop through all context matchers, returning the first successful match
return exports.contextPatternMatchers.some(function(matcher) {
return ctx = matcher(str, parentContext);
}) && ctx;
};
exports.contextPatternMatchers = [
// class, possibly exported by name or as a default
function(str) {
if (/^\s*(export(\s+default)?\s+)?class\s+([\w$]+)(\s+extends\s+([\w$.]+(?:\(.*\))?))?\s*{/.exec(str)) {
return {
type: 'class',
constructor: RegExp.$3,
cons: RegExp.$3,
name: RegExp.$3,
extends: RegExp.$5,
string: 'new ' + RegExp.$3 + '()'
};
}
},
// class constructor
function(str,
parentContext) {
if (/^\s*constructor\s*\(/.exec(str)) {
return {
type: 'method',
constructor: parentContext.name,
cons: parentContext.name,
name: 'constructor',
string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + 'constructor()',
is_constructor: true
};
}
},
// class method
function(str,
parentContext) {
if (/^\s*(static)?\s*(\*)?\s*([\w$]+|\[.*\])\s*\(/.exec(str)) {
return {
type: 'method',
constructor: parentContext.name,
cons: parentContext.name,
name: RegExp.$2 + RegExp.$3,
string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + (RegExp.$1 ? '.' : '.prototype.') : '') + RegExp.$2 + RegExp.$3 + '()'
};
}
},
// named function statementpossibly exported by name or as a default
function(str) {
if (/^\s*(export(\s+default)?\s+)?function\s+([\w$]+)\s*\(/.exec(str)) {
return {
type: 'function',
name: RegExp.$3,
string: RegExp.$3 + '()'
};
}
},
// anonymous function expression exported as a default
function(str) {
if (/^\s*export\s+default\s+function\s*\(/.exec(str)) {
return {
type: 'function',
name: RegExp.$1, // undefined
string: RegExp.$1 + '()'
};
}
},
// function expression
function(str) {
if (/^return\s+function(?:\s+([\w$]+))?\s*\(/.exec(str)) {
return {
type: 'function',
name: RegExp.$1,
string: RegExp.$1 + '()'
};
}
},
// function expression
function(str) {
if (/^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*function/.exec(str)) {
return {
type: 'function',
name: RegExp.$1,
string: RegExp.$1 + '()'
};
}
},
// prototype method
function(str,
parentContext) {
if (/^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*function/.exec(str)) {
return {
type: 'method',
constructor: RegExp.$1,
cons: RegExp.$1,
name: RegExp.$2,
string: RegExp.$1 + '.prototype.' + RegExp.$2 + '()'
};
}
},
// prototype property
function(str) {
if (/^\s*([\w$.]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) {
return {
type: 'property',
constructor: RegExp.$1,
cons: RegExp.$1,
name: RegExp.$2,
value: RegExp.$3.trim(),
string: RegExp.$1 + '.prototype.' + RegExp.$2
};
}
},
// prototype property without assignment
function(str) {
if (/^\s*([\w$]+)\s*\.\s*prototype\s*\.\s*([\w$]+)\s*/.exec(str)) {
return {
type: 'property',
constructor: RegExp.$1,
cons: RegExp.$1,
name: RegExp.$2,
string: RegExp.$1 + '.prototype.' + RegExp.$2
};
}
},
// inline prototype
function(str) {
if (/^\s*([\w$.]+)\s*\.\s*prototype\s*=\s*{/.exec(str)) {
return {
type: 'prototype',
constructor: RegExp.$1,
cons: RegExp.$1,
name: RegExp.$1,
string: RegExp.$1 + '.prototype'
};
}
},
// inline method
function(str,
parentContext) {
if (/^\s*([\w$.]+)\s*:\s*function/.exec(str)) {
return {
type: 'method',
constructor: parentContext.name,
cons: parentContext.name,
name: RegExp.$1,
string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + RegExp.$1 + '()'
};
}
},
// inline property
function(str,
parentContext) {
if (/^\s*([\w$.]+)\s*:\s*([^\n;]+)/.exec(str)) {
return {
type: 'property',
constructor: parentContext.name,
cons: parentContext.name,
name: RegExp.$1,
value: RegExp.$2.trim(),
string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.' : '') + RegExp.$1
};
}
},
// inline getter/setter
function(str,
parentContext) {
if (/^\s*(get|set)\s*([\w$.]+)\s*\(/.exec(str)) {
return {
type: 'property',
constructor: parentContext.name,
cons: parentContext.name,
name: RegExp.$2,
string: ((parentContext != null ? parentContext.name : void 0) ? parentContext.name + '.prototype.' : '') + RegExp.$2
};
}
},
// method
function(str) {
if (/^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*function/.exec(str)) {
return {
type: 'method',
receiver: RegExp.$1,
name: RegExp.$2,
string: RegExp.$1 + '.' + RegExp.$2 + '()'
};
}
},
// property
function(str) {
if (/^\s*([\w$.]+)\s*\.\s*([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) {
return {
type: 'property',
receiver: RegExp.$1,
name: RegExp.$2,
value: RegExp.$3.trim(),
string: RegExp.$1 + '.' + RegExp.$2
};
}
},
// declaration
function(str) {
if (/^\s*(?:const|let|var)\s+([\w$]+)\s*=\s*([^\n;]+)/.exec(str)) {
return {
type: 'declaration',
name: RegExp.$1,
value: RegExp.$2.trim(),
string: RegExp.$1
};
}
}
];