jsdoc-baseline
Version:
A basic template for JSDoc.
434 lines (345 loc) • 13.5 kB
JavaScript
/*
Copyright 2014-2019 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** @module lib/doclethelper */
const _ = require('lodash');
const helper = require('jsdoc/util/templateHelper');
const logger = require('jsdoc/util/logger');
const name = require('jsdoc/name');
const path = require('jsdoc/path');
// loaded by the file finder
let SymbolTracker;
let finders;
// loaded by the file finder
let ENUMS;
let CATEGORIES;
let KIND_TO_CATEGORY;
let OUTPUT_FILE_CATEGORIES;
const hasOwnProp = Object.prototype.hasOwnProperty;
// set up modules that cannot be preloaded
function init() {
if (!ENUMS) {
finders = {
// this finder should exist by the time we get here
modules: require('./filefinder').get('modules')
};
SymbolTracker = finders.modules.require('./symboltracker');
ENUMS = finders.modules.require('./enums');
CATEGORIES = ENUMS.CATEGORIES;
KIND_TO_CATEGORY = ENUMS.KIND_TO_CATEGORY;
OUTPUT_FILE_CATEGORIES = ENUMS.OUTPUT_FILE_CATEGORIES;
}
}
// TODO: think carefully about whether these are the only symbols that should appear as global. For
// example, why not show a global class as such?
function isGlobal({scope, kind}) {
const globalKinds = ['member', 'function', 'constant', 'typedef'];
if (scope === 'global' && globalKinds.includes(kind)) {
return true;
}
return false;
}
// Check whether doclet has a category and is not a dummy package object.
function shouldTrack({kind, longname}) {
return Boolean(KIND_TO_CATEGORY[kind]) && longname !== 'package:undefined';
}
function getPathFromMeta(meta) {
// TODO: why 'null' as a string?
return meta.path && meta.path !== 'null' ?
path.join(meta.path, meta.filename) :
meta.filename;
}
module.exports = class DocletHelper {
constructor() {
init();
// Doclets tracked by longname, not categorized
this.all = {};
// Global doclets
this.globals = new SymbolTracker();
// Listeners tracked by event longname
this.listeners = {};
// Doclets tracked by longname
this.longname = {};
// Doclets tracked by memberof
this.memberof = {};
// Longnames of doclets that need their own output file
this.needsFile = {};
this.navTree = {};
this.shortPaths = {};
this.symbols = new SymbolTracker();
this._sourcePaths = [];
}
_trackByCategory(doclet, category) {
const longname = doclet.longname;
if (isGlobal(doclet)) {
// Only track the doclet as a global; we don't want it to appear elsewhere
this.globals.add(doclet, category);
}
else if (longname) {
// Track the doclet by its longname, unless it's a package.
if (!hasOwnProp.call(this.all, longname) && doclet.kind !== 'package') {
this.all[longname] = doclet;
}
// Using a categorized tracker, track the doclet by its longname. Also, if the doclet is a
// member of something else, track it by its memberof value, so we can easily retrieve all
// of the members later.
['longname', 'memberof'].forEach(prop => {
const docletValue = doclet[prop];
if (docletValue) {
this[prop][docletValue] = hasOwnProp.call(this[prop], docletValue) ?
this[prop][docletValue] :
new SymbolTracker();
this[prop][docletValue].add(doclet, category);
}
});
}
return this;
}
_trackListeners(doclet) {
const listens = doclet.listens || [];
listens.forEach(longname => {
this.listeners[longname] = this.listeners[longname] || new SymbolTracker();
this.listeners[longname].add(doclet, CATEGORIES.LISTENERS);
});
return this;
}
_trackMemberof(doclet, category) {
const longname = doclet.longname;
if (isGlobal(doclet)) {
// Only track the doclet as a global; we don't want it to appear elsewhere
this.globals.add(doclet, category);
}
else if (longname && hasOwnProp.call(doclet, 'memberof')) {
this.memberof[doclet.memberof] = this.memberof[doclet.memberof] || new SymbolTracker();
this.memberof[doclet.memberof].add(doclet, category);
}
return this;
}
_trackNeedsFile({longname}, category) {
if (longname && OUTPUT_FILE_CATEGORIES.includes(category)) {
this.needsFile[longname] = true;
}
return this;
}
// TODO: rename
// TODO: can we move the doclet-munging elsewhere?
_categorize(doclet) {
let category;
// Remove the variation (if present) from the doclet's longname, so we can group variations
// under the same longname
// TODO: store these in a lookup table rather than mangling the doclet
doclet.longname = name.stripVariation(doclet.longname);
// Preprocessing based on the doclet's kind
switch (doclet.kind) {
case 'external':
// strip quotes from externals, since we allow quoted names that would normally indicate
// a namespace hierarchy (as in `@external "jquery.fn"`)
// TODO: we should probably be doing this for other types of symbols, here or elsewhere;
// see jsdoc3/jsdoc#396
// TODO: can we make this a filter?
doclet.name = doclet.name.replace(/^"([\s\S]+)"$/g, '$1');
break;
default:
// ignore
break;
}
category = KIND_TO_CATEGORY[doclet.kind];
if (!shouldTrack(doclet)) {
logger.debug('Not tracking doclet with kind: %s, name: %s, longname: %s',
doclet.kind, doclet.name, doclet.longname);
} else {
this.symbols.add(doclet, category);
this._trackByCategory(doclet, category)
._trackNeedsFile(doclet, category)
._trackListeners(doclet);
}
return this;
}
addDoclets(taffyData) {
let doclet;
let doclets;
let exported;
let i;
let ii;
let j;
let jj;
// TODO: make these steps configurable (especially sorting!)
helper.prune(taffyData);
taffyData.sort('longname, version, since');
doclets = taffyData().get();
for (i = 0, ii = doclets.length; i < ii; i++) {
this.addDoclet(doclets[i]);
}
this.findShortPaths()
.resolveModuleExports()
.addListeners();
this.navTree = helper.longnamesToTree(this.getOutputLongnames());
this.allLongnamesTree = helper.longnamesToTree(this.getAllLongnames(), this.all);
for (i = 0, ii = doclets.length; i < ii; i++) {
doclet = doclets[i];
this.registerLink(doclet)
.addShortPath(doclet);
// repeat the per-doclet tasks for any exported doclets that are attached to this one
// TODO: can we avoid the need to do this?
if (doclet.exports) {
for (j = 0, jj = doclet.exports.length; j < jj; j++) {
exported = doclet.exports[j];
this.addShortPath(exported);
}
}
}
return this;
}
addDoclet(doclet) {
this._categorize(doclet)
.addSourcePath(doclet);
return this;
}
hasGlobals() {
return this.globals.hasDoclets();
}
registerLink(doclet) {
const url = helper.createLink(doclet);
helper.registerLink(doclet.longname, url);
return this;
}
addSourcePath({meta}) {
let sourcePath;
if (meta) {
sourcePath = getPathFromMeta(meta);
this.shortPaths[sourcePath] = null;
if (!this._sourcePaths.includes(sourcePath)) {
this._sourcePaths.push(sourcePath);
}
}
return this;
}
_sortModuleDoclets(moduleDoclet, exportsDoclets) {
const result = {
primary: moduleDoclet || null,
secondary: []
};
if (exportsDoclets) {
result.secondary = exportsDoclets
// 1. Never add module doclets as secondary doclets.
// 2. Only show symbols that have a type or a description. Make an exception for
// classes, because we want to show the constructor-signature heading no matter what.
.filter(({type, description, kind}) => (type || description || kind === 'class') &&
kind !== 'module')
.map(function(doclet) {
// TODO: get rid of this, or make it configurable and move to template file
doclet = _.cloneDeep(doclet);
doclet.name = `${doclet.name.replace('module:', 'require("')}")`;
if (doclet.kind === 'class' || doclet.kind === 'function') {
doclet.name = `(${doclet.name})`;
}
this.longname[doclet.longname].remove(doclet, KIND_TO_CATEGORY[doclet.kind]);
return doclet;
}, this);
}
return result;
}
/**
* For classes or functions with the same name as modules (which indicates that the module exports
* only that class or function), attach the classes or functions to the `exports` property of the
* appropriate module doclets. The name of each class or function is also updated for display
* purposes. This method mutates the original arrays.
*
* @private
* @returns {this}
*/
resolveModuleExports() {
let exportsDoclets;
const modules = this.symbols.get(CATEGORIES.MODULES);
const newModules = new SymbolTracker();
let sorted;
if (modules && modules.length) {
modules.forEach((moduleDoclet) => {
exportsDoclets = this.symbols.getLongname(moduleDoclet.longname);
sorted = this._sortModuleDoclets(moduleDoclet, exportsDoclets);
if (sorted.secondary.length) {
moduleDoclet.exports = sorted.secondary;
}
// Add the primary module doclet to the new list of module doclets.
newModules.add(sorted.primary, CATEGORIES.MODULES);
});
}
this.symbols[CATEGORIES.MODULES] = newModules;
return this;
}
addListeners() {
const events = this.symbols.get(CATEGORIES.EVENTS);
const self = this;
events.forEach(eventDoclet => {
let listenerDoclets;
const listeners = self.listeners[eventDoclet.longname];
if (listeners) {
listenerDoclets = listeners.get(CATEGORIES.LISTENERS);
if (listenerDoclets && listenerDoclets.length) {
eventDoclet.listeners = eventDoclet.listeners || [];
listenerDoclets.forEach(({longname}) => {
eventDoclet.listeners.push(longname);
});
}
}
});
return this;
}
findShortPaths() {
let commonPrefix;
const self = this;
if (this._sourcePaths.length) {
commonPrefix = path.commonPrefix(this._sourcePaths);
this._sourcePaths.forEach(filepath => {
self.shortPaths[filepath] = filepath.replace(commonPrefix, '')
// always use forward slashes
.replace(/\\/g, '/');
});
}
return this;
}
addShortPath({meta}) {
let filepath;
if (meta) {
filepath = getPathFromMeta(meta);
if (filepath && hasOwnProp.call(this.shortPaths, filepath)) {
meta.shortpath = this.shortPaths[filepath];
}
}
return this;
}
getCategory(category) {
return this.symbols.get(category);
}
getLongname(longname) {
if (!hasOwnProp.call(this.longname, longname)) {
return {};
}
return this.longname[longname];
}
getMemberof(longname) {
if (!hasOwnProp.call(this.memberof, longname)) {
return {};
}
return this.memberof[longname];
}
getAllLongnames() {
return Object.keys(this.all);
}
getOutputLongnames() {
const needsFile = this.needsFile;
return Object.keys(needsFile).filter(longname => needsFile[longname] === true);
}
getPackage() {
return this.symbols.get(CATEGORIES.PACKAGES)[0];
}
};