UNPKG

@playcanvas/jsdoc-template

Version:

JSDoc 3.x and 4.x compatible template for PlayCanvas

540 lines (454 loc) 20 kB
const fs = require('jsdoc/fs'); const helper = require('jsdoc/util/templateHelper'); const path = require('jsdoc/path'); const handlebars = require("handlebars"); // alphabetical sort ignoring case, correctly sort diacritics, special symbols const localeAlphaSort = function (a, b) { if (a.longname && b.longname) { return a.longname.localeCompare(b.longname); } if (a.name && b.name) { return a.name.localeCompare(b.name); } return 0; }; const alphaSort = localeAlphaSort; /** @module publish */ /** * Generate documentation output. * * @param {object} data - A TaffyDB collection representing all the symbols documented in your code. * @param {object} opts - An object with options information. */ // Build an object for a class by collecting, class, methods, events, base class, inherited methods and events function getClassInfo(data, cls) { var members = []; var methods = []; var staticMethods = []; var events = []; var typedefs = []; // get all data used by classes var all = data({ kind: ["constant", "member", "function", "event", "typedef"], access: { "isUndefined": true }, inherited: { "isUndefined": true }, undocumented: { "isUndefined": true }, memberof: cls.longname }).get(); // sort into different kinds all.forEach((i) => { if (i.kind === "member" || i.kind === "constant") { // Workaround for the following JSDoc issue: // https://github.com/jsdoc/jsdoc/issues/1427 // We know that any PlayCanvas class property that is uppercase should // be static regardless of what scope is set to. if (i.scope === 'instance' && i.name === i.name.toUpperCase()) { i.scope = 'static'; } if (i.scope === 'instance') { if (!cls.properties) { cls.properties = []; } else { // make sure we don't add duplicate properties for (let p = 0; p < cls.properties.length; p++) { if (cls.properties[p].name === i.name) { return; } } } cls.properties.push(i); } else if (i.scope === 'static') { // make sure we don't add duplicate static members for (let p = 0; p < members.length; p++) { if (members[p].name === i.name) { return; } } members.push(i); } } else if (i.kind === "function") { if (i.scope === 'instance') { methods.push(i); } else if (i.scope === 'static') { staticMethods.push(i); } } else if (i.kind === "event") { events.push(i); } else if (i.kind === "typedef") { typedefs.push(i); } }); // sort members alphabetically if (cls.properties) cls.properties.sort(alphaSort); members.sort(alphaSort); methods.sort(alphaSort); staticMethods.sort(alphaSort); events.sort(alphaSort); typedefs.sort(alphaSort); var inherited = null; if (cls.augments && cls.augments.length) { inherited = { cls: [], members: [], methods: [], staticMethods: [], events: [] }; for (var i = 0; i < cls.augments.length; i++) { var base = cls.augments[i]; const queryItems = data({ kind: ["class", "interface"], access: { "isUndefined": true }, undocumented: { "isUndefined": true }, "longname": base }).get(); inherited.cls.push(queryItems[0]); all = data({ kind: ["member", "function", "event"], access: { "isUndefined": true }, inherited: true, undocumented: { "isUndefined": true }, memberof: cls.longname }).get(); // sort into different kinds // eslint-disable-next-line no-loop-func all.forEach((doclet) => { let kind; if (doclet.kind === "member") { if (doclet.scope === 'instance') { if (!inherited.cls[i].properties) { inherited.cls[i].properties = []; } // Making sure there is no existing property with that name. if (!inherited.cls[i].properties.find(d => doclet.name === d.name)) { inherited.cls[i].properties.push(doclet); } } else if (doclet.scope === 'static') { kind = 'members'; } } else if (doclet.kind === "function") { if (doclet.scope === 'instance') { kind = 'methods'; } else if (doclet.scope === 'static') { kind = 'staticMethods'; } } else if (doclet.kind === "event") { kind = 'events'; } // Making sure there is no doclet of that name within the same kind. if (kind && inherited[kind] && !inherited[kind].find(d => doclet.name === d.name)) { inherited[kind].push(doclet); } }); // sort members alphabetically inherited.members.sort(alphaSort); inherited.methods.sort(alphaSort); inherited.staticMethods.sort(alphaSort); inherited.events.sort(alphaSort); if (inherited.cls[i].properties) { inherited.cls[i].properties.sort(alphaSort); } } } return { cls: cls, inherited: inherited ? inherited : null, methods: methods.length ? methods : null, staticMethods: staticMethods.length ? staticMethods : null, members: members.length ? members : null, events: events.length ? events : null, typedefs: typedefs.length ? typedefs : null }; } // return the relative URL from a class or class longname var clsUrl = function (cls) { var name = cls.longname || cls; // This should catch any string literals that do no need to be hyperlinked if (name.includes('"')) { return null; } if (name !== 'pc') { name = 'pc.' + name; } // class name return name + ".html"; }; // Recursively unwrap type (if array or class reference) var unwrapType = function (name, display) { var match; var type; // Check for arrays of types match = /^Array.<(.*)>$/i.exec(name); if (match) { name = match[1]; type = unwrapType(name, display); display = type.display + "[]"; name = type.name; } // Check for key-value (aka dictionary or index signature) types match = /^Object.<string,\s*(.*)>$/i.exec(name); if (match) { name = match[1]; type = unwrapType(name, display); display = "{ [string]: " + type.display + " }"; name = type.name; } // Check for class reference types (as opposed to class instances) match = /^Class.<(.*)>$/i.exec(name); if (match) { name = match[1]; type = unwrapType(name, display); display = "typeof(" + type.display + ")"; name = type.name; } return { name: name, display: display || name }; }; // External type links const builtins = { "undefined": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Undefined", "null": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Null", "array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array", "arraybuffer": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer", "boolean": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean", "element": "https://developer.mozilla.org/en-US/docs/Web/API/Element", "error": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error", "function": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function", "htmlimageelement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement", "htmlvideoelement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement", "htmlaudioelement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement", "htmlcanvaselement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement", "globalthis.keyboardevent": "https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent", "globalthis.mouseevent": "https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent", "number": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number", "object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object", "string": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String", "globalthis.touch": "https://developer.mozilla.org/en-US/docs/Web/API/Touch", "globalthis.touchevent": "https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent", "audio": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio", "audionode": "https://developer.mozilla.org/en-US/docs/Web/API/AudioNode", "audiobuffer": "https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer", "audiocontext": "https://developer.mozilla.org/en-US/docs/Web/API/AudioContext", "audiobuffersourcenode": "https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode", "window": "https://developer.mozilla.org/en-US/docs/Web/API/Window", "int8array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array", "uint8array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array", "uint8clampedarray": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray", "int16array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array", "uint16array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array", "int32array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array", "uint32array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array", "float32array": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array", "gamepad": "https://developer.mozilla.org/en-US/docs/Web/API/Gamepad", "document": "https://developer.mozilla.org/en-US/docs/Web/API/Document", "xmlhttprequest": "https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest", "svgimageelement": "https://developer.mozilla.org/en-US/docs/Web/API/SVGImageElement", "blob": "https://developer.mozilla.org/en-US/docs/Web/API/Blob", "imagedata": "https://developer.mozilla.org/en-US/docs/Web/API/ImageData", "imagebitmap": "https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap", "this": "https://www.typescriptlang.org/docs/handbook/advanced-types.html#polymorphic-this-types", "*": "#" // blerg }; // Return an anchor link string from a type const typeLink = (type) => { // Get the name from string or type object let typeName = type; if (type.longname) { typeName = type.longname; } const unwrapped = unwrapType(typeName); const name = unwrapped.name; const display = unwrapped.display; // Check for builtin type let url; const builtin = builtins[name.toLowerCase()]; if (builtin) { url = builtin; } else if (name.endsWith('Callback')) { url = `pc.html#${name}`; } else { url = clsUrl(name); } return url ? `<a href="${url}">${display}</a>` : display; }; const setupTemplates = (dir) => { const partials = ['analytics', 'class', 'event', 'example', 'header', 'method', 'navigation', 'property', 'typedef']; partials.forEach((partialName) => { const partialPath = path.join(dir, 'tmpl', `${partialName}.hbs`); const partial = fs.readFileSync(partialPath, { encoding: 'utf8' }); handlebars.registerPartial(partialName, partial); }); handlebars.registerHelper('excerpt', function (text) { // return the first characters of the string up to and including the first period (.) not inside curly braces if (text) { const n = text.search(/\.(?![^{]*})/); if (n > 0) { return text.slice(0, n + 1); } return text; } return text; }); // parse a section of text and convert @link tags into links handlebars.registerHelper('parse', function (text) { let result = helper.resolveLinks(text); // add pc. prefix to API links const regex = /href="(\w+)\.html/g; let match; while ((match = regex.exec(result))) { if (match[1] === 'pc') continue; result = result.substring(0, match.index) + `href="pc.${match[1]}.html` + result.substring(match.index + 11 + match[1].length); } return result; }); // return a class URL from a class object or class longname handlebars.registerHelper('clsurl', function (cls) { return clsUrl(cls); }); handlebars.registerHelper('readonly', function (prop) { return prop.readonly ? '<span class="readonly">[read only]</span>' : ''; }); // return a string with the method signature. e.g. (parama, paramb) handlebars.registerHelper('methodsig', function (method) { let sig = '('; if (method.params) { for (let i = 0; i < method.params.length; i++) { if (method.params[i].name.indexOf('.') < 0) { // skip object param documentation if (i !== 0) sig += ', '; let name = method.params[i].name; if (method.params[i].optional) { name = `[${name}]`; } sig += name; } } } return sig + ')'; }); // return a link "<a>" for the set of types from a method/property/param handlebars.registerHelper('type-link', function (types) { try { var result = ''; for (var i = 0; i < types.names.length; i++) { if (i !== 0) result += ', '; result += typeLink(types.names[i]); } return result; } catch (e) { console.error("Can't generate type link. Search output for {{TYPE-ERROR}}"); return '{{TYPE-ERROR}}'; } }); }; const loadTemplate = (path) => { const templateSrc = fs.readFileSync(path, { encoding: "utf-8" }); return handlebars.compile(templateSrc, { preventIndent: true }); }; var copyStaticFiles = function (dir, outdir, callback) { fs.mkPath(outdir); // copy the template's static files to outdir const fromDir = path.join(dir, 'static'); const staticFiles = fs.ls(fromDir, 3); staticFiles.forEach((fileName) => { const toDir = fs.toDir(fileName.replace(fromDir, outdir)); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); callback(); }; var setup = function (opts, callback) { setupTemplates(opts.tmpldir); copyStaticFiles(opts.tmpldir, opts.outdir, function (err) { if (!err) { callback(); } else { console.error(err); } }); }; /** * @param {TAFFY} taffyData - See <http://taffydb.com/>. * @param {object} opts - Options object. * @param {Tutorial} tutorials - Tutorials data. */ exports.publish = (taffyData, opts, tutorials) => { var outdir = path.join(env.pwd, opts.destination); /* eslint-disable-line no-undef */ var tmpldir = opts.template; let data = taffyData; setup({ tmpldir: tmpldir, outdir: outdir }, function () { // register all links var registeredLinks = {}; var invalidCharsInLink = /#\./g; data = helper.prune(data); // Strip all typedefs that are not callbacks data(function () { return this.kind === 'typedef' && !this.name.endsWith('Callback'); }).remove(); data().each((doclet) => { if (registeredLinks[doclet.longname]) return; // move global scope doclets into the 'pc' namespace if (!doclet.memberof) { doclet.scope = 'static'; doclet.memberof = 'pc'; } let link = helper.createLink(doclet); // FIX: replace #. with # in links link = link.replace(invalidCharsInLink, '#'); helper.registerLink(doclet.longname, link); registeredLinks[doclet.longname] = true; }); // Query for list of all classes let classes = data({ kind: ["class", "interface"], access: { "isUndefined": true }, undocumented: { "isUndefined": true } }).get(); const modules = data({ kind: "namespace", access: { "isUndefined": true }, undocumented: { "isUndefined": true } }).get(); // Locale aware sort like taffydata.order classes.sort(localeAlphaSort); modules.sort(localeAlphaSort); classes = modules.concat(classes); classes.forEach((cls) => { cls._class = (cls.kind === "class" || cls.kind === "interface"); cls._namespace = (cls.kind === "namespace"); }); // Compile the standard page template let templatePath = path.join(tmpldir, 'tmpl', 'page.hbs'); let template = loadTemplate(templatePath); // Create a page for each class classes.forEach((cls) => { // register all the class names as links helper.registerLink(cls.longname, clsUrl(cls)); const info = getClassInfo(data, cls); const html = template({ env: opts.env, classes: classes, cls: info, title: cls.longname }); let name = cls.longname; if (name !== 'pc') { name = 'pc.' + name; } const outpath = path.join(outdir, name + '.html'); fs.writeFileSync(outpath, html, 'utf8'); }); templatePath = path.join(tmpldir, 'tmpl', 'frontpage.hbs'); template = loadTemplate(templatePath); const html = template({ env: opts.env, classes: classes }); const outpath = path.join(outdir, 'index.html'); fs.writeFileSync(outpath, html, 'utf8'); }); };