UNPKG

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