UNPKG

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