UNPKG

c2cs-jsdoc

Version:

A JSDoc theme and plugin collection based on Minami and Loke.

826 lines (686 loc) 22.6 kB
/*global env: true */ 'use strict'; var doop = require( 'jsdoc/util/doop' ); var fs = require( 'jsdoc/fs' ); var helper = require( 'jsdoc/util/templateHelper' ); var logger = require( 'jsdoc/util/logger' ); var path = require( 'jsdoc/path' ); var taffy = require( 'taffydb' ).taffy; var template = require( 'jsdoc/template' ); var util = require( 'util' ); var htmlsafe = helper.htmlsafe; var linkto = helper.linkto; var resolveAuthorLinks = helper.resolveAuthorLinks; var scopeToPunc = helper.scopeToPunc; var hasOwnProp = Object.prototype.hasOwnProperty; var data; var view; var outdir = path.normalize( env.opts.destination ); function find ( spec ) { return helper.find( data, spec ); } function tutoriallink ( tutorial ) { return helper.toTutorial( tutorial, null, { tag : 'em', classname : 'disabled', prefix : 'Tutorial: ' } ); } function getAncestorLinks ( doclet ) { return helper.getAncestorLinks( data, doclet ); } function hashToLink ( doclet, hash ) { if( !/^(#.+)/.test( hash ) ) { return hash; } var url = helper.createLink( doclet ); url = url.replace( /(#.+|$)/, hash ); return '<a href="' + url + '">' + hash + '</a>'; } function needsSignature ( doclet ) { var needsSig = false; // function and class definitions always get a signature if( doclet.kind === 'function' || doclet.kind === 'class' ) { needsSig = true; } // typedefs that contain functions get a signature, too else if( doclet.kind === 'typedef' && doclet.type && doclet.type.names && doclet.type.names.length ) { for( var i = 0, l = doclet.type.names.length; i < l; i++ ) { if( doclet.type.names[i].toLowerCase() === 'function' ) { needsSig = true; break; } } } return needsSig; } function getSignatureAttributes ( item ) { var attributes = []; if( item.optional ) { attributes.push( 'opt' ); } if( item.nullable === true ) { attributes.push( 'nullable' ); } else if( item.nullable === false ) { attributes.push( 'non-null' ); } return attributes; } function updateItemName ( item ) { var attributes = getSignatureAttributes( item ); var itemName = item.name || ''; if( item.variable ) { itemName = '&hellip;' + itemName; } if( attributes && attributes.length ) { itemName = util.format( '%s<span class="signature-attributes">%s</span>', itemName, attributes.join( ', ' ) ); } return itemName; } function addParamAttributes ( params ) { return params.filter( function( param ) { return param.name && param.name.indexOf( '.' ) === -1; } ).map( updateItemName ); } function buildItemTypeStrings ( item ) { var types = []; if( item && item.type && item.type.names ) { item.type.names.forEach( function( name ) { types.push( linkto( name, htmlsafe( name ) ) ); } ); } return types; } function buildAttribsString ( attribs ) { var attribsString = ''; if( attribs && attribs.length ) { attribsString = htmlsafe( util.format( '(%s) ', attribs.join( ', ' ) ) ); } return attribsString; } function addNonParamAttributes ( items ) { var types = []; items.forEach( function( item ) { types = types.concat( buildItemTypeStrings( item ) ); } ); return types; } function addSignatureParams ( f ) { var params = f.params ? addParamAttributes( f.params ) : []; f.signature = util.format( '%s(%s)', (f.signature || ''), params.join( ', ' ) ); } function addSignatureReturns ( f ) { var attribs = []; var attribsString = ''; var returnTypes = []; var returnTypesString = ''; // jam all the return-type attributes into an array. this could create odd results (for example, // if there are both nullable and non-nullable return types), but let's assume that most people // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa. if( f.returns ) { f.returns.forEach( function( item ) { helper.getAttribs( item ).forEach( function( attrib ) { if( attribs.indexOf( attrib ) === -1 ) { attribs.push( attrib ); } } ); } ); attribsString = buildAttribsString( attribs ); } if( f.returns ) { returnTypes = addNonParamAttributes( f.returns ); } if( returnTypes.length ) { returnTypesString = util.format( ' &rarr; %s{%s}', attribsString, returnTypes.join( '|' ) ); } f.signature = '<span class="signature">' + (f.signature || '') + '</span>' + '<span class="type-signature">' + returnTypesString + '</span>'; } function addSignatureTypes ( f ) { var types = f.type ? buildItemTypeStrings( f ) : []; f.signature = (f.signature || '') + '<span class="type-signature">' + (types.length ? ' :' + types.join( '|' ) : '') + '</span>'; } function addAttribs ( f ) { var attribs = helper.getAttribs( f ); var attribsString = buildAttribsString( attribs ); f.attribs = util.format( '<span class="type-signature">%s</span>', attribsString ); } function shortenPaths ( files, commonPrefix ) { Object.keys( files ).forEach( function( file ) { files[file].shortened = files[file].resolved.replace( commonPrefix, '' )// always use forward slashes .replace( /\\/g, '/' ); } ); return files; } function getPathFromDoclet ( doclet ) { if( !doclet.meta ) { return null; } return doclet.meta.path && doclet.meta.path !== 'null' ? path.join( doclet.meta.path, doclet.meta.filename ) : doclet.meta.filename; } function generate ( type, title, docs, filename, resolveLinks ) { resolveLinks = resolveLinks === false ? false : true; var docData = { type : type, title : title, docs : docs }; var outpath = path.join( outdir, filename ), html = view.render( 'container.tmpl', docData ); if( resolveLinks ) { html = helper.resolveLinks( html ); // turn {@link foo} into <a href="foodoc.html">foo</a> } fs.writeFileSync( outpath, html, 'utf8' ); } function generateSourceFiles ( sourceFiles, encoding ) { encoding = encoding || 'utf8'; Object.keys( sourceFiles ).forEach( function( file ) { var source; // links are keyed to the shortened path in each doclet's `meta.shortpath` property var sourceOutfile = helper.getUniqueFilename( sourceFiles[file].shortened ); helper.registerLink( sourceFiles[file].shortened, sourceOutfile ); try { source = { kind : 'source', code : helper.htmlsafe( fs.readFileSync( sourceFiles[file].resolved, encoding ) ) }; } catch( e ) { logger.error( 'Error while generating source file %s: %s', file, e.message ); } generate( 'Source', sourceFiles[file].shortened, [source], sourceOutfile, false ); } ); } /** * Look for classes or functions with the same name as modules (which indicates that the module * exports only that class or function), then attach the classes or functions to the `module` * property of the appropriate module doclets. The name of each class or function is also updated * for display purposes. This function mutates the original arrays. * * @private * @param {Array.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to * check. * @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search. */ function attachModuleSymbols ( doclets, modules ) { var symbols = {}; // build a lookup table doclets.forEach( function( symbol ) { symbols[symbol.longname] = symbols[symbol.longname] || []; symbols[symbol.longname].push( symbol ); } ); return modules.map( function( module ) { if( symbols[module.longname] ) { module.modules = symbols[module.longname]// Only show symbols that have a description. Make an exception for classes, because // we want to show the constructor-signature heading no matter what. .filter( function( symbol ) { return symbol.description || symbol.kind === 'class'; } ).map( function( symbol ) { symbol = doop( symbol ); if( symbol.kind === 'class' || symbol.kind === 'function' ) { symbol.name = symbol.name.replace( 'module:', '(require("' ) + '"))'; } return symbol; } ); } } ); } function buildApiMemberNav ( items, itemHeading, itemsSeen, linktoFn ) { var nav = ''; if( items.length ) { var routes = {}; items.forEach( function( item ) { var parts = item.route.split( ' ' ); var method = parts.length === 1 ? 'GET' : parts[0]; var route = parts.length === 1 ? parts[0] : parts[1]; if( !routes[route] ) { routes[route] = {}; } routes[route][method] = item; } ); var itemsNav = ''; var routeArr = []; for( var route in routes ) { routeArr.push( route ); } routeArr.sort(); routeArr.forEach( function( route ) { itemsNav += '<li><a href="#">' + route + '</a>'; itemsNav += "<ul class='methods'>"; for( var method in routes[route] ) { itemsNav += "<li data-type='method'>"; itemsNav += linkto( routes[route][method].longname, method ); itemsNav += "</li>"; } itemsNav += "</ul>"; itemsNav += '</li>'; } ); if( itemsNav !== '' ) { nav += '<h3>' + itemHeading + '</h3><ul>' + itemsNav + '</ul>'; } } return nav; } function buildMemberNav ( items, itemHeading, itemsSeen, linktoFn ) { var nav = ''; if( items.length ) { var itemsNav = ''; items.forEach( function( item ) { var isRpc = item.kind === 'rpc'; var longname = isRpc ? item.base : item.longname; var methodquery = { kind : 'function', memberof : item.longname }; var memberquery = { kind : 'member', memberof : item.longname }; if( isRpc ) { methodquery.rpc = {isUndefined : false}; memberquery.rpc = {isUndefined : false}; } var methods = find( methodquery ); var members = find( memberquery ); if( !hasOwnProp.call( item, 'longname' ) ) { itemsNav += '<li>' + linktoFn( '', item.name ); itemsNav += '</li>'; } else if( !hasOwnProp.call( itemsSeen, item.longname ) ) { itemsNav += '<li>' + linktoFn( item.longname, item.name.replace( /^module:/, '' ) ); if( methods.length ) { itemsNav += "<ul class='methods'>"; methods.forEach( function( method ) { var name = isRpc ? (typeof method.rpc === 'string') ? method.rpc : method.name : method.name; itemsNav += "<li data-type='method'>"; itemsNav += linkto( method.longname, name ); itemsNav += "</li>"; } ); itemsNav += "</ul>"; } itemsNav += '</li>'; itemsSeen[item.longname] = true; } } ); if( itemsNav !== '' ) { nav += '<h3>' + itemHeading + '</h3><ul>' + itemsNav + '</ul>'; } } return nav; } function linktoTutorial ( longName, name ) { return tutoriallink( name ); } function linktoExternal ( longName, name ) { return linkto( longName, name.replace( /(^"|"$)/g, '' ) ); } /** * Create the navigation sidebar. * @param {object} members The members that will be used to create the sidebar. * @param {array<object>} members.classes * @param {array<object>} members.externals * @param {array<object>} members.globals * @param {array<object>} members.mixins * @param {array<object>} members.modules * @param {array<object>} members.namespaces * @param {array<object>} members.tutorials * @param {array<object>} members.events * @param {array<object>} members.interfaces * @return {string} The HTML for the navigation sidebar. */ function buildNav ( members ) { var nav = '<h2><a href="index.html">Home</a></h2>'; var seen = {}; // we create different hashmaps for these as duplicates are allowed across them var seenTutorials = {}; var seenRpc = {}; var seenApi = {}; // get all functions with a route defined members.api = find( {kind : 'function'} ).filter( function( fnDoclet ) { //console.log( fnDoclet.route ); return fnDoclet.route; } ); // get all classes that are RPC services members.rpc = members.classes.filter( function( doclet ) { return doclet.rpc; } ).map( function( doclet ) { // create a new doclet... map some attributes return { kind : 'rpc', comment : doclet.comment, meta : doclet.meta, description : doclet.description, name : (typeof doclet.rpc === 'string') ? doclet.rpc : doclet.name, longname : doclet.longname, scope : doclet.scope }; } ); nav += buildMemberNav( members.tutorials, 'Tutorials', seenTutorials, linktoTutorial ); nav += buildApiMemberNav( members.api, 'API', seenApi, linkto ); nav += buildMemberNav( members.rpc, 'RPC', seenRpc, linkto ); nav += buildMemberNav( members.namespaces, 'Namespaces', seen, linkto ); nav += buildMemberNav( members.interfaces, 'Interfaces', seen, linkto ); nav += buildMemberNav( members.events, 'Events', seen, linkto ); nav += buildMemberNav( members.modules, 'Modules', {}, linkto ); nav += buildMemberNav( members.classes, 'Classes', seen, linkto ); nav += buildMemberNav( members.externals, 'Externals', seen, linktoExternal ); nav += buildMemberNav( members.mixins, 'Mixins', seen, linkto ); // ignore globals for now... they make a big mess return nav; if( members.globals.length ) { var globalNav = ''; members.globals.forEach( function( g ) { if( g.kind !== 'typedef' && !hasOwnProp.call( seen, g.longname ) ) { globalNav += '<li>' + linkto( g.longname, g.name ) + '</li>'; } seen[g.longname] = true; } ); if( !globalNav ) { // turn the heading into a link so you can actually get to the global page nav += '<h3>' + linkto( 'global', 'Global' ) + '</h3>'; } else { nav += '<h3>Global</h3><ul>' + globalNav + '</ul>'; } } return nav; } /** @param {TAFFY} taffyData See <http://taffydb.com/>. @param {object} opts @param {Tutorial} tutorials */ exports.publish = function( taffyData, opts, tutorials ) { data = taffyData; var conf = env.conf.templates || {}; conf.default = conf.default || {}; // Default a few config settings if( conf.addProjectSubdir === undefined || conf.addProjectSubdir !== true ) { conf.addProjectSubdir = false; } if( conf.addVersionSubdir === undefined || conf.addVersionSubdir !== true ) { conf.addVersionSubdir = false; } var templatePath = path.normalize( opts.template ); view = new template.Template( path.join( templatePath, 'tmpl' ) ); // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness // doesn't try to hand them out later var indexUrl = helper.getUniqueFilename( 'index' ); // don't call registerLink() on this one! 'index' is also a valid longname var globalUrl = helper.getUniqueFilename( 'global' ); helper.registerLink( 'global', globalUrl ); // set up templating view.layout = conf.default.layoutFile ? path.getResourcePath( path.dirname( conf.default.layoutFile ), path.basename( conf.default.layoutFile ) ) : 'layout.tmpl'; // set up tutorials for helper helper.setTutorials( tutorials ); data = helper.prune( data ); data.sort( 'longname, version, since' ); helper.addEventListeners( data ); var sourceFiles = {}; var sourceFilePaths = []; data().each( function( doclet ) { doclet.attribs = ''; if( doclet.examples ) { doclet.examples = doclet.examples.map( function( example ) { var caption, code; if( example.match( /^\s*<caption>([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i ) ) { caption = RegExp.$1; code = RegExp.$3; } return { caption : caption || '', code : code || example }; } ); } if( doclet.see ) { doclet.see.forEach( function( seeItem, i ) { doclet.see[i] = hashToLink( doclet, seeItem ); } ); } // build a list of source files var sourcePath; if( doclet.meta ) { sourcePath = getPathFromDoclet( doclet ); sourceFiles[sourcePath] = { resolved : sourcePath, shortened : null }; if( sourceFilePaths.indexOf( sourcePath ) === -1 ) { sourceFilePaths.push( sourcePath ); } } } ); // update outdir if necessary, then create outdir var packageInfo = ( find( {kind : 'package'} ) || [] ) [0]; if( packageInfo && packageInfo.name ) { if( conf.addProjectSubdir ) { outdir = path.join( outdir, packageInfo.name ); } if( conf.addVersionSubdir ) { outdir = path.join( outdir, (packageInfo.version || '') ); } } fs.mkPath( outdir ); // copy the template's static files to outdir var fromDir = path.join( templatePath, 'static' ); var staticFiles = fs.ls( fromDir, 3 ); staticFiles.forEach( function( fileName ) { var toDir = fs.toDir( fileName.replace( fromDir, outdir ) ); fs.mkPath( toDir ); fs.copyFileSync( fileName, toDir ); } ); // copy user-specified static files to outdir var staticFilePaths; var staticFileFilter; var staticFileScanner; if( conf.default.staticFiles ) { // The canonical property name is `include`. We accept `paths` for backwards compatibility // with a bug in JSDoc 3.2.x. staticFilePaths = conf.default.staticFiles.include || conf.default.staticFiles.paths || []; staticFileFilter = new (require( 'jsdoc/src/filter' )).Filter( conf.default.staticFiles ); staticFileScanner = new (require( 'jsdoc/src/scanner' )).Scanner(); staticFilePaths.forEach( function( filePath ) { var extraStaticFiles = staticFileScanner.scan( [filePath], 10, staticFileFilter ); extraStaticFiles.forEach( function( fileName ) { var sourcePath = fs.toDir( filePath ); var toDir = fs.toDir( fileName.replace( sourcePath, outdir ) ); fs.mkPath( toDir ); fs.copyFileSync( fileName, toDir ); } ); } ); } if( sourceFilePaths.length ) { sourceFiles = shortenPaths( sourceFiles, path.commonPrefix( sourceFilePaths ) ); } data().each( function( doclet ) { var url = helper.createLink( doclet ); helper.registerLink( doclet.longname, url ); // add a shortened version of the full path var docletPath; if( doclet.meta ) { docletPath = getPathFromDoclet( doclet ); docletPath = sourceFiles[docletPath].shortened; if( docletPath ) { doclet.meta.shortpath = docletPath; } } } ); data().each( function( doclet ) { var url = helper.longnameToUrl[doclet.longname]; if( url.indexOf( '#' ) > -1 ) { doclet.id = helper.longnameToUrl[doclet.longname].split( /#/ ).pop(); } else { doclet.id = doclet.name; } if( needsSignature( doclet ) ) { addSignatureParams( doclet ); addSignatureReturns( doclet ); addAttribs( doclet ); } } ); // do this after the urls have all been generated data().each( function( doclet ) { doclet.ancestors = getAncestorLinks( doclet ); if( doclet.kind === 'member' ) { addSignatureTypes( doclet ); addAttribs( doclet ); } if( doclet.kind === 'constant' ) { addSignatureTypes( doclet ); addAttribs( doclet ); doclet.kind = 'member'; } } ); var members = helper.getMembers( data ); members.tutorials = tutorials.children; // output pretty-printed source files by default var outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false ? true : false; // add template helpers view.find = find; view.linkto = linkto; view.resolveAuthorLinks = resolveAuthorLinks; view.tutoriallink = tutoriallink; view.htmlsafe = htmlsafe; view.outputSourceFiles = outputSourceFiles; // once for all view.nav = buildNav( members ); attachModuleSymbols( find( {longname : {left : 'module:'}} ), members.modules ); // generate the pretty-printed source files first so other pages can link to them if( outputSourceFiles ) { generateSourceFiles( sourceFiles, opts.encoding ); } if( members.globals.length ) { generate( '', 'Global', [{kind : 'globalobj'}], globalUrl ); } // index page displays information from package.json and lists files var files = find( {kind : 'file'} ); var packages = find( {kind : 'package'} ); generate( '', 'Home', packages.concat( [ { kind : 'mainpage', readme : opts.readme, longname : (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page' } ] ).concat( files ), indexUrl ); // set up the lists that we'll use to generate pages var classes = taffy( members.classes ); var modules = taffy( members.modules ); var namespaces = taffy( members.namespaces ); var mixins = taffy( members.mixins ); var externals = taffy( members.externals ); var interfaces = taffy( members.interfaces ); Object.keys( helper.longnameToUrl ).forEach( function( longname ) { var myModules = helper.find( modules, {longname : longname} ); if( myModules.length ) { generate( 'Module', myModules[0].name, myModules, helper.longnameToUrl[longname] ); } var myClasses = helper.find( classes, {longname : longname} ); if( myClasses.length ) { generate( 'Class', myClasses[0].name, myClasses, helper.longnameToUrl[longname] ); } var myNamespaces = helper.find( namespaces, {longname : longname} ); if( myNamespaces.length ) { generate( 'Namespace', myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname] ); } var myMixins = helper.find( mixins, {longname : longname} ); if( myMixins.length ) { generate( 'Mixin', myMixins[0].name, myMixins, helper.longnameToUrl[longname] ); } var myExternals = helper.find( externals, {longname : longname} ); if( myExternals.length ) { generate( 'External', myExternals[0].name, myExternals, helper.longnameToUrl[longname] ); } var myInterfaces = helper.find( interfaces, {longname : longname} ); if( myInterfaces.length ) { generate( 'Interface', myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname] ); } } ); // TODO: move the tutorial functions to templateHelper.js function generateTutorial ( title, tutorial, filename ) { var tutorialData = { title : title, header : tutorial.title, content : tutorial.parse(), children : tutorial.children }; var tutorialPath = path.join( outdir, filename ); var html = view.render( 'tutorial.tmpl', tutorialData ); // yes, you can use {@link} in tutorials too! html = helper.resolveLinks( html ); // turn {@link foo} into <a href="foodoc.html">foo</a> fs.writeFileSync( tutorialPath, html, 'utf8' ); } // tutorials can have only one parent so there is no risk for loops function saveChildren ( node ) { node.children.forEach( function( child ) { generateTutorial( 'Tutorial: ' + child.title, child, helper.tutorialToUrl( child.name ) ); saveChildren( child ); } ); } saveChildren( tutorials ); };