publish.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. "use strict";
  2. var fs = require("fs");
  3. // output stream
  4. var out = null;
  5. // documentation data
  6. var data = null;
  7. // already handled objects, by name
  8. var seen = {};
  9. // indentation level
  10. var indent = 0;
  11. // whether indent has been written for the current line yet
  12. var indentWritten = false;
  13. // provided options
  14. var options = {};
  15. // queued interfaces
  16. var queuedInterfaces = [];
  17. // whether writing the first line
  18. var firstLine = true;
  19. // JSDoc hook
  20. exports.publish = function publish(taffy, opts) {
  21. options = opts || {};
  22. // query overrides options
  23. if (options.query)
  24. Object.keys(options.query).forEach(function(key) {
  25. if (key !== "query")
  26. switch (options[key] = options.query[key]) {
  27. case "true":
  28. options[key] = true;
  29. break;
  30. case "false":
  31. options[key] = false;
  32. break;
  33. case "null":
  34. options[key] = null;
  35. break;
  36. }
  37. });
  38. // remove undocumented
  39. taffy({ undocumented: true }).remove();
  40. taffy({ ignore: true }).remove();
  41. taffy({ inherited: true }).remove();
  42. // remove private
  43. if (!options.private)
  44. taffy({ access: "private" }).remove();
  45. // setup output
  46. out = options.destination
  47. ? fs.createWriteStream(options.destination)
  48. : process.stdout;
  49. try {
  50. // setup environment
  51. data = taffy().get();
  52. indent = 0;
  53. indentWritten = false;
  54. firstLine = true;
  55. // wrap everything in a module if configured
  56. if (options.module) {
  57. writeln("export = ", options.module, ";");
  58. writeln();
  59. writeln("declare namespace ", options.module, " {");
  60. writeln();
  61. ++indent;
  62. }
  63. // handle all
  64. getChildrenOf(undefined).forEach(function(child) {
  65. handleElement(child, null);
  66. });
  67. // process queued
  68. while (queuedInterfaces.length) {
  69. var element = queuedInterfaces.shift();
  70. begin(element);
  71. writeInterface(element);
  72. writeln(";");
  73. }
  74. // end wrap
  75. if (options.module) {
  76. --indent;
  77. writeln("}");
  78. }
  79. // close file output
  80. if (out !== process.stdout)
  81. out.end();
  82. } finally {
  83. // gc environment objects
  84. out = data = null;
  85. seen = options = {};
  86. queuedInterfaces = [];
  87. }
  88. };
  89. //
  90. // Utility
  91. //
  92. // writes one or multiple strings
  93. function write() {
  94. var s = Array.prototype.slice.call(arguments).join("");
  95. if (!indentWritten) {
  96. for (var i = 0; i < indent; ++i)
  97. s = " " + s;
  98. indentWritten = true;
  99. }
  100. out.write(s);
  101. firstLine = false;
  102. }
  103. // writes zero or multiple strings, followed by a new line
  104. function writeln() {
  105. var s = Array.prototype.slice.call(arguments).join("");
  106. if (s.length)
  107. write(s, "\n");
  108. else if (!firstLine)
  109. out.write("\n");
  110. indentWritten = false;
  111. }
  112. var keepTags = [
  113. "param",
  114. "returns",
  115. "throws",
  116. "see"
  117. ];
  118. // parses a comment into text and tags
  119. function parseComment(comment) {
  120. var lines = comment.replace(/^ *\/\*\* *|^ *\*\/| *\*\/ *$|^ *\* */mg, "").trim().split(/\r?\n|\r/g); // property.description has just "\r" ?!
  121. var desc;
  122. var text = [];
  123. var tags = null;
  124. for (var i = 0; i < lines.length; ++i) {
  125. var match = /^@(\w+)\b/.exec(lines[i]);
  126. if (match) {
  127. if (!tags) {
  128. tags = [];
  129. desc = text;
  130. }
  131. text = [];
  132. tags.push({ name: match[1], text: text });
  133. lines[i] = lines[i].substring(match[1].length + 1).trim();
  134. }
  135. if (lines[i].length || text.length)
  136. text.push(lines[i]);
  137. }
  138. return {
  139. text: desc || text,
  140. tags: tags || []
  141. };
  142. }
  143. // writes a comment
  144. function writeComment(comment, otherwiseNewline) {
  145. if (!comment || options.comments === false) {
  146. if (otherwiseNewline)
  147. writeln();
  148. return;
  149. }
  150. if (typeof comment !== "object")
  151. comment = parseComment(comment);
  152. comment.tags = comment.tags.filter(function(tag) {
  153. return keepTags.indexOf(tag.name) > -1 && (tag.name !== "returns" || tag.text[0] !== "{undefined}");
  154. });
  155. writeln();
  156. if (!comment.tags.length && comment.text.length < 2) {
  157. writeln("/** " + comment.text[0] + " */");
  158. return;
  159. }
  160. writeln("/**");
  161. comment.text.forEach(function(line) {
  162. if (line.length)
  163. writeln(" * ", line);
  164. else
  165. writeln(" *");
  166. });
  167. comment.tags.forEach(function(tag) {
  168. var started = false;
  169. if (tag.text.length) {
  170. tag.text.forEach(function(line, i) {
  171. if (i > 0)
  172. write(" * ");
  173. else if (tag.name !== "throws")
  174. line = line.replace(/^\{[^\s]*} ?/, "");
  175. if (!line.length)
  176. return;
  177. if (!started) {
  178. write(" * @", tag.name, " ");
  179. started = true;
  180. }
  181. writeln(line);
  182. });
  183. }
  184. });
  185. writeln(" */");
  186. }
  187. // recursively replaces all occurencies of re's match
  188. function replaceRecursive(name, re, fn) {
  189. var found;
  190. function replacer() {
  191. found = true;
  192. return fn.apply(null, arguments);
  193. }
  194. do {
  195. found = false;
  196. name = name.replace(re, replacer);
  197. } while (found);
  198. return name;
  199. }
  200. // tests if an element is considered to be a class or class-like
  201. function isClassLike(element) {
  202. return isClass(element) || isInterface(element);
  203. }
  204. // tests if an element is considered to be a class
  205. function isClass(element) {
  206. return element && element.kind === "class";
  207. }
  208. // tests if an element is considered to be an interface
  209. function isInterface(element) {
  210. return element && (element.kind === "interface" || element.kind === "mixin");
  211. }
  212. // tests if an element is considered to be a namespace
  213. function isNamespace(element) {
  214. return element && (element.kind === "namespace" || element.kind === "module");
  215. }
  216. // gets all children of the specified parent
  217. function getChildrenOf(parent) {
  218. var memberof = parent ? parent.longname : undefined;
  219. return data.filter(function(element) {
  220. return element.memberof === memberof;
  221. });
  222. }
  223. // gets the literal type of an element
  224. function getTypeOf(element) {
  225. if (element.tsType)
  226. return element.tsType.replace(/\r?\n|\r/g, "\n");
  227. var name = "any";
  228. var type = element.type;
  229. if (type && type.names && type.names.length) {
  230. if (type.names.length === 1)
  231. name = element.type.names[0].trim();
  232. else
  233. name = "(" + element.type.names.join("|") + ")";
  234. } else
  235. return name;
  236. // Replace catchalls with any
  237. name = name.replace(/\*|\bmixed\b/g, "any");
  238. // Ensure upper case Object for map expressions below
  239. name = name.replace(/\bobject\b/g, "Object");
  240. // Correct Something.<Something> to Something<Something>
  241. name = replaceRecursive(name, /\b(?!Object|Array)([\w$]+)\.<([^>]*)>/gi, function($0, $1, $2) {
  242. return $1 + "<" + $2 + ">";
  243. });
  244. // Replace Array.<string> with string[]
  245. name = replaceRecursive(name, /\bArray\.?<([^>]*)>/gi, function($0, $1) {
  246. return $1 + "[]";
  247. });
  248. // Replace Object.<string,number> with { [k: string]: number }
  249. name = replaceRecursive(name, /\bObject\.?<([^,]*), *([^>]*)>/gi, function($0, $1, $2) {
  250. return "{ [k: " + $1 + "]: " + $2 + " }";
  251. });
  252. // Replace functions (there are no signatures) with Function
  253. name = name.replace(/\bfunction(?:\(\))?\b/g, "Function");
  254. // Convert plain Object back to just object
  255. name = name.replace(/\b(Object\b(?!\.))/g, function($0, $1) {
  256. return $1.toLowerCase();
  257. });
  258. return name;
  259. }
  260. // begins writing the definition of the specified element
  261. function begin(element, is_interface) {
  262. if (!seen[element.longname]) {
  263. if (isClass(element)) {
  264. var comment = parseComment(element.comment);
  265. var classdesc = comment.tags.find(function(tag) { return tag.name === "classdesc"; });
  266. if (classdesc) {
  267. comment.text = classdesc.text;
  268. comment.tags = [];
  269. }
  270. writeComment(comment, true);
  271. } else
  272. writeComment(element.comment, is_interface || isClassLike(element) || isNamespace(element) || element.isEnum || element.scope === "global");
  273. seen[element.longname] = element;
  274. } else
  275. writeln();
  276. if (element.scope !== "global" || options.module)
  277. return;
  278. write("export ");
  279. }
  280. // writes the function signature describing element
  281. function writeFunctionSignature(element, isConstructor, isTypeDef) {
  282. write("(");
  283. var params = {};
  284. // this type
  285. if (element.this)
  286. params["this"] = {
  287. type: element.this.replace(/^{|}$/g, ""),
  288. optional: false
  289. };
  290. // parameter types
  291. if (element.params)
  292. element.params.forEach(function(param) {
  293. var path = param.name.split(/\./g);
  294. if (path.length === 1)
  295. params[param.name] = {
  296. type: getTypeOf(param),
  297. variable: param.variable === true,
  298. optional: param.optional === true,
  299. defaultValue: param.defaultvalue // Not used yet (TODO)
  300. };
  301. else // Property syntax (TODO)
  302. params[path[0]].type = "{ [k: string]: any }";
  303. });
  304. var paramNames = Object.keys(params);
  305. paramNames.forEach(function(name, i) {
  306. var param = params[name];
  307. var type = param.type;
  308. if (param.variable) {
  309. name = "..." + name;
  310. type = param.type.charAt(0) === "(" ? "any[]" : param.type + "[]";
  311. }
  312. write(name, !param.variable && param.optional ? "?: " : ": ", type);
  313. if (i < paramNames.length - 1)
  314. write(", ");
  315. });
  316. write(")");
  317. // return type
  318. if (!isConstructor) {
  319. write(isTypeDef ? " => " : ": ");
  320. var typeName;
  321. if (element.returns && element.returns.length && (typeName = getTypeOf(element.returns[0])) !== "undefined")
  322. write(typeName);
  323. else
  324. write("void");
  325. }
  326. }
  327. // writes (a typedef as) an interface
  328. function writeInterface(element) {
  329. write("interface ", element.name);
  330. writeInterfaceBody(element);
  331. writeln();
  332. }
  333. function writeInterfaceBody(element) {
  334. writeln("{");
  335. ++indent;
  336. if (element.tsType)
  337. writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
  338. else if (element.properties && element.properties.length)
  339. element.properties.forEach(writeProperty);
  340. --indent;
  341. write("}");
  342. }
  343. function writeProperty(property, declare) {
  344. writeComment(property.description);
  345. if (declare)
  346. write("let ");
  347. write(property.name);
  348. if (property.optional)
  349. write("?");
  350. writeln(": ", getTypeOf(property), ";");
  351. }
  352. //
  353. // Handlers
  354. //
  355. // handles a single element of any understood type
  356. function handleElement(element, parent) {
  357. if (element.scope === "inner")
  358. return false;
  359. if (element.optional !== true && element.type && element.type.names && element.type.names.length) {
  360. for (var i = 0; i < element.type.names.length; i++) {
  361. if (element.type.names[i].toLowerCase() === "undefined") {
  362. // This element is actually optional. Set optional to true and
  363. // remove the 'undefined' type
  364. element.optional = true;
  365. element.type.names.splice(i, 1);
  366. i--;
  367. }
  368. }
  369. }
  370. if (seen[element.longname])
  371. return true;
  372. if (isClassLike(element))
  373. handleClass(element, parent);
  374. else switch (element.kind) {
  375. case "module":
  376. case "namespace":
  377. handleNamespace(element, parent);
  378. break;
  379. case "constant":
  380. case "member":
  381. handleMember(element, parent);
  382. break;
  383. case "function":
  384. handleFunction(element, parent);
  385. break;
  386. case "typedef":
  387. handleTypeDef(element, parent);
  388. break;
  389. case "package":
  390. break;
  391. }
  392. seen[element.longname] = element;
  393. return true;
  394. }
  395. // handles (just) a namespace
  396. function handleNamespace(element/*, parent*/) {
  397. var children = getChildrenOf(element);
  398. if (!children.length)
  399. return;
  400. var first = true;
  401. if (element.properties)
  402. element.properties.forEach(function(property) {
  403. if (!/^[$\w]+$/.test(property.name)) // incompatible in namespace
  404. return;
  405. if (first) {
  406. begin(element);
  407. writeln("namespace ", element.name, " {");
  408. ++indent;
  409. first = false;
  410. }
  411. writeProperty(property, true);
  412. });
  413. children.forEach(function(child) {
  414. if (child.scope === "inner" || seen[child.longname])
  415. return;
  416. if (first) {
  417. begin(element);
  418. writeln("namespace ", element.name, " {");
  419. ++indent;
  420. first = false;
  421. }
  422. handleElement(child, element);
  423. });
  424. if (!first) {
  425. --indent;
  426. writeln("}");
  427. }
  428. }
  429. // a filter function to remove any module references
  430. function notAModuleReference(ref) {
  431. return ref.indexOf("module:") === -1;
  432. }
  433. // handles a class or class-like
  434. function handleClass(element, parent) {
  435. var is_interface = isInterface(element);
  436. begin(element, is_interface);
  437. if (is_interface)
  438. write("interface ");
  439. else {
  440. if (element.virtual)
  441. write("abstract ");
  442. write("class ");
  443. }
  444. write(element.name);
  445. if (element.templates && element.templates.length)
  446. write("<", element.templates.join(", "), ">");
  447. write(" ");
  448. // extended classes
  449. if (element.augments) {
  450. var augments = element.augments.filter(notAModuleReference);
  451. if (augments.length)
  452. write("extends ", augments[0], " ");
  453. }
  454. // implemented interfaces
  455. var impls = [];
  456. if (element.implements)
  457. Array.prototype.push.apply(impls, element.implements);
  458. if (element.mixes)
  459. Array.prototype.push.apply(impls, element.mixes);
  460. impls = impls.filter(notAModuleReference);
  461. if (impls.length)
  462. write("implements ", impls.join(", "), " ");
  463. writeln("{");
  464. ++indent;
  465. if (element.tsType)
  466. writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
  467. // constructor
  468. if (!is_interface && !element.virtual)
  469. handleFunction(element, parent, true);
  470. // properties
  471. if (is_interface && element.properties)
  472. element.properties.forEach(function(property) {
  473. writeProperty(property);
  474. });
  475. // class-compatible members
  476. var incompatible = [];
  477. getChildrenOf(element).forEach(function(child) {
  478. if (isClassLike(child) || child.kind === "module" || child.kind === "typedef" || child.isEnum) {
  479. incompatible.push(child);
  480. return;
  481. }
  482. handleElement(child, element);
  483. });
  484. --indent;
  485. writeln("}");
  486. // class-incompatible members
  487. if (incompatible.length) {
  488. writeln();
  489. if (element.scope === "global" && !options.module)
  490. write("export ");
  491. writeln("namespace ", element.name, " {");
  492. ++indent;
  493. incompatible.forEach(function(child) {
  494. handleElement(child, element);
  495. });
  496. --indent;
  497. writeln("}");
  498. }
  499. }
  500. // handles a namespace or class member
  501. function handleMember(element, parent) {
  502. begin(element);
  503. if (element.isEnum) {
  504. var stringEnum = false;
  505. element.properties.forEach(function(property) {
  506. if (isNaN(property.defaultvalue)) {
  507. stringEnum = true;
  508. }
  509. });
  510. if (stringEnum) {
  511. writeln("type ", element.name, " =");
  512. ++indent;
  513. element.properties.forEach(function(property, i) {
  514. write(i === 0 ? "" : "| ", JSON.stringify(property.defaultvalue));
  515. });
  516. --indent;
  517. writeln(";");
  518. } else {
  519. writeln("enum ", element.name, " {");
  520. ++indent;
  521. element.properties.forEach(function(property, i) {
  522. write(property.name);
  523. if (property.defaultvalue !== undefined)
  524. write(" = ", JSON.stringify(property.defaultvalue));
  525. if (i < element.properties.length - 1)
  526. writeln(",");
  527. else
  528. writeln();
  529. });
  530. --indent;
  531. writeln("}");
  532. }
  533. } else {
  534. var inClass = isClassLike(parent);
  535. if (inClass) {
  536. write(element.access || "public", " ");
  537. if (element.scope === "static")
  538. write("static ");
  539. if (element.readonly)
  540. write("readonly ");
  541. } else
  542. write(element.kind === "constant" ? "const " : "let ");
  543. write(element.name);
  544. if (element.optional)
  545. write("?");
  546. write(": ");
  547. if (element.type && element.type.names && /^Object\b/i.test(element.type.names[0]) && element.properties) {
  548. writeln("{");
  549. ++indent;
  550. element.properties.forEach(function(property, i) {
  551. writeln(JSON.stringify(property.name), ": ", getTypeOf(property), i < element.properties.length - 1 ? "," : "");
  552. });
  553. --indent;
  554. writeln("};");
  555. } else
  556. writeln(getTypeOf(element), ";");
  557. }
  558. }
  559. // handles a function or method
  560. function handleFunction(element, parent, isConstructor) {
  561. var insideClass = true;
  562. if (isConstructor) {
  563. writeComment(element.comment);
  564. write("constructor");
  565. } else {
  566. begin(element);
  567. insideClass = isClassLike(parent);
  568. if (insideClass) {
  569. write(element.access || "public", " ");
  570. if (element.scope === "static")
  571. write("static ");
  572. } else
  573. write("function ");
  574. write(element.name);
  575. if (element.templates && element.templates.length)
  576. write("<", element.templates.join(", "), ">");
  577. }
  578. writeFunctionSignature(element, isConstructor, false);
  579. writeln(";");
  580. if (!insideClass)
  581. handleNamespace(element);
  582. }
  583. // handles a type definition (not a real type)
  584. function handleTypeDef(element, parent) {
  585. if (isInterface(element)) {
  586. if (isClassLike(parent))
  587. queuedInterfaces.push(element);
  588. else {
  589. begin(element);
  590. writeInterface(element);
  591. }
  592. } else {
  593. writeComment(element.comment, true);
  594. write("type ", element.name);
  595. if (element.templates && element.templates.length)
  596. write("<", element.templates.join(", "), ">");
  597. write(" = ");
  598. if (element.tsType)
  599. write(element.tsType.replace(/\r?\n|\r/g, "\n"));
  600. else {
  601. var type = getTypeOf(element);
  602. if (element.type && element.type.names.length === 1 && element.type.names[0] === "function")
  603. writeFunctionSignature(element, false, true);
  604. else if (type === "object") {
  605. if (element.properties && element.properties.length)
  606. writeInterfaceBody(element);
  607. else
  608. write("{}");
  609. } else
  610. write(type);
  611. }
  612. writeln(";");
  613. }
  614. }