publish.js 21 KB

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