|
|
"use strict";
var fs = require("fs");
// output stream
var out = null;
// documentation data
var data = null;
// already handled objects, by name
var seen = {};
// indentation level
var indent = 0;
// whether indent has been written for the current line yet
var indentWritten = false;
// provided options
var options = {};
// queued interfaces
var queuedInterfaces = [];
// whether writing the first line
var firstLine = true;
// JSDoc hook
exports.publish = function publish(taffy, opts) { options = opts || {};
// query overrides options
if (options.query) Object.keys(options.query).forEach(function(key) { if (key !== "query") switch (options[key] = options.query[key]) { case "true": options[key] = true; break; case "false": options[key] = false; break; case "null": options[key] = null; break; } });
// remove undocumented
taffy({ undocumented: true }).remove(); taffy({ ignore: true }).remove(); taffy({ inherited: true }).remove();
// remove private
if (!options.private) taffy({ access: "private" }).remove();
// setup output
out = options.destination ? fs.createWriteStream(options.destination) : process.stdout;
try { // setup environment
data = taffy().get(); indent = 0; indentWritten = false; firstLine = true;
// wrap everything in a module if configured
if (options.module) { writeln("export = ", options.module, ";"); writeln(); writeln("declare namespace ", options.module, " {"); writeln(); ++indent; }
// handle all
getChildrenOf(undefined).forEach(function(child) { handleElement(child, null); });
// process queued
while (queuedInterfaces.length) { var element = queuedInterfaces.shift(); begin(element); writeInterface(element); writeln(";"); }
// end wrap
if (options.module) { --indent; writeln("}"); }
// close file output
if (out !== process.stdout) out.end();
} finally { // gc environment objects
out = data = null; seen = options = {}; queuedInterfaces = []; } };
//
// Utility
//
// writes one or multiple strings
function write() { var s = Array.prototype.slice.call(arguments).join(""); if (!indentWritten) { for (var i = 0; i < indent; ++i) s = " " + s; indentWritten = true; } out.write(s); firstLine = false; }
// writes zero or multiple strings, followed by a new line
function writeln() { var s = Array.prototype.slice.call(arguments).join(""); if (s.length) write(s, "\n"); else if (!firstLine) out.write("\n"); indentWritten = false; }
var keepTags = [ "param", "returns", "throws", "see" ];
// parses a comment into text and tags
function parseComment(comment) { var lines = comment.replace(/^ *\/\*\* *|^ *\*\/| *\*\/ *$|^ *\* */mg, "").trim().split(/\r?\n|\r/g); // property.description has just "\r" ?!
var desc; var text = []; var tags = null; for (var i = 0; i < lines.length; ++i) { var match = /^@(\w+)\b/.exec(lines[i]); if (match) { if (!tags) { tags = []; desc = text; } text = []; tags.push({ name: match[1], text: text }); lines[i] = lines[i].substring(match[1].length + 1).trim(); } if (lines[i].length || text.length) text.push(lines[i]); } return { text: desc || text, tags: tags || [] }; }
// writes a comment
function writeComment(comment, otherwiseNewline) { if (!comment || options.comments === false) { if (otherwiseNewline) writeln(); return; } if (typeof comment !== "object") comment = parseComment(comment); comment.tags = comment.tags.filter(function(tag) { return keepTags.indexOf(tag.name) > -1 && (tag.name !== "returns" || tag.text[0] !== "{undefined}"); }); writeln(); if (!comment.tags.length && comment.text.length < 2) { writeln("/** " + comment.text[0] + " */"); return; } writeln("/**"); comment.text.forEach(function(line) { if (line.length) writeln(" * ", line); else writeln(" *"); }); comment.tags.forEach(function(tag) { var started = false; if (tag.text.length) { tag.text.forEach(function(line, i) { if (i > 0) write(" * "); else if (tag.name !== "throws") line = line.replace(/^\{[^\s]*} ?/, ""); if (!line.length) return; if (!started) { write(" * @", tag.name, " "); started = true; } writeln(line); }); } }); writeln(" */"); }
// recursively replaces all occurencies of re's match
function replaceRecursive(name, re, fn) { var found;
function replacer() { found = true; return fn.apply(null, arguments); }
do { found = false; name = name.replace(re, replacer); } while (found); return name; }
// tests if an element is considered to be a class or class-like
function isClassLike(element) { return isClass(element) || isInterface(element); }
// tests if an element is considered to be a class
function isClass(element) { return element && element.kind === "class"; }
// tests if an element is considered to be an interface
function isInterface(element) { return element && (element.kind === "interface" || element.kind === "mixin"); }
// tests if an element is considered to be a namespace
function isNamespace(element) { return element && (element.kind === "namespace" || element.kind === "module"); }
// gets all children of the specified parent
function getChildrenOf(parent) { var memberof = parent ? parent.longname : undefined; return data.filter(function(element) { return element.memberof === memberof; }); }
// gets the literal type of an element
function getTypeOf(element) { if (element.tsType) return element.tsType.replace(/\r?\n|\r/g, "\n"); var name = "any"; var type = element.type; if (type && type.names && type.names.length) { if (type.names.length === 1) name = element.type.names[0].trim(); else name = "(" + element.type.names.join("|") + ")"; } else return name;
// Replace catchalls with any
name = name.replace(/\*|\bmixed\b/g, "any");
// Ensure upper case Object for map expressions below
name = name.replace(/\bobject\b/g, "Object");
// Correct Something.<Something> to Something<Something>
name = replaceRecursive(name, /\b(?!Object|Array)([\w$]+)\.<([^>]*)>/gi, function($0, $1, $2) { return $1 + "<" + $2 + ">"; });
// Replace Array.<string> with string[]
name = replaceRecursive(name, /\bArray\.?<([^>]*)>/gi, function($0, $1) { return $1 + "[]"; });
// Replace Object.<string,number> with { [k: string]: number }
name = replaceRecursive(name, /\bObject\.?<([^,]*), *([^>]*)>/gi, function($0, $1, $2) { return "{ [k: " + $1 + "]: " + $2 + " }"; });
// Replace functions (there are no signatures) with Function
name = name.replace(/\bfunction(?:\(\))?\b/g, "Function");
// Convert plain Object back to just object
name = name.replace(/\b(Object\b(?!\.))/g, function($0, $1) { return $1.toLowerCase(); });
return name; }
// begins writing the definition of the specified element
function begin(element, is_interface) { if (!seen[element.longname]) { if (isClass(element)) { var comment = parseComment(element.comment); var classdesc = comment.tags.find(function(tag) { return tag.name === "classdesc"; }); if (classdesc) { comment.text = classdesc.text; comment.tags = []; } writeComment(comment, true); } else writeComment(element.comment, is_interface || isClassLike(element) || isNamespace(element) || element.isEnum || element.scope === "global"); seen[element.longname] = element; } else writeln(); // ????: something changed in JSDoc 3.6.0? so that @exports + @enum does
// no longer yield a 'global' scope, but is some sort of unscoped module
// element now. The additional condition added below works around this.
if ((element.scope === "global" || element.isEnum && element.scope === undefined) && !options.module) write("export "); }
// writes the function signature describing element
function writeFunctionSignature(element, isConstructor, isTypeDef) { write("(");
var params = {};
// this type
if (element.this) params["this"] = { type: element.this.replace(/^{|}$/g, ""), optional: false };
// parameter types
if (element.params) element.params.forEach(function(param) { var path = param.name.split(/\./g); if (path.length === 1) params[param.name] = { type: getTypeOf(param), variable: param.variable === true, optional: param.optional === true, defaultValue: param.defaultvalue // Not used yet (TODO)
}; else // Property syntax (TODO)
params[path[0]].type = "{ [k: string]: any }"; });
var paramNames = Object.keys(params); paramNames.forEach(function(name, i) { var param = params[name]; var type = param.type; if (param.variable) { name = "..." + name; type = param.type.charAt(0) === "(" ? "any[]" : param.type + "[]"; } write(name, !param.variable && param.optional ? "?: " : ": ", type); if (i < paramNames.length - 1) write(", "); });
write(")");
// return type
if (!isConstructor) { write(isTypeDef ? " => " : ": "); var typeName; if (element.returns && element.returns.length && (typeName = getTypeOf(element.returns[0])) !== "undefined") write(typeName); else write("void"); } }
// writes (a typedef as) an interface
function writeInterface(element) { write("interface ", element.name); writeInterfaceBody(element); writeln(); }
function writeInterfaceBody(element) { writeln("{"); ++indent; if (element.tsType) writeln(element.tsType.replace(/\r?\n|\r/g, "\n")); else if (element.properties && element.properties.length) element.properties.forEach(writeProperty); --indent; write("}"); }
function writeProperty(property, declare) { writeComment(property.description); if (declare) write("let "); write(property.name); if (property.optional) write("?"); writeln(": ", getTypeOf(property), ";"); }
//
// Handlers
//
// handles a single element of any understood type
function handleElement(element, parent) { if (element.scope === "inner") return false;
if (element.optional !== true && element.type && element.type.names && element.type.names.length) { for (var i = 0; i < element.type.names.length; i++) { if (element.type.names[i].toLowerCase() === "undefined") { // This element is actually optional. Set optional to true and
// remove the 'undefined' type
element.optional = true; element.type.names.splice(i, 1); i--; } } }
if (seen[element.longname]) return true; if (isClassLike(element)) handleClass(element, parent); else switch (element.kind) { case "module": if (element.isEnum) { handleEnum(element, parent); break; } // eslint-disable-line no-fallthrough
case "namespace": handleNamespace(element, parent); break; case "constant": case "member": handleMember(element, parent); break; case "function": handleFunction(element, parent); break; case "typedef": handleTypeDef(element, parent); break; case "package": break; } seen[element.longname] = element; return true; }
// handles (just) a namespace
function handleNamespace(element/*, parent*/) { var children = getChildrenOf(element); if (!children.length) return; var first = true; if (element.properties) element.properties.forEach(function(property) { if (!/^[$\w]+$/.test(property.name)) // incompatible in namespace
return; if (first) { begin(element); writeln("namespace ", element.name, " {"); ++indent; first = false; } writeProperty(property, true); }); children.forEach(function(child) { if (child.scope === "inner" || seen[child.longname]) return; if (first) { begin(element); writeln("namespace ", element.name, " {"); ++indent; first = false; } handleElement(child, element); }); if (!first) { --indent; writeln("}"); } }
// a filter function to remove any module references
function notAModuleReference(ref) { return ref.indexOf("module:") === -1; }
// handles a class or class-like
function handleClass(element, parent) { var is_interface = isInterface(element); begin(element, is_interface); if (is_interface) write("interface "); else { if (element.virtual) write("abstract "); write("class "); } write(element.name); if (element.templates && element.templates.length) write("<", element.templates.join(", "), ">"); write(" ");
// extended classes
if (element.augments) { var augments = element.augments.filter(notAModuleReference); if (augments.length) write("extends ", augments[0], " "); }
// implemented interfaces
var impls = []; if (element.implements) Array.prototype.push.apply(impls, element.implements); if (element.mixes) Array.prototype.push.apply(impls, element.mixes); impls = impls.filter(notAModuleReference); if (impls.length) write("implements ", impls.join(", "), " ");
writeln("{"); ++indent;
if (element.tsType) writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
// constructor
if (!is_interface && !element.virtual) handleFunction(element, parent, true);
// properties
if (is_interface && element.properties) element.properties.forEach(function(property) { writeProperty(property); });
// class-compatible members
var incompatible = []; getChildrenOf(element).forEach(function(child) { if (isClassLike(child) || child.kind === "module" || child.kind === "typedef" || child.isEnum) { incompatible.push(child); return; } handleElement(child, element); });
--indent; writeln("}");
// class-incompatible members
if (incompatible.length) { writeln(); if (element.scope === "global" && !options.module) write("export "); writeln("namespace ", element.name, " {"); ++indent; incompatible.forEach(function(child) { handleElement(child, element); }); --indent; writeln("}"); } }
// handles an enum
function handleEnum(element) { begin(element);
var stringEnum = false; element.properties.forEach(function(property) { if (isNaN(property.defaultvalue)) { stringEnum = true; } }); if (stringEnum) { writeln("type ", element.name, " ="); ++indent; element.properties.forEach(function(property, i) { write(i === 0 ? "" : "| ", JSON.stringify(property.defaultvalue)); }); --indent; writeln(";"); } else { writeln("enum ", element.name, " {"); ++indent; element.properties.forEach(function(property, i) { write(property.name); if (property.defaultvalue !== undefined) write(" = ", JSON.stringify(property.defaultvalue)); if (i < element.properties.length - 1) writeln(","); else writeln(); }); --indent; writeln("}"); } }
// handles a namespace or class member
function handleMember(element, parent) { if (element.isEnum) { handleEnum(element); return; } begin(element);
var inClass = isClassLike(parent); if (inClass) { write(element.access || "public", " "); if (element.scope === "static") write("static "); if (element.readonly) write("readonly "); } else write(element.kind === "constant" ? "const " : "let ");
write(element.name); if (element.optional) write("?"); write(": ");
if (element.type && element.type.names && /^Object\b/i.test(element.type.names[0]) && element.properties) { writeln("{"); ++indent; element.properties.forEach(function(property, i) { writeln(JSON.stringify(property.name), ": ", getTypeOf(property), i < element.properties.length - 1 ? "," : ""); }); --indent; writeln("};"); } else writeln(getTypeOf(element), ";"); }
// handles a function or method
function handleFunction(element, parent, isConstructor) { var insideClass = true; if (isConstructor) { writeComment(element.comment); write("constructor"); } else { begin(element); insideClass = isClassLike(parent); if (insideClass) { write(element.access || "public", " "); if (element.scope === "static") write("static "); } else write("function "); write(element.name); if (element.templates && element.templates.length) write("<", element.templates.join(", "), ">"); } writeFunctionSignature(element, isConstructor, false); writeln(";"); if (!insideClass) handleNamespace(element); }
// handles a type definition (not a real type)
function handleTypeDef(element, parent) { if (isInterface(element)) { if (isClassLike(parent)) queuedInterfaces.push(element); else { begin(element); writeInterface(element); } } else { writeComment(element.comment, true); write("type ", element.name); if (element.templates && element.templates.length) write("<", element.templates.join(", "), ">"); write(" = "); if (element.tsType) write(element.tsType.replace(/\r?\n|\r/g, "\n")); else { var type = getTypeOf(element); if (element.type && element.type.names.length === 1 && element.type.names[0] === "function") writeFunctionSignature(element, false, true); else if (type === "object") { if (element.properties && element.properties.length) writeInterfaceBody(element); else write("{}"); } else write(type); } writeln(";"); } }
|