publish.js 20 KB

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