UNPKG

crojsdoc

Version:

A documentation generator for JavaScript and CoffeeScript

863 lines (837 loc) 25.6 kB
// 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;