crojsdoc
Version:
A documentation generator for JavaScript and CoffeeScript
863 lines (837 loc) • 25.6 kB
JavaScript
// Generated by CoffeeScript 2.4.1
//#
// Collects comments from source files
// @module collect
// @see Collector
var Collector, _, collect, dox, inflect, is_test_mode, markdown;
_ = require('lodash');
dox = require('./dox');
inflect = require('inflect');
markdown = require('marked');
is_test_mode = process.env.NODE_ENV === 'test';
//#
// Collects comments from source files
Collector = class Collector {
//#
// Create a Collector instance
constructor(contents1, options1 = {}) {
this.contents = contents1;
this.options = options1;
this.result = {
project_title: this.options.title || 'croquis-jsdoc',
ids: {},
classes: {},
guides: [],
pages: {},
restapis: {},
features: [],
files: []
};
}
//#
// Adds a guide file to the result
_addGuide(path, data) {
var id, item, name;
id = path.substr(0, path.length - 3);
name = path.substr(0, path.length - 8).replace(/\//g, '.');
item = {
name: inflect.humanize(inflect.underscore(name)),
filename: 'guides/' + name,
content: markdown(data)
};
this.result.guides.push(item);
return this.result.ids[id] = item;
}
//#
// Adds a feature file to the result
_addFeature(path, data) {
var feature, name, namespace;
name = path.substr(0, path.length - 8);
namespace = '';
name = name.replace(/(.*)\//, function(_, $1) {
namespace = $1 + '/';
return '';
});
feature = '';
data = data.replace(/Feature: (.*)/, function(_, $1) {
feature = $1;
return '';
});
return this.result.features.push({
name: namespace + name,
namespace: namespace,
filename: 'features/' + namespace.replace(/\//g, '.') + name,
feature: feature,
content: data
});
}
//#
// Adds a source file to the result
_addFile(path, data) {
var name, namespace;
namespace = '';
name = path.replace(/(.*)\//, function(_, $1) {
namespace = $1 + '/';
return '';
});
return this.result.files.push({
name: namespace + name,
namespace: namespace,
filename: 'files/' + namespace.replace(/\//g, '.') + name,
content: data
});
}
//#
// Checks flags of parameter
// * '[' name ']' : optional
// * name '=' value : default value
// * '+' name : addable
// * '-' name : excludable
// @param {Object} tag
// @return {Object} given tag
_processParamFlags(tag) {
var pos;
// is optional parameter?
if (tag.name[0] === '[' && tag.name[tag.name.length - 1] === ']') {
tag.name = tag.name.substr(1, tag.name.length - 2);
if ((pos = tag.name.indexOf('=')) >= 0) {
tag.default_value = tag.name.substr(pos + 1);
tag.name = tag.name.substr(0, pos);
}
tag.optional = true;
}
if (tag.name.substr(0, 1) === '+') {
tag.name = tag.name.substr(1);
tag.addable = true;
}
if (tag.name.substr(0, 1) === '-') {
tag.name = tag.name.substr(1);
tag.excludable = true;
}
return tag;
}
//#
// Finds a parameter in the list
// @param {Array<Object>} params
// @param {String} name
// @return {Object}
_findParam(params, name) {
var found, j, len, param;
for (j = 0, len = params.length; j < len; j++) {
param = params[j];
if (param.name === name) {
return param;
}
if (param.params) {
found = this._findParam(param.params, name);
if (found) {
return found;
}
}
}
}
//#
// Makes parameters(or returnprops) nested
_makeNested(comment, targetName) {
var i, match, param, parentParam, results;
i = comment[targetName].length;
results = [];
while (i-- > 0) {
param = comment[targetName][i];
if (match = param.name.match(/\[?([^=]*)\.([^\]]*)\]?/)) {
parentParam = this._findParam(comment[targetName], match[1]);
if (parentParam) {
comment[targetName].splice(i, 1);
parentParam[targetName] = parentParam[targetName] || [];
param.name = match[2];
results.push(parentParam[targetName].unshift(param));
} else {
results.push(void 0);
}
} else {
results.push(void 0);
}
}
return results;
}
//#
// Apply markdown
_applyMarkdown(str) {
// we cannot use '###' for header level 3 or above in CoffeeScript, instead web use '##\#', ''##\##', ...
// recover this for markdown
str = str.replace(/#\\#/g, '##');
return markdown(str);
}
//#
// Classifies type and collect id
_classifyComments(comments) {
var current_class, current_module;
current_class = void 0;
current_module = void 0;
return comments.forEach((comment) => {
var i, id, j, last, len, ref, seperator, tag, typeString;
comment.ctx || (comment.ctx = {});
comment.params = [];
comment.returnprops = [];
comment.throws = [];
comment.resterrors = [];
comment.sees = [];
comment.reverse_sees = [];
comment.todos = [];
comment.extends = [];
comment.subclasses = [];
comment.uses = [];
comment.usedbys = [];
comment.properties = [];
comment.examples = [];
if (comment.ctx.type === 'property' || comment.ctx.type === 'method') {
id = comment.ctx.string.replace('()', '');
} else {
id = comment.ctx.name;
}
comment.ctx.fullname = id;
comment.namespace || (comment.namespace = '');
if (comment.ctx.type === 'property' || comment.ctx.type === 'method') {
if (comment.ctx.cons != null) {
comment.isStatic = false;
comment.ctx.class_name = comment.ctx.cons;
} else if (comment.ctx.receiver != null) {
comment.isStatic = true;
comment.ctx.class_name = comment.ctx.receiver;
}
}
last = 0;
ref = comment.tags;
for (i = j = 0, len = ref.length; j < len; i = ++j) {
tag = ref[i];
if (tag.type === '') {
comment.tags[last].string += `\n${tag.string}`;
continue;
}
last = i;
switch (tag.type) {
case 'page':
case 'restapi':
case 'class':
comment.ctx.type = tag.type;
if (tag.string) {
comment.ctx.name = tag.string;
comment.ctx.fullname = id = comment.ctx.name;
}
break;
case 'module':
comment.ctx.type = 'class';
comment.is_module = true;
if (tag.string) {
comment.ctx.name = tag.string;
comment.ctx.fullname = id = comment.ctx.name;
}
comment.code = null;
break;
case 'memberof':
if (/(::|#|\.prototype)$/.test(tag.parent)) {
comment.isStatic = false;
comment.ctx.class_name = tag.parent.replace(/(::|#|\.prototype)$/, '');
} else {
comment.isStatic = true;
comment.ctx.class_name = tag.parent;
}
break;
case 'namespace':
comment.namespace = tag.string ? tag.string : '';
break;
case 'property':
comment.ctx.type = tag.type;
comment.ctx.name = tag.name;
break;
case 'method':
comment.ctx.type = tag.type;
if (tag.string) {
comment.ctx.name = tag.string;
}
break;
case 'static':
comment.isStatic = true;
break;
case 'private':
comment.isPrivate = true;
break;
case 'abstract':
comment.isAbstract = true;
break;
case 'async':
comment.isAsync = true;
break;
case 'promise':
comment.doesReturnPromise = true;
break;
case 'nodejscallback':
comment.doesReturnNodejscallback = true;
break;
case 'chainable':
comment.isChainable = true;
break;
case 'type':
if (!tag.types && tag.typeString) {
typeString = tag.typeString;
if (!/{.*}/.test(typeString)) {
typeString = '{' + typeString + '}';
}
dox.parseTagTypes(typeString, tag);
}
break;
case 'apimethod':
comment.apimethod = tag.string.toUpperCase();
id += '_' + comment.apimethod;
break;
case 'param':
case 'return':
case 'returns':
case 'returnprop':
case 'throws':
case 'resterror':
case 'see':
case 'extends':
case 'todo':
case 'api':
case 'uses':
case 'override':
case 'example':
case 'internal':
break;
default:
console.log(`Unknown tag : ${tag.type} in ${comment.full_path}`);
}
}
if (comment.namespace) {
comment.namespace += '.';
}
if (comment.ctx.class_name) {
if (comment.ctx.type === 'function') {
comment.ctx.type = 'method';
} else if (comment.ctx.type === 'declaration') {
comment.ctx.type = 'property';
}
seperator = comment.isStatic ? '.' : '::';
id = comment.ctx.class_name + seperator + comment.ctx.name;
comment.ctx.fullname = comment.ctx.class_name.replace(/.*[\.\/](\w+)/, '$1') + seperator + comment.ctx.name;
}
if (comment.ctx.type === 'class') {
current_class = comment;
if (comment.is_module) {
current_module = comment;
}
}
if ((comment.ctx.type === 'property' || comment.ctx.type === 'method') && !comment.namespace) {
if (current_class) {
comment.namespace = current_class.namespace;
}
if (current_module && !comment.ctx.class_name) {
comment.ctx.class_name = current_module.ctx.name;
}
}
if (id) {
comment.id = id;
if (this.result.ids.hasOwnProperty(id)) {
this.result.ids[id] = 'DUPLICATED ENTRY';
} else {
this.result.ids[id] = comment;
}
if (comment.namespace && this.result.ids.hasOwnProperty(comment.namespace + id)) {
this.result.ids[comment.namespace + id] = 'DUPLICATED ENTRY';
} else {
this.result.ids[comment.namespace + id] = comment;
}
comment.html_id = (comment.namespace + id).replace(/[^A-Za-z0-9_]/g, '_');
}
switch (comment.ctx.type) {
case 'class':
comment.ctx.name = comment.namespace + comment.ctx.name;
comment.ctx.fullname = comment.namespace + comment.ctx.fullname;
this.result.classes[comment.ctx.name] = comment;
if (comment.is_module) {
return comment.filename = 'modules/' + comment.ctx.name.replace(/\//g, '.');
} else {
return comment.filename = 'classes/' + comment.ctx.name.replace(/\//g, '.');
}
break;
case 'property':
case 'method':
comment.ctx.class_name = comment.namespace + comment.ctx.class_name;
return comment.filename = 'classes/' + comment.ctx.class_name.replace(/\//g, '.');
case 'page':
return comment.filename = 'pages';
case 'restapi':
return comment.filename = 'restapis';
}
});
}
//#
// Returns list of comments of the given file
// @return {Array<Comment>}
_getComments(type, full_path, path, data) {
var comments, name, namespace;
if (type === 'coffeescript') {
comments = dox.parseCommentsCoffee(data, {
raw: true
});
comments.forEach(function(comment) {
return comment.language = 'coffeescript';
});
} else if (type === 'javascript') {
comments = dox.parseComments(data, {
raw: true
});
comments.forEach(function(comment) {
return comment.language = 'javascript';
});
} else if (type === 'typescript') {
comments = dox.parseCommentsTS(data);
comments.forEach(function(comment) {
return comment.language = 'typescript';
});
} else if (type === 'page') {
namespace = '';
name = path.substr(0, path.length - 3).replace(/[^A-Za-z0-9]*Page$/, '');
name = name.replace(/(.*)\//, function(_, $1) {
namespace = $1;
return '';
});
comments = [
{
description: {
summary: '',
body: data,
full: ''
},
tags: [
{
type: 'page',
string: name
},
{
type: 'namespace',
string: namespace
}
]
}
];
}
if (comments == null) {
return;
}
// filter out empty comments
comments = comments.filter(function(comment) {
var ref;
return comment.description.full || comment.description.summary || comment.description.body || ((ref = comment.tags) != null ? ref.length : void 0) > 0;
});
comments.forEach(function(comment) {
comment.full_path = full_path;
comment.path = path;
});
if (this.options.plugins) {
comments.forEach((comment) => {
this.options.plugins.forEach(function(plugin) {
plugin.onComment(comment);
});
});
}
this._classifyComments(comments);
return comments;
}
//#
// Structuralizes comments
_processComments(comments) {
return comments.forEach((comment) => {
var callback_params, class_comment, class_name, desc, i, j, k, l, len, len1, len2, len3, len4, m, n, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, str, tag, type;
desc = comment.description;
if (desc) {
desc.full = this._applyMarkdown(desc.full);
desc.summary = this._applyMarkdown(desc.summary);
desc.body = this._applyMarkdown(desc.body);
}
ref = comment.tags;
for (j = 0, len = ref.length; j < len; j++) {
tag = ref[j];
switch (tag.type) {
case 'param':
tag = this._processParamFlags(tag);
ref1 = tag.types;
for (i = k = 0, len1 = ref1.length; k < len1; i = ++k) {
type = ref1[i];
tag.types[i] = type;
}
tag.description = tag.description;
comment.params.push(tag);
break;
case 'return':
case 'returns':
ref2 = tag.types;
for (i = l = 0, len2 = ref2.length; l < len2; i = ++l) {
type = ref2[i];
tag.types[i] = type;
}
tag.description = tag.description;
comment.return = tag;
break;
case 'returnprop':
tag = dox.parseTag('@param ' + tag.string);
tag = this._processParamFlags(tag);
ref3 = tag.types;
for (i = m = 0, len3 = ref3.length; m < len3; i = ++m) {
type = ref3[i];
tag.types[i] = type;
}
tag.description = tag.description;
comment.returnprops.push(tag);
break;
case 'throws':
comment.throws.push({
message: tag.message,
description: tag.description
});
break;
case 'resterror':
if (/{(\d+)\/([A-Za-z0-9_ ]+)}\s*(.*)/.exec(tag.string)) {
comment.resterrors.push({
code: RegExp.$1,
message: RegExp.$2,
description: RegExp.$3
});
}
break;
case 'see':
str = tag.local || tag.url;
comment.sees.push(str);
break;
case 'todo':
comment.todos.push(tag.string);
break;
case 'extends':
comment.extends.push(tag.string);
if ((ref4 = this.result.ids[tag.string]) != null) {
ref4.subclasses.push(comment.ctx.name);
}
break;
case 'uses':
comment.uses.push(tag.string);
if ((ref5 = this.result.ids[tag.string]) != null) {
ref5.usedbys.push(comment.ctx.name);
}
break;
case 'type':
ref6 = tag.types;
for (i = n = 0, len4 = ref6.length; n < len4; i = ++n) {
type = ref6[i];
tag.types[i] = type;
}
comment.types = tag.types;
break;
case 'example':
comment.examples.push(tag);
break;
case 'override':
if (this.result.ids[tag.string] && this.result.ids[tag.string] !== 'DUPLICATED ENTRY') {
comment.override = this.result.ids[tag.string];
}
comment.override_link = tag.string;
}
}
if (comment.ctx.type === 'class') {
if (/^class +\w+ +extends +([\w\.]+)/.exec(comment.class_code)) {
comment.extends.push(RegExp.$1);
if ((ref7 = this.result.ids[RegExp.$1]) != null) {
ref7.subclasses.push(comment.ctx.name);
}
}
}
// make parameters nested
this._makeNested(comment, 'params');
this._makeNested(comment, 'returnprops');
if (comment.doesReturnNodejscallback) {
callback_params = [
{
name: 'error',
types: ['Error'],
description: 'See throws'
}
];
if (comment.return) {
callback_params.push({
name: 'result',
types: comment.return.types,
description: 'See returns'
});
}
comment.params.push({
name: 'callback',
types: ['Function'],
optional: comment.doesReturnPromise,
description: 'NodeJS style\'s callback',
params: callback_params
});
}
if (comment.isChainable && !comment.return) {
comment.return = {
types: [comment.ctx.class_name],
description: 'this'
};
}
switch (comment.ctx.type) {
case 'property':
case 'method':
class_name = comment.ctx.class_name;
if (class_name && (class_comment = this.result.classes[class_name])) {
if (comment.ctx.is_constructor) {
// merge to class comment
class_comment.code = comment.code;
class_comment.codeStart = comment.codeStart;
return class_comment.params = comment.params;
} else {
class_comment.properties.push(comment);
if (class_comment.is_module) {
return comment.filename = comment.filename.replace('classes/', 'modules/');
}
}
}
break;
case 'page':
return this.result.pages[comment.ctx.name] = comment;
case 'restapi':
if (comment.apimethod) {
this.result.restapis[comment.ctx.name + comment.apimethod] = comment;
return;
}
if (/^(GET|POST|PATCH|PUT|DELETE|HEAD)/i.test(comment.ctx.name)) {
comment.apimethod = RegExp.$1.toUpperCase();
}
return this.result.restapis[comment.ctx.name] = comment;
}
});
}
//#
// Refines result.
// - convert hash to sorted array
// - classes -> classes & modules
_refineResult() {
var result;
result = this.result;
result.classes = Object.keys(result.classes).sort(function(a, b) {
var a_ns, b_ns;
a_ns = result.classes[a].namespace;
b_ns = result.classes[b].namespace;
if (a_ns < b_ns) {
return -1;
}
if (a_ns > b_ns) {
return 1;
}
if (a < b) {
return -1;
} else {
return 1;
}
}).map(function(name) {
return result.classes[name];
});
result.pages = Object.keys(result.pages).sort(function(a, b) {
var a_ns, b_ns;
a_ns = result.pages[a].namespace;
b_ns = result.pages[b].namespace;
if (a_ns < b_ns) {
return -1;
}
if (a_ns > b_ns) {
return 1;
}
if (a < b) {
return -1;
} else {
return 1;
}
}).map(function(name) {
return result.pages[name];
});
result.restapis = Object.keys(result.restapis).sort(function(a, b) {
var a_ns, b_ns;
a_ns = result.restapis[a].namespace;
b_ns = result.restapis[b].namespace;
if (a_ns < b_ns) {
return -1;
}
if (a_ns > b_ns) {
return 1;
}
a = a.replace(/([A-Z]+) \/(.*)/, '-$2 $1');
b = b.replace(/([A-Z]+) \/(.*)/, '-$2 $1');
if (a < b) {
return -1;
} else {
return 1;
}
}).map(function(name) {
return result.restapis[name];
});
result.guides = result.guides.sort(function(a, b) {
if (a.name < b.name) {
return -1;
} else {
return 1;
}
});
result.features = result.features.sort(function(a, b) {
if (a.name < b.name) {
return -1;
} else {
return 1;
}
});
result.files = result.files.sort(function(a, b) {
var a_ns, b_ns;
a_ns = a.namespace;
b_ns = b.namespace;
if (a_ns < b_ns) {
return -1;
}
if (a_ns > b_ns) {
return 1;
}
if (a.name < b.name) {
return -1;
} else {
return 1;
}
});
result.classes.forEach(function(klass) {
var j, len, property, ref, results;
klass.properties.sort(function(a, b) {
if (a.ctx.name < b.ctx.name) {
return -1;
} else {
return 1;
}
});
ref = klass.properties;
results = [];
for (j = 0, len = ref.length; j < len; j++) {
property = ref[j];
results.push(property.ctx = _.pick(property.ctx, 'type', 'name', 'fullname'));
}
return results;
});
result.modules = result.classes.filter(function(klass) {
return klass.is_module;
});
return result.classes = result.classes.filter(function(klass) {
return !klass.is_module;
});
}
//#
// Returns the type of a file
_getType(path) {
if (/\.coffee$/.test(path)) {
return 'coffeescript';
} else if (/\.js$/.test(path)) {
return 'javascript';
} else if (/\.ts$/.test(path)) {
return 'typescript';
} else if (/Page\.md$/.test(path)) {
return 'page';
} else if (/Guide\.md$/.test(path)) {
return 'guide';
} else if (/\.feature$/.test(path)) {
return 'feature';
} else if (path === 'README') {
return 'readme';
} else {
return 'unknown';
}
}
//#
// Makes reverse see alsos
_makeReverseSeeAlso(comments) {
var comment, j, k, len, len1, me, other, ref, ref1, ref2, see;
for (j = 0, len = comments.length; j < len; j++) {
comment = comments[j];
ref = comment.sees;
for (k = 0, len1 = ref.length; k < len1; k++) {
see = ref[k];
other = this.result.ids[see];
if (other && other !== 'DUPLICATED ENTRY') {
me = this.result.ids[comment.id];
if (me && me === 'DUPLICATED ENTRY') {
if ((ref1 = other.reverse_sees) != null) {
ref1.push(comment.namespace + comment.id);
}
} else {
if ((ref2 = other.reverse_sees) != null) {
ref2.push(comment.id);
}
}
}
}
}
}
//#
// Runs
run() {
var all_comments, comments, data, file_count_read, full_path, j, len, path, ref, type;
all_comments = [];
file_count_read = 0;
ref = this.contents;
for (j = 0, len = ref.length; j < len; j++) {
({full_path, path, data} = ref[j]);
type = this._getType(path);
switch (type) {
case 'guide':
this._addGuide(path, data);
break;
case 'feature':
this._addFeature(path, data);
break;
case 'coffeescript':
case 'javascript':
case 'typescript':
case 'page':
comments = this._getComments(type, full_path, path, data);
if (comments != null) {
[].push.apply(all_comments, comments);
}
break;
case 'readme':
this.result.readme = markdown(data);
}
if (type === 'coffeescript' || type === 'javascript' || type === 'typescript') {
this._addFile(path, data);
}
file_count_read++;
if (!(this.options.quiet || is_test_mode)) {
console.log(path + ' is processed');
}
}
if (!is_test_mode) {
console.log('Total ' + file_count_read + ' files processed');
}
this._processComments(all_comments);
if (this.options.reverse_see_also) {
this._makeReverseSeeAlso(all_comments);
}
if (!this.options.files) {
this.result.files = [];
}
return this._refineResult();
}
};
//#
// Collects
// @param {Array<Content>} contents
// @param {Options} options
// @return {Result}
// @memberOf collect
collect = function(contents, options) {
var collector;
collector = new Collector(contents, options);
collector.run();
return collector.result;
};
module.exports = collect;