No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

publish.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  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. // ????: something changed in JSDoc 3.6.0? so that @exports + @enum does
  277. // no longer yield a 'global' scope, but is some sort of unscoped module
  278. // element now. The additional condition added below works around this.
  279. if ((element.scope === "global" || element.isEnum && element.scope === undefined) && !options.module)
  280. write("export ");
  281. }
  282. // writes the function signature describing element
  283. function writeFunctionSignature(element, isConstructor, isTypeDef) {
  284. write("(");
  285. var params = {};
  286. // this type
  287. if (element.this)
  288. params["this"] = {
  289. type: element.this.replace(/^{|}$/g, ""),
  290. optional: false
  291. };
  292. // parameter types
  293. if (element.params)
  294. element.params.forEach(function(param) {
  295. var path = param.name.split(/\./g);
  296. if (path.length === 1)
  297. params[param.name] = {
  298. type: getTypeOf(param),
  299. variable: param.variable === true,
  300. optional: param.optional === true,
  301. defaultValue: param.defaultvalue // Not used yet (TODO)
  302. };
  303. else // Property syntax (TODO)
  304. params[path[0]].type = "{ [k: string]: any }";
  305. });
  306. var paramNames = Object.keys(params);
  307. paramNames.forEach(function(name, i) {
  308. var param = params[name];
  309. var type = param.type;
  310. if (param.variable) {
  311. name = "..." + name;
  312. type = param.type.charAt(0) === "(" ? "any[]" : param.type + "[]";
  313. }
  314. write(name, !param.variable && param.optional ? "?: " : ": ", type);
  315. if (i < paramNames.length - 1)
  316. write(", ");
  317. });
  318. write(")");
  319. // return type
  320. if (!isConstructor) {
  321. write(isTypeDef ? " => " : ": ");
  322. var typeName;
  323. if (element.returns && element.returns.length && (typeName = getTypeOf(element.returns[0])) !== "undefined")
  324. write(typeName);
  325. else
  326. write("void");
  327. }
  328. }
  329. // writes (a typedef as) an interface
  330. function writeInterface(element) {
  331. write("interface ", element.name);
  332. writeInterfaceBody(element);
  333. writeln();
  334. }
  335. function writeInterfaceBody(element) {
  336. writeln("{");
  337. ++indent;
  338. if (element.tsType)
  339. writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
  340. else if (element.properties && element.properties.length)
  341. element.properties.forEach(writeProperty);
  342. --indent;
  343. write("}");
  344. }
  345. function writeProperty(property, declare) {
  346. writeComment(property.description);
  347. if (declare)
  348. write("let ");
  349. write(property.name);
  350. if (property.optional)
  351. write("?");
  352. writeln(": ", getTypeOf(property), ";");
  353. }
  354. //
  355. // Handlers
  356. //
  357. // handles a single element of any understood type
  358. function handleElement(element, parent) {
  359. if (element.scope === "inner")
  360. return false;
  361. if (element.optional !== true && element.type && element.type.names && element.type.names.length) {
  362. for (var i = 0; i < element.type.names.length; i++) {
  363. if (element.type.names[i].toLowerCase() === "undefined") {
  364. // This element is actually optional. Set optional to true and
  365. // remove the 'undefined' type
  366. element.optional = true;
  367. element.type.names.splice(i, 1);
  368. i--;
  369. }
  370. }
  371. }
  372. if (seen[element.longname])
  373. return true;
  374. if (isClassLike(element))
  375. handleClass(element, parent);
  376. else switch (element.kind) {
  377. case "module":
  378. if (element.isEnum) {
  379. handleEnum(element, parent);
  380. break;
  381. }
  382. // eslint-disable-line no-fallthrough
  383. case "namespace":
  384. handleNamespace(element, parent);
  385. break;
  386. case "constant":
  387. case "member":
  388. handleMember(element, parent);
  389. break;
  390. case "function":
  391. handleFunction(element, parent);
  392. break;
  393. case "typedef":
  394. handleTypeDef(element, parent);
  395. break;
  396. case "package":
  397. break;
  398. }
  399. seen[element.longname] = element;
  400. return true;
  401. }
  402. // handles (just) a namespace
  403. function handleNamespace(element/*, parent*/) {
  404. var children = getChildrenOf(element);
  405. if (!children.length)
  406. return;
  407. var first = true;
  408. if (element.properties)
  409. element.properties.forEach(function(property) {
  410. if (!/^[$\w]+$/.test(property.name)) // incompatible in namespace
  411. return;
  412. if (first) {
  413. begin(element);
  414. writeln("namespace ", element.name, " {");
  415. ++indent;
  416. first = false;
  417. }
  418. writeProperty(property, true);
  419. });
  420. children.forEach(function(child) {
  421. if (child.scope === "inner" || seen[child.longname])
  422. return;
  423. if (first) {
  424. begin(element);
  425. writeln("namespace ", element.name, " {");
  426. ++indent;
  427. first = false;
  428. }
  429. handleElement(child, element);
  430. });
  431. if (!first) {
  432. --indent;
  433. writeln("}");
  434. }
  435. }
  436. // a filter function to remove any module references
  437. function notAModuleReference(ref) {
  438. return ref.indexOf("module:") === -1;
  439. }
  440. // handles a class or class-like
  441. function handleClass(element, parent) {
  442. var is_interface = isInterface(element);
  443. begin(element, is_interface);
  444. if (is_interface)
  445. write("interface ");
  446. else {
  447. if (element.virtual)
  448. write("abstract ");
  449. write("class ");
  450. }
  451. write(element.name);
  452. if (element.templates && element.templates.length)
  453. write("<", element.templates.join(", "), ">");
  454. write(" ");
  455. // extended classes
  456. if (element.augments) {
  457. var augments = element.augments.filter(notAModuleReference);
  458. if (augments.length)
  459. write("extends ", augments[0], " ");
  460. }
  461. // implemented interfaces
  462. var impls = [];
  463. if (element.implements)
  464. Array.prototype.push.apply(impls, element.implements);
  465. if (element.mixes)
  466. Array.prototype.push.apply(impls, element.mixes);
  467. impls = impls.filter(notAModuleReference);
  468. if (impls.length)
  469. write("implements ", impls.join(", "), " ");
  470. writeln("{");
  471. ++indent;
  472. if (element.tsType)
  473. writeln(element.tsType.replace(/\r?\n|\r/g, "\n"));
  474. // constructor
  475. if (!is_interface && !element.virtual)
  476. handleFunction(element, parent, true);
  477. // properties
  478. if (is_interface && element.properties)
  479. element.properties.forEach(function(property) {
  480. writeProperty(property);
  481. });
  482. // class-compatible members
  483. var incompatible = [];
  484. getChildrenOf(element).forEach(function(child) {
  485. if (isClassLike(child) || child.kind === "module" || child.kind === "typedef" || child.isEnum) {
  486. incompatible.push(child);
  487. return;
  488. }
  489. handleElement(child, element);
  490. });
  491. --indent;
  492. writeln("}");
  493. // class-incompatible members
  494. if (incompatible.length) {
  495. writeln();
  496. if (element.scope === "global" && !options.module)
  497. write("export ");
  498. writeln("namespace ", element.name, " {");
  499. ++indent;
  500. incompatible.forEach(function(child) {
  501. handleElement(child, element);
  502. });
  503. --indent;
  504. writeln("}");
  505. }
  506. }
  507. // handles an enum
  508. function handleEnum(element) {
  509. begin(element);
  510. var stringEnum = false;
  511. element.properties.forEach(function(property) {
  512. if (isNaN(property.defaultvalue)) {
  513. stringEnum = true;
  514. }
  515. });
  516. if (stringEnum) {
  517. writeln("type ", element.name, " =");
  518. ++indent;
  519. element.properties.forEach(function(property, i) {
  520. write(i === 0 ? "" : "| ", JSON.stringify(property.defaultvalue));
  521. });
  522. --indent;
  523. writeln(";");
  524. } else {
  525. writeln("enum ", element.name, " {");
  526. ++indent;
  527. element.properties.forEach(function(property, i) {
  528. write(property.name);
  529. if (property.defaultvalue !== undefined)
  530. write(" = ", JSON.stringify(property.defaultvalue));
  531. if (i < element.properties.length - 1)
  532. writeln(",");
  533. else
  534. writeln();
  535. });
  536. --indent;
  537. writeln("}");
  538. }
  539. }
  540. // handles a namespace or class member
  541. function handleMember(element, parent) {
  542. if (element.isEnum) {
  543. handleEnum(element);
  544. return;
  545. }
  546. begin(element);
  547. var inClass = isClassLike(parent);
  548. if (inClass) {
  549. write(element.access || "public", " ");
  550. if (element.scope === "static")
  551. write("static ");
  552. if (element.readonly)
  553. write("readonly ");
  554. } else
  555. write(element.kind === "constant" ? "const " : "let ");
  556. write(element.name);
  557. if (element.optional)
  558. write("?");
  559. write(": ");
  560. if (element.type && element.type.names && /^Object\b/i.test(element.type.names[0]) && element.properties) {
  561. writeln("{");
  562. ++indent;
  563. element.properties.forEach(function(property, i) {
  564. writeln(JSON.stringify(property.name), ": ", getTypeOf(property), i < element.properties.length - 1 ? "," : "");
  565. });
  566. --indent;
  567. writeln("};");
  568. } else
  569. writeln(getTypeOf(element), ";");
  570. }
  571. // handles a function or method
  572. function handleFunction(element, parent, isConstructor) {
  573. var insideClass = true;
  574. if (isConstructor) {
  575. writeComment(element.comment);
  576. write("constructor");
  577. } else {
  578. begin(element);
  579. insideClass = isClassLike(parent);
  580. if (insideClass) {
  581. write(element.access || "public", " ");
  582. if (element.scope === "static")
  583. write("static ");
  584. } else
  585. write("function ");
  586. write(element.name);
  587. if (element.templates && element.templates.length)
  588. write("<", element.templates.join(", "), ">");
  589. }
  590. writeFunctionSignature(element, isConstructor, false);
  591. writeln(";");
  592. if (!insideClass)
  593. handleNamespace(element);
  594. }
  595. // handles a type definition (not a real type)
  596. function handleTypeDef(element, parent) {
  597. if (isInterface(element)) {
  598. if (isClassLike(parent))
  599. queuedInterfaces.push(element);
  600. else {
  601. begin(element);
  602. writeInterface(element);
  603. }
  604. } else {
  605. writeComment(element.comment, true);
  606. write("type ", element.name);
  607. if (element.templates && element.templates.length)
  608. write("<", element.templates.join(", "), ">");
  609. write(" = ");
  610. if (element.tsType)
  611. write(element.tsType.replace(/\r?\n|\r/g, "\n"));
  612. else {
  613. var type = getTypeOf(element);
  614. if (element.type && element.type.names.length === 1 && element.type.names[0] === "function")
  615. writeFunctionSignature(element, false, true);
  616. else if (type === "object") {
  617. if (element.properties && element.properties.length)
  618. writeInterfaceBody(element);
  619. else
  620. write("{}");
  621. } else
  622. write(type);
  623. }
  624. writeln(";");
  625. }
  626. }