publish.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. const doop = require('jsdoc/util/doop');
  2. const env = require('jsdoc/env');
  3. const fs = require('jsdoc/fs');
  4. const helper = require('jsdoc/util/templateHelper');
  5. const logger = require('jsdoc/util/logger');
  6. const path = require('jsdoc/path');
  7. const { taffy } = require('@jsdoc/salty');
  8. const template = require('jsdoc/template');
  9. const util = require('util');
  10. const htmlsafe = helper.htmlsafe;
  11. const linkto = helper.linkto;
  12. const resolveAuthorLinks = helper.resolveAuthorLinks;
  13. const hasOwnProp = Object.prototype.hasOwnProperty;
  14. let data;
  15. let view;
  16. let outdir = path.normalize(env.opts.destination);
  17. function find(spec) {
  18. return helper.find(data, spec);
  19. }
  20. function tutoriallink(tutorial) {
  21. return helper.toTutorial(tutorial, null, {
  22. tag: 'em',
  23. classname: 'disabled',
  24. prefix: 'Tutorial: '
  25. });
  26. }
  27. function getAncestorLinks(doclet) {
  28. return helper.getAncestorLinks(data, doclet);
  29. }
  30. function hashToLink(doclet, hash) {
  31. let url;
  32. if ( !/^(#.+)/.test(hash) ) {
  33. return hash;
  34. }
  35. url = helper.createLink(doclet);
  36. url = url.replace(/(#.+|$)/, hash);
  37. return `<a href="${url}">${hash}</a>`;
  38. }
  39. function needsSignature({kind, type, meta}) {
  40. let needsSig = false;
  41. // function and class definitions always get a signature
  42. if (kind === 'function' || kind === 'class') {
  43. needsSig = true;
  44. }
  45. // typedefs that contain functions get a signature, too
  46. else if (kind === 'typedef' && type && type.names &&
  47. type.names.length) {
  48. for (let i = 0, l = type.names.length; i < l; i++) {
  49. if (type.names[i].toLowerCase() === 'function') {
  50. needsSig = true;
  51. break;
  52. }
  53. }
  54. }
  55. // and namespaces that are functions get a signature (but finding them is a
  56. // bit messy)
  57. else if (kind === 'namespace' && meta && meta.code &&
  58. meta.code.type && meta.code.type.match(/[Ff]unction/)) {
  59. needsSig = true;
  60. }
  61. return needsSig;
  62. }
  63. function getSignatureAttributes({optional, nullable}) {
  64. const attributes = [];
  65. if (optional) {
  66. attributes.push('opt');
  67. }
  68. if (nullable === true) {
  69. attributes.push('nullable');
  70. }
  71. else if (nullable === false) {
  72. attributes.push('non-null');
  73. }
  74. return attributes;
  75. }
  76. function updateItemName(item) {
  77. const attributes = getSignatureAttributes(item);
  78. let itemName = item.name || '';
  79. if (item.variable) {
  80. itemName = `&hellip;${itemName}`;
  81. }
  82. if (attributes && attributes.length) {
  83. itemName = util.format( '%s<span class="signature-attributes">%s</span>', itemName,
  84. attributes.join(', ') );
  85. }
  86. return itemName;
  87. }
  88. function addParamAttributes(params) {
  89. return params.filter(({name}) => name && !name.includes('.')).map(updateItemName);
  90. }
  91. function buildItemTypeStrings(item) {
  92. const types = [];
  93. if (item && item.type && item.type.names) {
  94. item.type.names.forEach(name => {
  95. types.push( linkto(name, htmlsafe(name)) );
  96. });
  97. }
  98. return types;
  99. }
  100. function buildAttribsString(attribs) {
  101. let attribsString = '';
  102. if (attribs && attribs.length) {
  103. attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) );
  104. }
  105. return attribsString;
  106. }
  107. function addNonParamAttributes(items) {
  108. let types = [];
  109. items.forEach(item => {
  110. types = types.concat( buildItemTypeStrings(item) );
  111. });
  112. return types;
  113. }
  114. function addSignatureParams(f) {
  115. const params = f.params ? addParamAttributes(f.params) : [];
  116. f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') );
  117. }
  118. function addSignatureReturns(f) {
  119. const attribs = [];
  120. let attribsString = '';
  121. let returnTypes = [];
  122. let returnTypesString = '';
  123. const source = f.yields || f.returns;
  124. // jam all the return-type attributes into an array. this could create odd results (for example,
  125. // if there are both nullable and non-nullable return types), but let's assume that most people
  126. // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa.
  127. if (source) {
  128. source.forEach(item => {
  129. helper.getAttribs(item).forEach(attrib => {
  130. if (!attribs.includes(attrib)) {
  131. attribs.push(attrib);
  132. }
  133. });
  134. });
  135. attribsString = buildAttribsString(attribs);
  136. }
  137. if (source) {
  138. returnTypes = addNonParamAttributes(source);
  139. }
  140. if (returnTypes.length) {
  141. returnTypesString = util.format( ' &rarr; %s{%s}', attribsString, returnTypes.join('|') );
  142. }
  143. f.signature = `<span class="signature">${f.signature || ''}</span><span class="type-signature">${returnTypesString}</span>`;
  144. }
  145. function addSignatureTypes(f) {
  146. const types = f.type ? buildItemTypeStrings(f) : [];
  147. f.signature = `${f.signature || ''}<span class="type-signature">${types.length ? ` :${types.join('|')}` : ''}</span>`;
  148. }
  149. function addAttribs(f) {
  150. const attribs = helper.getAttribs(f);
  151. const attribsString = buildAttribsString(attribs);
  152. f.attribs = util.format('<span class="type-signature">%s</span>', attribsString);
  153. }
  154. function shortenPaths(files, commonPrefix) {
  155. Object.keys(files).forEach(file => {
  156. files[file].shortened = files[file].resolved.replace(commonPrefix, '')
  157. // always use forward slashes
  158. .replace(/\\/g, '/');
  159. });
  160. return files;
  161. }
  162. function getPathFromDoclet({meta}) {
  163. if (!meta) {
  164. return null;
  165. }
  166. return meta.path && meta.path !== 'null' ?
  167. path.join(meta.path, meta.filename) :
  168. meta.filename;
  169. }
  170. function generate(title, docs, filename, resolveLinks) {
  171. let docData;
  172. let html;
  173. let outpath;
  174. resolveLinks = resolveLinks !== false;
  175. docData = {
  176. env: env,
  177. title: title,
  178. docs: docs
  179. };
  180. outpath = path.join(outdir, filename);
  181. html = view.render('container.tmpl', docData);
  182. if (resolveLinks) {
  183. html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
  184. }
  185. fs.writeFileSync(outpath, html, 'utf8');
  186. }
  187. function generateSourceFiles(sourceFiles, encoding = 'utf8') {
  188. Object.keys(sourceFiles).forEach(file => {
  189. let source;
  190. // links are keyed to the shortened path in each doclet's `meta.shortpath` property
  191. const sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
  192. helper.registerLink(sourceFiles[file].shortened, sourceOutfile);
  193. try {
  194. source = {
  195. kind: 'source',
  196. code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
  197. };
  198. }
  199. catch (e) {
  200. logger.error('Error while generating source file %s: %s', file, e.message);
  201. }
  202. generate(`Source: ${sourceFiles[file].shortened}`, [source], sourceOutfile,
  203. false);
  204. });
  205. }
  206. /**
  207. * Look for classes or functions with the same name as modules (which indicates that the module
  208. * exports only that class or function), then attach the classes or functions to the `module`
  209. * property of the appropriate module doclets. The name of each class or function is also updated
  210. * for display purposes. This function mutates the original arrays.
  211. *
  212. * @private
  213. * @param {Array.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to
  214. * check.
  215. * @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search.
  216. */
  217. function attachModuleSymbols(doclets, modules) {
  218. const symbols = {};
  219. // build a lookup table
  220. doclets.forEach(symbol => {
  221. symbols[symbol.longname] = symbols[symbol.longname] || [];
  222. symbols[symbol.longname].push(symbol);
  223. });
  224. modules.forEach(module => {
  225. if (symbols[module.longname]) {
  226. module.modules = symbols[module.longname]
  227. // Only show symbols that have a description. Make an exception for classes, because
  228. // we want to show the constructor-signature heading no matter what.
  229. .filter(({description, kind}) => description || kind === 'class')
  230. .map(symbol => {
  231. symbol = doop(symbol);
  232. if (symbol.kind === 'class' || symbol.kind === 'function') {
  233. symbol.name = `${symbol.name.replace('module:', '(require("')}"))`;
  234. }
  235. return symbol;
  236. });
  237. }
  238. });
  239. }
  240. function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
  241. let nav = '';
  242. if (items.length) {
  243. let itemsNav = '';
  244. items.forEach(item => {
  245. let displayName;
  246. if ( !hasOwnProp.call(item, 'longname') ) {
  247. itemsNav += `<li>${linktoFn('', item.name)}</li>`;
  248. }
  249. else if ( !hasOwnProp.call(itemsSeen, item.longname) ) {
  250. if (env.conf.templates.default.useLongnameInNav) {
  251. displayName = item.longname;
  252. } else {
  253. displayName = item.name;
  254. }
  255. itemsNav += `<li>${linktoFn(item.longname, displayName.replace(/\b(module|event):/g, ''))}</li>`;
  256. itemsSeen[item.longname] = true;
  257. }
  258. });
  259. if (itemsNav !== '') {
  260. nav += `<h3>${itemHeading}</h3><ul>${itemsNav}</ul>`;
  261. }
  262. }
  263. return nav;
  264. }
  265. function linktoTutorial(longName, name) {
  266. return tutoriallink(name);
  267. }
  268. function linktoExternal(longName, name) {
  269. return linkto(longName, name.replace(/(^"|"$)/g, ''));
  270. }
  271. /**
  272. * Create the navigation sidebar.
  273. * @param {object} members The members that will be used to create the sidebar.
  274. * @param {array<object>} members.classes
  275. * @param {array<object>} members.externals
  276. * @param {array<object>} members.globals
  277. * @param {array<object>} members.mixins
  278. * @param {array<object>} members.modules
  279. * @param {array<object>} members.namespaces
  280. * @param {array<object>} members.tutorials
  281. * @param {array<object>} members.events
  282. * @param {array<object>} members.interfaces
  283. * @return {string} The HTML for the navigation sidebar.
  284. */
  285. function buildNav(members) {
  286. let globalNav;
  287. let nav = '<h2><a href="index.html">Home</a></h2>';
  288. const seen = {};
  289. const seenTutorials = {};
  290. nav += buildMemberNav(members.modules, 'Modules', {}, linkto);
  291. nav += buildMemberNav(members.externals, 'Externals', seen, linktoExternal);
  292. nav += buildMemberNav(members.namespaces, 'Namespaces', seen, linkto);
  293. nav += buildMemberNav(members.classes, 'Classes', seen, linkto);
  294. nav += buildMemberNav(members.interfaces, 'Interfaces', seen, linkto);
  295. nav += buildMemberNav(members.events, 'Events', seen, linkto);
  296. nav += buildMemberNav(members.mixins, 'Mixins', seen, linkto);
  297. nav += buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial);
  298. if (members.globals.length) {
  299. globalNav = '';
  300. members.globals.forEach(({kind, longname, name}) => {
  301. if ( kind !== 'typedef' && !hasOwnProp.call(seen, longname) ) {
  302. globalNav += `<li>${linkto(longname, name)}</li>`;
  303. }
  304. seen[longname] = true;
  305. });
  306. if (!globalNav) {
  307. // turn the heading into a link so you can actually get to the global page
  308. nav += `<h3>${linkto('global', 'Global')}</h3>`;
  309. }
  310. else {
  311. nav += `<h3>Global</h3><ul>${globalNav}</ul>`;
  312. }
  313. }
  314. return nav;
  315. }
  316. /**
  317. @param {TAFFY} taffyData See <http://taffydb.com/>.
  318. @param {object} opts
  319. @param {Tutorial} tutorials
  320. */
  321. exports.publish = (taffyData, opts, tutorials) => {
  322. let classes;
  323. let conf;
  324. let externals;
  325. let files;
  326. let fromDir;
  327. let globalUrl;
  328. let indexUrl;
  329. let interfaces;
  330. let members;
  331. let mixins;
  332. let modules;
  333. let namespaces;
  334. let outputSourceFiles;
  335. let packageInfo;
  336. let packages;
  337. const sourceFilePaths = [];
  338. let sourceFiles = {};
  339. let staticFileFilter;
  340. let staticFilePaths;
  341. let staticFiles;
  342. let staticFileScanner;
  343. let templatePath;
  344. data = taffyData;
  345. conf = env.conf.templates || {};
  346. conf.default = conf.default || {};
  347. templatePath = path.normalize(opts.template);
  348. view = new template.Template( path.join(templatePath, 'tmpl') );
  349. // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness
  350. // doesn't try to hand them out later
  351. indexUrl = helper.getUniqueFilename('index');
  352. // don't call registerLink() on this one! 'index' is also a valid longname
  353. globalUrl = helper.getUniqueFilename('global');
  354. helper.registerLink('global', globalUrl);
  355. // set up templating
  356. view.layout = conf.default.layoutFile ?
  357. path.getResourcePath(path.dirname(conf.default.layoutFile),
  358. path.basename(conf.default.layoutFile) ) :
  359. 'layout.tmpl';
  360. // set up tutorials for helper
  361. helper.setTutorials(tutorials);
  362. data = helper.prune(data);
  363. data.sort('longname, version, since');
  364. helper.addEventListeners(data);
  365. data().each(doclet => {
  366. let sourcePath;
  367. doclet.attribs = '';
  368. if (doclet.examples) {
  369. doclet.examples = doclet.examples.map(example => {
  370. let caption;
  371. let code;
  372. if (example.match(/^\s*<caption>([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) {
  373. caption = RegExp.$1;
  374. code = RegExp.$3;
  375. }
  376. return {
  377. caption: caption || '',
  378. code: code || example
  379. };
  380. });
  381. }
  382. if (doclet.see) {
  383. doclet.see.forEach((seeItem, i) => {
  384. doclet.see[i] = hashToLink(doclet, seeItem);
  385. });
  386. }
  387. // build a list of source files
  388. if (doclet.meta) {
  389. sourcePath = getPathFromDoclet(doclet);
  390. sourceFiles[sourcePath] = {
  391. resolved: sourcePath,
  392. shortened: null
  393. };
  394. if (!sourceFilePaths.includes(sourcePath)) {
  395. sourceFilePaths.push(sourcePath);
  396. }
  397. }
  398. });
  399. // update outdir if necessary, then create outdir
  400. packageInfo = ( find({kind: 'package'}) || [] )[0];
  401. if (packageInfo && packageInfo.name) {
  402. outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') );
  403. }
  404. fs.mkPath(outdir);
  405. // copy the template's static files to outdir
  406. fromDir = path.join(templatePath, 'static');
  407. staticFiles = fs.ls(fromDir, 3);
  408. staticFiles.forEach(fileName => {
  409. const toDir = fs.toDir( fileName.replace(fromDir, outdir) );
  410. fs.mkPath(toDir);
  411. fs.copyFileSync(fileName, toDir);
  412. });
  413. // copy user-specified static files to outdir
  414. if (conf.default.staticFiles) {
  415. // The canonical property name is `include`. We accept `paths` for backwards compatibility
  416. // with a bug in JSDoc 3.2.x.
  417. staticFilePaths = conf.default.staticFiles.include ||
  418. conf.default.staticFiles.paths ||
  419. [];
  420. staticFileFilter = new (require('jsdoc/src/filter').Filter)(conf.default.staticFiles);
  421. staticFileScanner = new (require('jsdoc/src/scanner').Scanner)();
  422. staticFilePaths.forEach(filePath => {
  423. let extraStaticFiles;
  424. filePath = path.resolve(env.pwd, filePath);
  425. extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter);
  426. extraStaticFiles.forEach(fileName => {
  427. const sourcePath = fs.toDir(filePath);
  428. const toDir = fs.toDir( fileName.replace(sourcePath, outdir) );
  429. fs.mkPath(toDir);
  430. fs.copyFileSync(fileName, toDir);
  431. });
  432. });
  433. }
  434. if (sourceFilePaths.length) {
  435. sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) );
  436. }
  437. data().each(doclet => {
  438. let docletPath;
  439. const url = helper.createLink(doclet);
  440. helper.registerLink(doclet.longname, url);
  441. // add a shortened version of the full path
  442. if (doclet.meta) {
  443. docletPath = getPathFromDoclet(doclet);
  444. docletPath = sourceFiles[docletPath].shortened;
  445. if (docletPath) {
  446. doclet.meta.shortpath = docletPath;
  447. }
  448. }
  449. });
  450. data().each(doclet => {
  451. const url = helper.longnameToUrl[doclet.longname];
  452. if (url.includes('#')) {
  453. doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop();
  454. }
  455. else {
  456. doclet.id = doclet.name;
  457. }
  458. if ( needsSignature(doclet) ) {
  459. addSignatureParams(doclet);
  460. addSignatureReturns(doclet);
  461. addAttribs(doclet);
  462. }
  463. });
  464. // do this after the urls have all been generated
  465. data().each(doclet => {
  466. doclet.ancestors = getAncestorLinks(doclet);
  467. if (doclet.kind === 'member') {
  468. addSignatureTypes(doclet);
  469. addAttribs(doclet);
  470. }
  471. if (doclet.kind === 'constant') {
  472. addSignatureTypes(doclet);
  473. addAttribs(doclet);
  474. doclet.kind = 'member';
  475. }
  476. });
  477. members = helper.getMembers(data);
  478. members.tutorials = tutorials.children;
  479. // output pretty-printed source files by default
  480. outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false;
  481. // add template helpers
  482. view.find = find;
  483. view.linkto = linkto;
  484. view.resolveAuthorLinks = resolveAuthorLinks;
  485. view.tutoriallink = tutoriallink;
  486. view.htmlsafe = htmlsafe;
  487. view.outputSourceFiles = outputSourceFiles;
  488. // once for all
  489. view.nav = buildNav(members);
  490. attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules );
  491. // generate the pretty-printed source files first so other pages can link to them
  492. if (outputSourceFiles) {
  493. generateSourceFiles(sourceFiles, opts.encoding);
  494. }
  495. if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
  496. // index page displays information from package.json and lists files
  497. files = find({kind: 'file'});
  498. packages = find({kind: 'package'});
  499. generate('Home',
  500. packages.concat(
  501. [{
  502. kind: 'mainpage',
  503. readme: opts.readme,
  504. longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'
  505. }]
  506. ).concat(files), indexUrl);
  507. // set up the lists that we'll use to generate pages
  508. classes = taffy(members.classes);
  509. modules = taffy(members.modules);
  510. namespaces = taffy(members.namespaces);
  511. mixins = taffy(members.mixins);
  512. externals = taffy(members.externals);
  513. interfaces = taffy(members.interfaces);
  514. Object.keys(helper.longnameToUrl).forEach(longname => {
  515. const myClasses = helper.find(classes, {longname: longname});
  516. const myExternals = helper.find(externals, {longname: longname});
  517. const myInterfaces = helper.find(interfaces, {longname: longname});
  518. const myMixins = helper.find(mixins, {longname: longname});
  519. const myModules = helper.find(modules, {longname: longname});
  520. const myNamespaces = helper.find(namespaces, {longname: longname});
  521. if (myModules.length) {
  522. generate(`Module: ${myModules[0].name}`, myModules, helper.longnameToUrl[longname]);
  523. }
  524. if (myClasses.length) {
  525. generate(`Class: ${myClasses[0].name}`, myClasses, helper.longnameToUrl[longname]);
  526. }
  527. if (myNamespaces.length) {
  528. generate(`Namespace: ${myNamespaces[0].name}`, myNamespaces, helper.longnameToUrl[longname]);
  529. }
  530. if (myMixins.length) {
  531. generate(`Mixin: ${myMixins[0].name}`, myMixins, helper.longnameToUrl[longname]);
  532. }
  533. if (myExternals.length) {
  534. generate(`External: ${myExternals[0].name}`, myExternals, helper.longnameToUrl[longname]);
  535. }
  536. if (myInterfaces.length) {
  537. generate(`Interface: ${myInterfaces[0].name}`, myInterfaces, helper.longnameToUrl[longname]);
  538. }
  539. });
  540. // TODO: move the tutorial functions to templateHelper.js
  541. function generateTutorial(title, tutorial, filename) {
  542. const tutorialData = {
  543. title: title,
  544. header: tutorial.title,
  545. content: tutorial.parse(),
  546. children: tutorial.children
  547. };
  548. const tutorialPath = path.join(outdir, filename);
  549. let html = view.render('tutorial.tmpl', tutorialData);
  550. // yes, you can use {@link} in tutorials too!
  551. html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
  552. fs.writeFileSync(tutorialPath, html, 'utf8');
  553. }
  554. // tutorials can have only one parent so there is no risk for loops
  555. function saveChildren({children}) {
  556. children.forEach(child => {
  557. generateTutorial(`Tutorial: ${child.title}`, child, helper.tutorialToUrl(child.name));
  558. saveChildren(child);
  559. });
  560. }
  561. saveChildren(tutorials);
  562. };