publish.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /* eslint-disable strict */
  2. 'use strict';
  3. const fs = require('fs');
  4. class JSDoclet {
  5. constructor(doc) {
  6. this.doc = doc;
  7. this.description = doc['description'] || '';
  8. this.name = doc['name'] || 'unknown';
  9. this.longname = doc['longname'] || '';
  10. this.types = [];
  11. if (doc['type'] && doc['type']['names']) {
  12. this.types = doc['type']['names'].slice();
  13. }
  14. this.type = this.types.length > 0 ? this.types.join('\\|') : '*';
  15. this.variable = doc['variable'] || false;
  16. this.kind = doc['kind'] || '';
  17. this.memberof = doc['memberof'] || null;
  18. this.scope = doc['scope'] || '';
  19. this.members = [];
  20. this.optional = doc['optional'] || false;
  21. this.defaultvalue = doc['defaultvalue'];
  22. this.summary = doc['summary'] || null;
  23. this.classdesc = doc['classdesc'] || null;
  24. // Parameters (functions)
  25. this.params = [];
  26. this.returns = doc['returns'] ? doc['returns'][0]['type']['names'][0] : 'void';
  27. this.returns_desc = doc['returns'] ? doc['returns'][0]['description'] : null;
  28. this.params = (doc['params'] || []).slice().map((p) => new JSDoclet(p));
  29. // Custom tags
  30. this.tags = doc['tags'] || [];
  31. this.header = this.tags.filter((t) => t['title'] === 'header').map((t) => t['text']).pop() || null;
  32. }
  33. add_member(obj) {
  34. this.members.push(obj);
  35. }
  36. is_static() {
  37. return this.scope === 'static';
  38. }
  39. is_instance() {
  40. return this.scope === 'instance';
  41. }
  42. is_object() {
  43. return this.kind === 'Object' || (this.kind === 'typedef' && this.type === 'Object');
  44. }
  45. is_class() {
  46. return this.kind === 'class';
  47. }
  48. is_function() {
  49. return this.kind === 'function' || (this.kind === 'typedef' && this.type === 'function');
  50. }
  51. is_module() {
  52. return this.kind === 'module';
  53. }
  54. }
  55. function format_table(f, data, depth = 0) {
  56. if (!data.length) {
  57. return;
  58. }
  59. const column_sizes = new Array(data[0].length).fill(0);
  60. data.forEach((row) => {
  61. row.forEach((e, idx) => {
  62. column_sizes[idx] = Math.max(e.length, column_sizes[idx]);
  63. });
  64. });
  65. const indent = ' '.repeat(depth);
  66. let sep = indent;
  67. column_sizes.forEach((size) => {
  68. sep += '+';
  69. sep += '-'.repeat(size + 2);
  70. });
  71. sep += '+\n';
  72. f.write(sep);
  73. data.forEach((row) => {
  74. let row_text = `${indent}|`;
  75. row.forEach((entry, idx) => {
  76. row_text += ` ${entry.padEnd(column_sizes[idx])} |`;
  77. });
  78. row_text += '\n';
  79. f.write(row_text);
  80. f.write(sep);
  81. });
  82. f.write('\n');
  83. }
  84. function make_header(header, sep) {
  85. return `${header}\n${sep.repeat(header.length)}\n\n`;
  86. }
  87. function indent_multiline(text, depth) {
  88. const indent = ' '.repeat(depth);
  89. return text.split('\n').map((l) => (l === '' ? l : indent + l)).join('\n');
  90. }
  91. function make_rst_signature(obj, types = false, style = false) {
  92. let out = '';
  93. const fmt = style ? '*' : '';
  94. obj.params.forEach((arg, idx) => {
  95. if (idx > 0) {
  96. if (arg.optional) {
  97. out += ` ${fmt}[`;
  98. }
  99. out += ', ';
  100. } else {
  101. out += ' ';
  102. if (arg.optional) {
  103. out += `${fmt}[ `;
  104. }
  105. }
  106. if (types) {
  107. out += `${arg.type} `;
  108. }
  109. const variable = arg.variable ? '...' : '';
  110. const defval = arg.defaultvalue !== undefined ? `=${arg.defaultvalue}` : '';
  111. out += `${variable}${arg.name}${defval}`;
  112. if (arg.optional) {
  113. out += ` ]${fmt}`;
  114. }
  115. });
  116. out += ' ';
  117. return out;
  118. }
  119. function make_rst_param(f, obj, depth = 0) {
  120. const indent = ' '.repeat(depth * 3);
  121. f.write(indent);
  122. f.write(`:param ${obj.type} ${obj.name}:\n`);
  123. f.write(indent_multiline(obj.description, (depth + 1) * 3));
  124. f.write('\n\n');
  125. }
  126. function make_rst_attribute(f, obj, depth = 0, brief = false) {
  127. const indent = ' '.repeat(depth * 3);
  128. f.write(indent);
  129. f.write(`.. js:attribute:: ${obj.name}\n\n`);
  130. if (brief) {
  131. if (obj.summary) {
  132. f.write(indent_multiline(obj.summary, (depth + 1) * 3));
  133. }
  134. f.write('\n\n');
  135. return;
  136. }
  137. f.write(indent_multiline(obj.description, (depth + 1) * 3));
  138. f.write('\n\n');
  139. f.write(indent);
  140. f.write(` :type: ${obj.type}\n\n`);
  141. if (obj.defaultvalue !== undefined) {
  142. let defval = obj.defaultvalue;
  143. if (defval === '') {
  144. defval = '""';
  145. }
  146. f.write(indent);
  147. f.write(` :value: \`\`${defval}\`\`\n\n`);
  148. }
  149. }
  150. function make_rst_function(f, obj, depth = 0) {
  151. let prefix = '';
  152. if (obj.is_instance()) {
  153. prefix = 'prototype.';
  154. }
  155. const indent = ' '.repeat(depth * 3);
  156. const sig = make_rst_signature(obj);
  157. f.write(indent);
  158. f.write(`.. js:function:: ${prefix}${obj.name}(${sig})\n`);
  159. f.write('\n');
  160. f.write(indent_multiline(obj.description, (depth + 1) * 3));
  161. f.write('\n\n');
  162. obj.params.forEach((param) => {
  163. make_rst_param(f, param, depth + 1);
  164. });
  165. if (obj.returns !== 'void') {
  166. f.write(indent);
  167. f.write(' :return:\n');
  168. f.write(indent_multiline(obj.returns_desc, (depth + 2) * 3));
  169. f.write('\n\n');
  170. f.write(indent);
  171. f.write(` :rtype: ${obj.returns}\n\n`);
  172. }
  173. }
  174. function make_rst_object(f, obj) {
  175. let brief = false;
  176. // Our custom header flag.
  177. if (obj.header !== null) {
  178. f.write(make_header(obj.header, '-'));
  179. f.write(`${obj.description}\n\n`);
  180. brief = true;
  181. }
  182. // Format members table and descriptions
  183. const data = [['type', 'name']].concat(obj.members.map((m) => [m.type, `:js:attr:\`${m.name}\``]));
  184. f.write(make_header('Properties', '^'));
  185. format_table(f, data, 0);
  186. make_rst_attribute(f, obj, 0, brief);
  187. if (!obj.members.length) {
  188. return;
  189. }
  190. f.write(' **Property Descriptions**\n\n');
  191. // Properties first
  192. obj.members.filter((m) => !m.is_function()).forEach((m) => {
  193. make_rst_attribute(f, m, 1);
  194. });
  195. // Callbacks last
  196. obj.members.filter((m) => m.is_function()).forEach((m) => {
  197. make_rst_function(f, m, 1);
  198. });
  199. }
  200. function make_rst_class(f, obj) {
  201. const header = obj.header ? obj.header : obj.name;
  202. f.write(make_header(header, '-'));
  203. if (obj.classdesc) {
  204. f.write(`${obj.classdesc}\n\n`);
  205. }
  206. const funcs = obj.members.filter((m) => m.is_function());
  207. function make_data(m) {
  208. const base = m.is_static() ? obj.name : `${obj.name}.prototype`;
  209. const params = make_rst_signature(m, true, true);
  210. const sig = `:js:attr:\`${m.name} <${base}.${m.name}>\` **(**${params}**)**`;
  211. return [m.returns, sig];
  212. }
  213. const sfuncs = funcs.filter((m) => m.is_static());
  214. const ifuncs = funcs.filter((m) => !m.is_static());
  215. f.write(make_header('Static Methods', '^'));
  216. format_table(f, sfuncs.map((m) => make_data(m)));
  217. f.write(make_header('Instance Methods', '^'));
  218. format_table(f, ifuncs.map((m) => make_data(m)));
  219. const sig = make_rst_signature(obj);
  220. f.write(`.. js:class:: ${obj.name}(${sig})\n\n`);
  221. f.write(indent_multiline(obj.description, 3));
  222. f.write('\n\n');
  223. obj.params.forEach((p) => {
  224. make_rst_param(f, p, 1);
  225. });
  226. f.write(' **Static Methods**\n\n');
  227. sfuncs.forEach((m) => {
  228. make_rst_function(f, m, 1);
  229. });
  230. f.write(' **Instance Methods**\n\n');
  231. ifuncs.forEach((m) => {
  232. make_rst_function(f, m, 1);
  233. });
  234. }
  235. function make_rst_module(f, obj) {
  236. const header = obj.header !== null ? obj.header : obj.name;
  237. f.write(make_header(header, '='));
  238. f.write(obj.description);
  239. f.write('\n\n');
  240. }
  241. function write_base_object(f, obj) {
  242. if (obj.is_object()) {
  243. make_rst_object(f, obj);
  244. } else if (obj.is_function()) {
  245. make_rst_function(f, obj);
  246. } else if (obj.is_class()) {
  247. make_rst_class(f, obj);
  248. } else if (obj.is_module()) {
  249. make_rst_module(f, obj);
  250. }
  251. }
  252. function generate(f, docs) {
  253. const globs = [];
  254. const SYMBOLS = {};
  255. docs.filter((d) => !d.ignore && d.kind !== 'package').forEach((d) => {
  256. SYMBOLS[d.name] = d;
  257. if (d.memberof) {
  258. const up = SYMBOLS[d.memberof];
  259. if (up === undefined) {
  260. console.log(d); // eslint-disable-line no-console
  261. console.log(`Undefined symbol! ${d.memberof}`); // eslint-disable-line no-console
  262. throw new Error('Undefined symbol!');
  263. }
  264. SYMBOLS[d.memberof].add_member(d);
  265. } else {
  266. globs.push(d);
  267. }
  268. });
  269. f.write('.. _doc_html5_shell_classref:\n\n');
  270. globs.forEach((obj) => write_base_object(f, obj));
  271. }
  272. /**
  273. * Generate documentation output.
  274. *
  275. * @param {TAFFY} data - A TaffyDB collection representing
  276. * all the symbols documented in your code.
  277. * @param {object} opts - An object with options information.
  278. */
  279. exports.publish = function (data, opts) {
  280. const docs = data().get().filter((doc) => !doc.undocumented && !doc.ignore).map((doc) => new JSDoclet(doc));
  281. const dest = opts.destination;
  282. if (dest === 'dry-run') {
  283. process.stdout.write('Dry run... ');
  284. generate({
  285. write: function () { /* noop */ },
  286. }, docs);
  287. process.stdout.write('Okay!\n');
  288. return;
  289. }
  290. if (dest !== '' && !dest.endsWith('.rst')) {
  291. throw new Error('Destination file must be either a ".rst" file, or an empty string (for printing to stdout)');
  292. }
  293. if (dest !== '') {
  294. const f = fs.createWriteStream(dest);
  295. generate(f, docs);
  296. } else {
  297. generate(process.stdout, docs);
  298. }
  299. };