Browse Source

JSDoc: Add custom template. (#30449)

* JSDoc: Add custom template.

* Update CodeQL config.
Michael Herzog 11 months ago
parent
commit
64dfc2c212

+ 2 - 0
.github/codeql-config.yml

@@ -5,3 +5,5 @@ paths-ignore:
   - "examples/jsm/loaders/ifc/**/*.*"
   - "build/*.*"
   - "manual/3rdparty/**/*.*"
+  - "utils/docs/template/static/scripts/fuse/**/*.*"
+  - "utils/docs/template/static/scripts/prettify/**/*.*"

+ 0 - 1
package.json

@@ -96,7 +96,6 @@
     "@rollup/plugin-node-resolve": "^16.0.0",
     "@rollup/plugin-terser": "^0.4.0",
     "chalk": "^5.2.0",
-    "clean-jsdoc-theme": "^4.3.0",
     "concurrently": "^9.0.0",
     "dpdm": "^3.14.0",
     "eslint": "^8.37.0",

+ 0 - 33
utils/docs/custom/three.css

@@ -1,33 +0,0 @@
-:root {
-    --background-color-dark: #222;
-    --background-color-light: #fff;
-    --border-style-dark: 1px solid #444;
-    --border-style-light: 1px solid #E8E8E8;
-}
-
-/* dark */
-
-body { 
-    background-color: var(--background-color-dark) !important;
-}
-.navbar-container {
-    background-color: var(--background-color-dark) !important;
-}
-.sidebar-container {
-    border-right: var(--border-style-dark);
-}
-
-/* light */
-
-body.light  { 
-    background-color: var(--background-color-light) !important;
-}
-.light .navbar-container {
-    background-color: var(--background-color-light) !important;
-}
-.light .sidebar-container {
-    border-right: var(--border-style-light);
-}
-.light .sidebar {
-    background-color: var(--background-color-light) !important;
-}

+ 1 - 10
utils/docs/jsdoc.config.json

@@ -4,12 +4,7 @@
             "encoding": "utf8",
             "package": "package.json",
             "recurse": true,
-            "template": "node_modules/clean-jsdoc-theme",
-            "theme_opts": {
-                "homepageTitle": "three.js docs",
-                "sections": ["Classes"],
-                "include_css": ["utils/docs/custom/three.css"]
-           }
+            "template": "utils/docs/template"
     },
     "plugins": [ "plugins/markdown" ],
     "source": {
@@ -21,9 +16,5 @@
             "src/renderers/common", 
             "src/renderers/webgpu" 
         ]
-    },
-    "markdown": {
-        "hardwrap": false,
-        "idInHeadings": true
     }
 }

+ 6 - 0
utils/docs/template/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "three-template",
+  "version": "1.0.0",
+  "description": "Custom JSDoc template for three.js",
+  "license": "MIT"
+}

+ 782 - 0
utils/docs/template/publish.js

@@ -0,0 +1,782 @@
+const env = require( 'jsdoc/env' );
+const fs = require( 'jsdoc/fs' );
+const helper = require( 'jsdoc/util/templateHelper' );
+const logger = require( 'jsdoc/util/logger' );
+const path = require( 'jsdoc/path' );
+const { taffy } = require( '@jsdoc/salty' );
+const template = require( 'jsdoc/template' );
+const util = require( 'util' );
+
+const htmlsafe = helper.htmlsafe;
+const linkto = helper.linkto;
+const resolveAuthorLinks = helper.resolveAuthorLinks;
+const hasOwnProp = Object.prototype.hasOwnProperty;
+
+let data;
+let view;
+
+let outdir = path.normalize( env.opts.destination );
+
+function mkdirSync( filepath ) {
+
+	return fs.mkdirSync( filepath, { recursive: true } );
+
+}
+
+function find( spec ) {
+
+	return helper.find( data, spec );
+
+}
+
+function getAncestorLinks( doclet ) {
+
+	return helper.getAncestorLinks( data, doclet );
+
+}
+
+function hashToLink( doclet, hash ) {
+
+	let url;
+
+	if ( ! /^(#.+)/.test( hash ) ) {
+
+		return hash;
+
+	}
+
+	url = helper.createLink( doclet );
+	url = url.replace( /(#.+|$)/, hash );
+
+	return `<a href="${url}">${hash}</a>`;
+
+}
+
+function needsSignature( { kind, type, meta } ) {
+
+	let needsSig = false;
+
+	// function and class definitions always get a signature
+	if ( kind === 'function' || kind === 'class' ) {
+
+		needsSig = true;
+
+	} else if ( kind === 'typedef' && type && type.names && type.names.length ) {
+
+		// typedefs that contain functions get a signature, too
+
+		for ( let i = 0, l = type.names.length; i < l; i ++ ) {
+
+			if ( type.names[ i ].toLowerCase() === 'function' ) {
+
+				needsSig = true;
+				break;
+
+			}
+
+		}
+
+	} else if ( kind === 'namespace' && meta && meta.code && meta.code.type && meta.code.type.match( /[Ff]unction/ ) ) {
+
+		// and namespaces that are functions get a signature (but finding them is a
+		// bit messy)
+
+		needsSig = true;
+
+	}
+
+	return needsSig;
+
+}
+
+function updateItemName( item ) {
+
+	let itemName = item.name || '';
+
+	if ( item.variable ) {
+
+		itemName = `&hellip;${itemName}`;
+
+	}
+
+	return itemName;
+
+}
+
+function addParamAttributes( params ) {
+
+	return params.filter( ( { name } ) => name && ! name.includes( '.' ) ).map( updateItemName );
+
+}
+
+function buildItemTypeStrings( item ) {
+
+	const types = [];
+
+	if ( item && item.type && item.type.names ) {
+
+		item.type.names.forEach( name => {
+
+			types.push( linkto( name, htmlsafe( name ) ) );
+
+		} );
+
+	}
+
+	return types;
+
+}
+
+function buildSearchListForData() {
+
+	const searchList = [];
+
+	data().each( ( item ) => {
+
+		if ( item.kind !== 'package' && ! item.inherited ) {
+
+			searchList.push( {
+				title: item.longname,
+				link: linkto( item.longname, item.name ),
+				description: item.description,
+			} );
+
+		}
+
+	} );
+
+	return searchList;
+
+}
+
+function buildAttribsString( attribs ) {
+
+	let attribsString = '';
+
+	if ( attribs && attribs.length ) {
+
+		attribsString = htmlsafe( util.format( '(%s) ', attribs.join( ', ' ) ) );
+
+	}
+
+	return attribsString;
+
+}
+
+function addNonParamAttributes( items ) {
+
+	let types = [];
+
+	items.forEach( item => {
+
+		types = types.concat( buildItemTypeStrings( item ) );
+
+	} );
+
+	return types;
+
+}
+
+function addSignatureParams( f ) {
+
+	const params = f.params ? addParamAttributes( f.params ) : [];
+
+	f.signature = util.format( '%s(%s)', ( f.signature || '' ), params.join( ', ' ) );
+
+}
+
+function addSignatureReturns( f ) {
+
+	const attribs = [];
+	let attribsString = '';
+	let returnTypes = [];
+	let returnTypesString = '';
+	const source = f.yields || f.returns;
+
+	// 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 ( source ) {
+
+		source.forEach( item => {
+
+			helper.getAttribs( item ).forEach( attrib => {
+
+				if ( ! attribs.includes( attrib ) ) {
+
+					attribs.push( attrib );
+
+				}
+
+			} );
+
+		} );
+
+		attribsString = buildAttribsString( attribs );
+
+	}
+
+	if ( source ) {
+
+		returnTypes = addNonParamAttributes( source );
+
+	}
+
+	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 ) {
+
+	const types = f.type ? buildItemTypeStrings( f ) : [];
+
+	f.signature = `${f.signature || ''}<span class="type-signature">${types.length ? ` :${types.join( '|' )}` : ''}</span>`;
+
+}
+
+function addAttribs( f ) {
+
+	const attribs = helper.getAttribs( f );
+	const attribsString = buildAttribsString( attribs );
+
+	f.attribs = util.format( '<span class="type-signature">%s</span>', attribsString );
+
+}
+
+function shortenPaths( files, commonPrefix ) {
+
+	Object.keys( files ).forEach( file => {
+
+		files[ file ].shortened = files[ file ].resolved.replace( commonPrefix, '' )
+		// always use forward slashes
+			.replace( /\\/g, '/' );
+
+	} );
+
+	return files;
+
+}
+
+function getPathFromDoclet( { meta } ) {
+
+	if ( ! meta ) {
+
+		return null;
+
+	}
+
+	return meta.path && meta.path !== 'null' ?
+		path.join( meta.path, meta.filename ) :
+		meta.filename;
+
+}
+
+function generate( title, docs, filename, resolveLinks ) {
+
+	let html;
+
+	resolveLinks = resolveLinks !== false;
+
+	const docData = {
+		env: env,
+		title: title,
+		docs: docs
+	};
+
+	const 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 = 'utf8' ) {
+
+	Object.keys( sourceFiles ).forEach( file => {
+
+		let source;
+		// links are keyed to the shortened path in each doclet's `meta.shortpath` property
+		const 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 );
+
+	} );
+
+}
+
+function buildClassNav( items, itemsSeen, linktoFn ) {
+
+	const coreDirectory = 'src';
+	const addonsDirectory = 'examples/jsm';
+
+	const hierarchy = new Map();
+	hierarchy.set( 'Core', new Map() );
+	hierarchy.set( 'Addons', new Map() );
+
+	let nav = '';
+
+	if ( items.length ) {
+
+		items.forEach( item => {
+
+			let displayName;
+			let itemNav = '';
+
+			if ( ! hasOwnProp.call( itemsSeen, item.longname ) ) {
+
+				if ( env.conf.templates.default.useLongnameInNav ) {
+
+					displayName = item.longname;
+
+				} else {
+
+					displayName = item.name;
+
+				}
+
+				itemNav += `<li>${linktoFn( item.longname, displayName.replace( /\b(module|event):/g, '' ) )}</li>`;
+
+				itemsSeen[ item.longname ] = true;
+
+				const path = item.meta.shortpath;
+
+				if ( path.startsWith( coreDirectory ) ) {
+
+					const subCategory = path.split( '/' )[ 1 ];
+
+					pushNavItem( hierarchy, 'Core', subCategory, itemNav );
+
+				} else if ( path.startsWith( addonsDirectory ) ) {
+
+					const subCategory = path.split( '/' )[ 2 ];
+
+					pushNavItem( hierarchy, 'Addons', subCategory, itemNav );
+
+				}
+
+			}
+
+		} );
+
+		for ( const [ mainCategory, map ] of hierarchy ) {
+
+			nav += `<h2>${mainCategory}</h2>`;
+
+			for ( const [ subCategory, links ] of map ) {
+
+				nav += `<h3>${subCategory}</h3>`;
+
+				let navItems = '';
+
+				for ( const link of links ) {
+
+					navItems += link;
+
+				}
+
+				nav += `<ul>${navItems}</ul>`;
+
+			}
+
+		}
+
+	}
+
+	return nav;
+
+}
+
+function buildGlobalsNav( globals, seen ) {
+
+	let globalNav;
+	let nav = '';
+
+	if ( globals.length ) {
+
+		// TSL
+
+		let tslNav = '';
+
+		globals.forEach( ( { kind, longname, name, tags } ) => {
+
+			if ( kind !== 'typedef' && ! hasOwnProp.call( seen, longname ) && Array.isArray( tags ) && tags[ 0 ].title === 'tsl' ) {
+
+				tslNav += `<li>${linkto( longname, name )}</li>`;
+
+				seen[ longname ] = true;
+
+			}
+
+		} );
+
+		nav += `<h2>TSL</h2><ul>${tslNav}</ul>`;
+
+		// Globals
+
+		globalNav = '';
+
+		globals.forEach( ( { kind, longname, name } ) => {
+
+			if ( kind !== 'typedef' && ! hasOwnProp.call( seen, longname ) ) {
+
+				globalNav += `<li>${linkto( longname, name )}</li>`;
+
+			}
+
+			seen[ 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 += `<h2>Global</h2><ul>${globalNav}</ul>`;
+
+		}
+
+	}
+
+	return nav;
+
+}
+
+function pushNavItem( hierarchy, mainCategory, subCategory, itemNav ) {
+
+	subCategory = subCategory[ 0 ].toUpperCase() + subCategory.slice( 1 ); // capitalize
+
+	if ( hierarchy.get( mainCategory ).get( subCategory ) === undefined ) {
+
+		hierarchy.get( mainCategory ).set( subCategory, [] );
+
+	}
+
+	const categoryList = hierarchy.get( mainCategory ).get( subCategory );
+
+	categoryList.push( itemNav );
+
+}
+
+/**
+ * Create the navigation sidebar.
+ * @param {object} members The members that will be used to create the sidebar.
+ * @param {array<object>} members.classes
+ * @return {string} The HTML for the navigation sidebar.
+ */
+function buildNav( members ) {
+
+	let nav = '';
+	const seen = {};
+
+	nav += buildClassNav( members.classes, seen, linkto );
+	nav += buildGlobalsNav( members.globals, seen );
+
+	return nav;
+
+}
+
+/**
+    @param {TAFFY} taffyData See <http://taffydb.com/>.
+    @param {object} opts
+    @param {Tutorial} tutorials
+ */
+exports.publish = ( taffyData, opts, tutorials ) => {
+
+	const sourceFilePaths = [];
+	let sourceFiles = {};
+	let staticFileFilter;
+	let staticFilePaths;
+	let staticFileScanner;
+
+	data = taffyData;
+
+	const conf = env.conf.templates || {};
+	conf.default = conf.default || {};
+
+	const 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
+	const indexUrl = helper.getUniqueFilename( 'index' );
+	// don't call registerLink() on this one! 'index' is also a valid longname
+
+	const 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 );
+
+	data().each( doclet => {
+
+		let sourcePath;
+
+		doclet.attribs = '';
+
+		if ( doclet.see ) {
+
+			doclet.see.forEach( ( seeItem, i ) => {
+
+				doclet.see[ i ] = hashToLink( doclet, seeItem );
+
+			} );
+
+		}
+
+		// build a list of source files
+		if ( doclet.meta ) {
+
+			sourcePath = getPathFromDoclet( doclet );
+			sourceFiles[ sourcePath ] = {
+				resolved: sourcePath,
+				shortened: null
+			};
+			if ( ! sourceFilePaths.includes( sourcePath ) ) {
+
+				sourceFilePaths.push( sourcePath );
+
+			}
+
+		}
+
+	} );
+
+	// update outdir if necessary, then create outdir
+	const packageInfo = ( find( { kind: 'package' } ) || [] )[ 0 ];
+	if ( packageInfo && packageInfo.name ) {
+
+		outdir = path.join( outdir, packageInfo.name, ( packageInfo.version || '' ) );
+
+	}
+
+	fs.mkPath( outdir );
+
+	// copy the template's static files to outdir
+	const fromDir = path.join( templatePath, '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 );
+
+	} );
+
+	// copy user-specified static files to outdir
+	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( filePath => {
+
+			filePath = path.resolve( env.pwd, filePath );
+			const extraStaticFiles = staticFileScanner.scan( [ filePath ], 10, staticFileFilter );
+
+			extraStaticFiles.forEach( fileName => {
+
+				const sourcePath = fs.toDir( filePath );
+				const 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( doclet => {
+
+		let docletPath;
+		const url = helper.createLink( doclet );
+
+		helper.registerLink( doclet.longname, url );
+
+		// add a shortened version of the full path
+		if ( doclet.meta ) {
+
+			docletPath = getPathFromDoclet( doclet );
+			docletPath = sourceFiles[ docletPath ].shortened;
+			if ( docletPath ) {
+
+				doclet.meta.shortpath = docletPath;
+
+			}
+
+		}
+
+	} );
+
+	data().each( doclet => {
+
+		const url = helper.longnameToUrl[ doclet.longname ];
+
+		if ( url.includes( '#' ) ) {
+
+			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( doclet => {
+
+		doclet.ancestors = getAncestorLinks( doclet );
+
+		if ( doclet.kind === 'member' ) {
+
+			addSignatureTypes( doclet );
+			addAttribs( doclet );
+
+		}
+
+		if ( doclet.kind === 'constant' ) {
+
+			addSignatureTypes( doclet );
+			addAttribs( doclet );
+			doclet.kind = 'member';
+
+		}
+
+	} );
+
+	const members = helper.getMembers( data );
+	members.tutorials = tutorials.children;
+
+	// output pretty-printed source files by default
+	const outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false;
+
+	// add template helpers
+	view.find = find;
+	view.linkto = linkto;
+	view.resolveAuthorLinks = resolveAuthorLinks;
+	view.htmlsafe = htmlsafe;
+	view.outputSourceFiles = outputSourceFiles;
+
+	// once for all
+	view.nav = buildNav( members );
+
+	// 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
+	const files = find( { kind: 'file' } );
+	const packages = find( { kind: 'package' } );
+
+	generate( '', // MODIFIED (Remove Home title)
+		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
+	const classes = taffy( members.classes );
+
+	Object.keys( helper.longnameToUrl ).forEach( longname => {
+
+		const myClasses = helper.find( classes, { longname: longname } );
+
+		if ( myClasses.length ) {
+
+			generate( `${myClasses[ 0 ].name}`, myClasses, helper.longnameToUrl[ longname ] );
+
+		}
+
+	} );
+
+	// search
+
+	const searchList = buildSearchListForData();
+
+	mkdirSync( path.join( outdir, 'data' ) );
+
+	fs.writeFileSync(
+		path.join( outdir, 'data', 'search.json' ),
+		JSON.stringify( {
+			list: searchList,
+		} )
+	);
+
+};

File diff suppressed because it is too large
+ 8 - 0
utils/docs/template/static/scripts/fuse/fuse.js


+ 33 - 0
utils/docs/template/static/scripts/linenumber.js

@@ -0,0 +1,33 @@
+/*global document */
+( () => {
+
+	const source = document.getElementsByClassName( 'prettyprint source linenums' );
+	let i = 0;
+	let lineNumber = 0;
+	let lineId;
+	let lines;
+	let totalLines;
+	let anchorHash;
+
+	if ( source && source[ 0 ] ) {
+
+		anchorHash = document.location.hash.substring( 1 );
+		lines = source[ 0 ].getElementsByTagName( 'li' );
+		totalLines = lines.length;
+
+		for ( ; i < totalLines; i ++ ) {
+
+			lineNumber ++;
+			lineId = `line${lineNumber}`;
+			lines[ i ].id = lineId;
+			if ( lineId === anchorHash ) {
+
+				lines[ i ].className += ' selected';
+
+			}
+
+		}
+
+	}
+
+} )();

+ 202 - 0
utils/docs/template/static/scripts/prettify/Apache-License-2.0.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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
+
+       http://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.

+ 2 - 0
utils/docs/template/static/scripts/prettify/lang-css.js

@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com",
+/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);

+ 28 - 0
utils/docs/template/static/scripts/prettify/prettify.js

@@ -0,0 +1,28 @@
+var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
+(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
+[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
+f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
+(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
+{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
+t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
+"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
+l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
+q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
+q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
+"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
+a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
+for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
+m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
+a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
+j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
+"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
+H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
+J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
+I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
+["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
+/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
+["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
+hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
+!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
+250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
+PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();

+ 159 - 0
utils/docs/template/static/scripts/search.js

@@ -0,0 +1,159 @@
+const contentContainer = document.querySelector( '#page-content' );
+const searchContainer = document.querySelector( '#search-content' );
+const resultBox = document.querySelector( '#search-result' );
+
+// eslint-disable-next-line no-unused-vars
+function hideSearch() {
+
+	searchContainer.style.display = 'none';
+	contentContainer.style.display = 'block';
+
+
+}
+
+function showResultText( text ) {
+
+	resultBox.innerHTML = text;
+
+}
+
+function showSearch() {
+
+	searchContainer.style.display = 'block';
+	contentContainer.style.display = 'none';
+
+}
+
+function extractUrlBase( url ) {
+
+	const index = url.lastIndexOf( '/' );
+
+	if ( index === - 1 ) return './';
+
+	return url.slice( 0, index + 1 );
+
+}
+
+async function fetchAllData() {
+
+	const { origin, pathname } = location;
+
+	const baseURL = extractUrlBase( pathname );
+
+	const base = origin + baseURL;
+
+	const url = new URL( 'data/search.json', base );
+	const result = await fetch( url );
+	const { list } = await result.json();
+
+	return list;
+
+}
+
+
+function buildSearchResult( result ) {
+
+	let output = '';
+	const removeHTMLTagsRegExp = /(<([^>]+)>)/ig;
+
+	for ( const res of result ) {
+
+		const { title = '', description = '' } = res.item;
+
+		const _link = res.item.link.replace( '<a href="', '' ).replace( /">.*/, '' );
+		const _title = title.replace( removeHTMLTagsRegExp, '' );
+		const _description = description.replace( removeHTMLTagsRegExp, '' );
+
+		output += `
+    <a href="${_link}" class="search-result-item">
+      <span class="search-result-item-title">${_title}</span>
+      <span class="search-result-item-description">- ${_description || 'No description available.'}</span>
+    </a>
+    `;
+
+	}
+
+	return output;
+
+}
+
+function getSearchResult( list, keys, searchKey ) {
+
+	const defaultOptions = {
+		shouldSort: true,
+		threshold: 0.4,
+		location: 0,
+		distance: 100,
+		maxPatternLength: 32,
+		minMatchCharLength: 1,
+		keys: keys
+	};
+
+	const options = { ...defaultOptions };
+
+	// eslint-disable-next-line no-undef
+	const searchIndex = Fuse.createIndex( options.keys, list );
+
+	// eslint-disable-next-line no-undef
+	const fuse = new Fuse( list, options, searchIndex );
+
+	const result = fuse.search( searchKey );
+
+	if ( result.length > 20 ) {
+
+		return result.slice( 0, 20 );
+
+	}
+
+	return result;
+
+}
+
+let searchData;
+
+// eslint-disable-next-line no-unused-vars
+async function search( value ) {
+
+	if ( value === '' ) {
+
+		hideSearch();
+		return;
+
+	}
+
+	showSearch();
+
+	const keys = [ 'title', 'description' ];
+
+	if ( searchData === undefined ) {
+
+		showResultText( 'Loading...' );
+
+		try {
+
+			searchData = await fetchAllData();
+
+		} catch ( e ) {
+
+			console.log( e );
+			showResultText( 'Failed to load result.' );
+
+			return;
+
+		}
+
+	}
+
+	const result = getSearchResult( searchData, keys, value );
+
+	if ( ! result.length ) {
+
+		showResultText( 'No result found! Try some different combination.' );
+
+		return;
+
+	}
+
+	resultBox.innerHTML = buildSearchResult( result );
+
+}

+ 280 - 0
utils/docs/template/static/styles/page.css

@@ -0,0 +1,280 @@
+h3 {
+	margin-top: 16px;
+	margin-bottom: 16px;
+}
+
+/* Below CSS should only affect the page section */
+
+#page {
+	--font-size: 16px;
+	--line-height: 26px;
+	--panel-width: 300px;
+	--page-padding: 24px;
+	--max-width: 960px;
+	--icon-size: 20px;
+	--border-style: 1px solid #E8E8E8;
+	
+	color: var(--text-color);
+	tab-size: 4;
+	overflow: auto;
+	max-width: var(--max-width);
+	margin: 0 auto;
+	padding-top: var(--page-padding);
+	padding-bottom: var(--page-padding);
+	padding-right: var(--page-padding);
+	padding-left: var(--page-padding);
+	word-break: break-word;
+	
+	@font-face {
+		font-family: 'Roboto Mono';
+		src: local('Roboto Mono'), local('RobotoMono-Regular'), url('/files/RobotoMono-Regular.woff2') format('woff2');
+		font-style: normal;
+		font-weight: 400;
+	}
+	
+	@font-face {
+		font-family: 'Inter';
+		font-style: normal;
+		font-weight: 400;
+		src: local('Inter-Regular'), url("/files/Inter-Regular.woff2?v=3.6") format("woff2");
+	}
+	
+	@font-face {
+		font-family: 'Inter';
+		font-style: normal;
+		font-weight: 600;
+		src: local('Inter-SemiBold'), url("/files/Inter-SemiBold.woff2?v=3.6") format("woff2");
+	}
+
+	a {
+		color: var(--color-blue);
+		cursor: pointer;
+		text-decoration: none;
+	}
+	
+	h1 {
+		font-size: 40px;
+		line-height: 48px;
+		font-weight: normal;
+		margin-left: -2px;
+		margin-top: 16px;
+	}
+	
+	h2 {
+		font-size: 28px;
+		line-height: 36px;
+		font-weight: normal;
+		margin-left: -1px;
+		margin-top: 28px;
+	}
+	
+	h3 {
+		font-size: 20px;
+		line-height: 28px;
+		font-weight: normal;
+		margin-top: 32px;
+		margin-bottom: 32px;
+	}
+
+	p,
+	div,
+	table,
+	ol,
+	ul {
+		margin-top: 16px;
+		margin-bottom: 16px;
+	}
+	
+	p {
+		padding-right: 16px;
+	}
+	
+	ul, ol {
+		box-sizing: border-box;
+		padding-left: 24px;
+	}
+	ul li,
+	ol li {
+		padding-left: 4px;
+		margin-bottom: 4px;
+	}
+	
+	li ul,
+	li ol {
+		margin-top: 4px;
+	}
+
+	table {
+		width: 100%;
+		border-collapse: collapse;
+	}
+	
+	.desc {
+		padding-left: 0px;
+	}
+	
+	table th,
+	table td {
+		text-align: left;
+		vertical-align: top;
+		padding: 16px 8px;
+		border-bottom: var(--border-style);
+		max-width: 480px;
+		min-width: 120px;
+	}
+	
+	table th {
+		text-decoration: none;
+	}
+	table th:first-child,
+	table td:first-child {
+		padding-left: 0;
+	}
+
+	table p {
+		margin: 0;
+	}
+	
+	strong {
+		font-weight: 600;
+	}
+	
+	a.permalink {
+		float: right;
+		margin-left: 5px;
+		display: none;
+	}
+	
+	a.param,
+	span.param {
+		color: #999;
+	}
+	
+	a.param:hover {
+		color: var(--text-color);
+	}
+
+	.method,
+	.member {
+		margin-bottom: 64px;
+	}
+
+	ol.linenums {
+		padding-left: 64px;
+	}
+
+	code {
+		display: inline-block;
+		vertical-align: middle;
+		border-radius: 4px;
+		padding: 0px 5px;
+	}
+
+	pre {
+		overflow: auto;
+		white-space: pre-wrap;
+		font-size: calc(var(--font-size) - 1px);
+		line-height: calc(var(--line-height) - 1px);
+	}
+
+	pre > code {
+		display: block;
+		box-sizing: border-box;
+		border-radius: 0;
+		padding: calc(var(--page-padding) - 6px) var(--page-padding);
+	}
+
+	.link-anchor {
+		color: #ddd;
+		visibility: hidden;
+	}
+
+	.name:hover .link-anchor {
+		visibility: visible;
+	}
+
+	.search-result-item {
+		border-bottom: var(--border-style);
+		display: block;
+		padding: 16px 0;
+	}
+	
+	.search-result-item-description {
+		color: var(--text-color);
+	}
+
+}
+
+@media (prefers-color-scheme: dark) {
+	
+	#page {
+		--text-color: #bbb;
+		--border-style: 1px solid #444;
+
+		.link-anchor {
+			color: #555;
+		}
+	
+	}
+
+}
+
+@media all and ( min-width: 1700px ) {
+	
+	#page {
+		--panel-width: 360px;
+		--font-size: 18px;
+		--line-height: 28px;
+		--max-width: 1080px;
+		--page-padding: 28px;
+		--icon-size: 24px;
+
+		h1 {
+			font-size: 42px;
+			line-height: 50px;
+		}
+
+		h2 {
+			font-size: 32px;
+			line-height: 40px;
+		}
+
+		h3 {
+			font-size: 24px;
+			line-height: 32px;
+		}
+
+	}
+
+}
+
+/* mobile */
+
+@media all and ( max-width: 640px ) {
+
+	#page {
+		--page-padding: 16px;
+		--icon-size: 24px;
+
+		padding: var(--page-padding);
+
+		h1 {
+			font-size: 28px;
+			line-height: 36px;
+			padding-right: 20px;
+			margin-top: 0;
+		}
+
+		h2 {
+			font-size: 24px;
+			line-height: 32px;
+			margin-top: 24px;
+		}
+
+		h3 {
+			font-size: 20px;
+			line-height: 28px;
+		}
+
+	}
+}

+ 29 - 0
utils/docs/template/static/styles/prettify-three.css

@@ -0,0 +1,29 @@
+pre .str, code .str { color: #8000ff; } /* string */
+pre .kwd, code .kwd { color: #30b030; } /* keyword */
+pre .com, code .com { color: #999999; } /* comment */
+pre .typ, code .typ { color: #2194ce; } /* type */
+pre .lit, code .lit { color: #ff0080; } /* literal */
+pre .pun, code .pun { color: #888888; } /* punctuation */
+pre .pln, code .pln { color: #444444; } /* plaintext */
+pre .dec, code .dec { color: #22c0c4; } /* decimal */
+
+pre, code {
+	background-color: #F5F5F5;
+	font-family: 'Roboto Mono', monospace;
+}
+
+@media (prefers-color-scheme: dark) {
+
+	pre .tag, code .tag { color: #2194ce; } /* HTML tag */
+	pre .atn, code .atn { color: #BB55FF; } /* HTML attribute name */
+	pre .atv, code .atv { color: #30b030; } /* HTML attribute value */
+	pre .str, code .str { color: #BB55FF; } /* string */
+	pre .com, code .com { color: #666666; } /* comment */
+	pre .lit, code .lit { color: #ff3399; } /* literal */
+	pre .pln, code .pln { color: #aaaaaa; } /* plaintext */
+
+	pre, code {
+		background-color: #333333;
+	}
+
+}

+ 10 - 0
utils/docs/template/tmpl/augments.tmpl

@@ -0,0 +1,10 @@
+<?js
+    var data = obj;
+    var self = this;
+?>
+
+<?js if (data.augments && data.augments.length) { ?>
+    <ul><?js data.augments.forEach(function(a) { ?>
+        <li><?js= self.linkto(a, a) ?></li>
+    <?js }) ?></ul>
+<?js } ?>

+ 188 - 0
utils/docs/template/tmpl/container.tmpl

@@ -0,0 +1,188 @@
+<?js
+    var self = this;
+    var isGlobalPage;
+
+    docs.forEach(function(doc, i) {
+?>
+
+<?js
+    // we only need to check this once
+    if (typeof isGlobalPage === 'undefined') {
+        isGlobalPage = (doc.kind === 'globalobj');
+    }
+?>
+<?js if (doc.kind === 'mainpage' || (doc.kind === 'package')) { ?>
+    <?js= self.partial('mainpage.tmpl', doc) ?>
+<?js } else if (doc.kind === 'source') { ?>
+    <?js= self.partial('source.tmpl', doc) ?>
+<?js } else { ?>
+
+<section>
+
+<header>
+    <?js if (!doc.longname || doc.kind !== 'module') { ?>
+        <?js if (doc.classdesc) { ?>
+            <div class="class-description"><?js= doc.classdesc ?></div>
+        <?js } ?>
+    <?js } else if (doc.kind === 'module' && doc.modules) { ?>
+        <?js doc.modules.forEach(function(module) { ?>
+            <?js if (module.classdesc) { ?>
+                <div class="class-description"><?js= module.classdesc ?></div>
+            <?js } ?>
+        <?js }) ?>
+    <?js } ?>
+</header>
+
+<article>
+    <div class="container-overview">
+    <?js if (doc.kind === 'module' && doc.modules) { ?>
+        <?js if (doc.description) { ?>
+            <div class="description"><?js= doc.description ?></div>
+        <?js } ?>
+
+        <?js doc.modules.forEach(function(module) { ?>
+            <?js= self.partial('method.tmpl', module) ?>
+        <?js }) ?>
+    <?js } else if (doc.kind === 'class' || (doc.kind === 'namespace' && doc.signature)) { ?>
+        <?js= self.partial('method.tmpl', doc) ?>
+    <?js } else { ?>
+        <?js if (doc.description) { ?>
+            <div class="description"><?js= doc.description ?></div>
+        <?js } ?>
+
+        <?js= self.partial('details.tmpl', doc) ?>
+
+        <?js if (doc.examples && doc.examples.length) { ?>
+            <h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
+            <?js= self.partial('examples.tmpl', doc.examples) ?>
+        <?js } ?>
+    <?js } ?>
+    </div>
+
+    <?js if (doc.augments && doc.augments.length) { ?>
+        <h3 class="subsection-title">Extends</h3>
+
+        <?js= self.partial('augments.tmpl', doc) ?>
+    <?js } ?>
+
+    <?js if (doc.requires && doc.requires.length) { ?>
+        <h3 class="subsection-title">Requires</h3>
+
+        <ul><?js doc.requires.forEach(function(r) { ?>
+            <li><?js= self.linkto(r, r) ?></li>
+        <?js }); ?></ul>
+    <?js } ?>
+
+    <?js
+        var classes = self.find({kind: 'class', memberof: doc.longname});
+        if (!isGlobalPage && classes && classes.length) {
+    ?>
+        <h3 class="subsection-title">Classes</h3>
+
+        <dl><?js classes.forEach(function(c) { ?>
+            <dt><?js= self.linkto(c.longname, c.name) ?></dt>
+            <dd><?js if (c.summary) { ?><?js= c.summary ?><?js } ?></dd>
+        <?js }); ?></dl>
+    <?js } ?>
+
+    <?js
+        var interfaces = self.find({kind: 'interface', memberof: doc.longname});
+        if (!isGlobalPage && interfaces && interfaces.length) {
+    ?>
+        <h3 class="subsection-title">Interfaces</h3>
+
+        <dl><?js interfaces.forEach(function(i) { ?>
+            <dt><?js= self.linkto(i.longname, i.name) ?></dt>
+            <dd><?js if (i.summary) { ?><?js= i.summary ?><?js } ?></dd>
+        <?js }); ?></dl>
+    <?js } ?>
+
+    <?js
+        var mixins = self.find({kind: 'mixin', memberof: doc.longname});
+        if (!isGlobalPage && mixins && mixins.length) {
+    ?>
+        <h3 class="subsection-title">Mixins</h3>
+
+        <dl><?js mixins.forEach(function(m) { ?>
+            <dt><?js= self.linkto(m.longname, m.name) ?></dt>
+            <dd><?js if (m.summary) { ?><?js= m.summary ?><?js } ?></dd>
+        <?js }); ?></dl>
+    <?js } ?>
+
+    <?js
+        var namespaces = self.find({kind: 'namespace', memberof: doc.longname});
+        if (!isGlobalPage && namespaces && namespaces.length) {
+    ?>
+        <h3 class="subsection-title">Namespaces</h3>
+
+        <dl><?js namespaces.forEach(function(n) { ?>
+            <dt><?js= self.linkto(n.longname, n.name) ?></dt>
+            <dd><?js if (n.summary) { ?><?js= n.summary ?><?js } ?></dd>
+        <?js }); ?></dl>
+    <?js } ?>
+
+    <?js
+        var members = self.find({kind: 'member', memberof: isGlobalPage ? {isUndefined: true} : doc.longname});
+
+        // symbols that are assigned to module.exports are not globals, even though they're not a memberof anything
+        if (isGlobalPage && members && members.length && members.forEach) {
+            members = members.filter(function(m) {
+                return m.longname && m.longname.indexOf('module:') !== 0;
+            });
+        }
+        if (members && members.length && members.forEach) {
+    ?>
+        <h3 class="subsection-title">Members</h3>
+
+        <?js members.forEach(function(p) { ?>
+            <?js= self.partial('members.tmpl', p) ?>
+        <?js }); ?>
+    <?js } ?>
+
+    <?js
+        var methods = self.find({kind: 'function', memberof: isGlobalPage ? {isUndefined: true} : doc.longname});
+        if (methods && methods.length && methods.forEach) {
+    ?>
+        <h3 class="subsection-title">Methods</h3>
+
+        <?js methods.forEach(function(m) { ?>
+            <?js= self.partial('method.tmpl', m) ?>
+        <?js }); ?>
+    <?js } ?>
+
+    <?js
+        var typedefs = self.find({kind: 'typedef', memberof: isGlobalPage ? {isUndefined: true} : doc.longname});
+        if (typedefs && typedefs.length && typedefs.forEach) {
+    ?>
+        <h3 class="subsection-title">Type Definitions</h3>
+
+        <?js typedefs.forEach(function(e) {
+                if (e.signature) {
+            ?>
+                <?js= self.partial('method.tmpl', e) ?>
+            <?js
+                }
+                else {
+            ?>
+                <?js= self.partial('members.tmpl', e) ?>
+            <?js
+                }
+            }); ?>
+    <?js } ?>
+
+    <?js
+        var events = self.find({kind: 'event', memberof: isGlobalPage ? {isUndefined: true} : doc.longname});
+        if (events && events.length && events.forEach) {
+    ?>
+        <h3 class="subsection-title">Events</h3>
+
+        <?js events.forEach(function(e) { ?>
+            <?js= self.partial('method.tmpl', e) ?>
+        <?js }); ?>
+    <?js } ?>
+</article>
+
+</section>
+<?js } ?>
+
+<?js }); ?>

+ 143 - 0
utils/docs/template/tmpl/details.tmpl

@@ -0,0 +1,143 @@
+<?js
+var data = obj;
+var self = this;
+var defaultObjectClass = '';
+
+// Check if the default value is an object or array; if so, apply code highlighting
+if (data.defaultvalue && (data.defaultvaluetype === 'object' || data.defaultvaluetype === 'array')) {
+    data.defaultvalue = "<pre class=\"prettyprint\"><code>" + data.defaultvalue + "</code></pre>";
+    defaultObjectClass = ' class="object-value"';
+}
+?>
+<?js
+    var properties = data.properties;
+    if (properties && properties.length && properties.forEach && !data.hideconstructor) {
+?>
+
+    <h5 class="subsection-title">Properties:</h5>
+
+    <?js= this.partial('properties.tmpl', data) ?>
+
+<?js } ?>
+
+<dl class="details">
+
+    <?js if (data.version) {?>
+    <dt class="tag-version">Version:</dt>
+    <dd class="tag-version"><ul class="dummy"><li><?js= version ?></li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.since) {?>
+    <dt class="tag-since">Since:</dt>
+    <dd class="tag-since"><ul class="dummy"><li><?js= since ?></li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.inherited && data.inherits && !data.overrides) { ?>
+    <dt class="inherited-from">Inherited From:</dt>
+    <dd class="inherited-from"><ul class="dummy"><li>
+        <?js= this.linkto(data.inherits, this.htmlsafe(data.inherits)) ?>
+    </li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.overrides) { ?>
+    <dt class="tag-overrides">Overrides:</dt>
+    <dd class="tag-overrides"><ul class="dummy"><li>
+        <?js= this.linkto(data.overrides, this.htmlsafe(data.overrides)) ?>
+    </li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.implementations && data.implementations.length) { ?>
+    <dt class="implementations">Implementations:</dt>
+    <dd class="implementations"><ul>
+        <?js data.implementations.forEach(function(impl) { ?>
+            <li><?js= self.linkto(impl, self.htmlsafe(impl)) ?></li>
+        <?js }); ?>
+    </ul></dd>
+    <?js } ?>
+
+    <?js if (data.implements && data.implements.length) { ?>
+    <dt class="implements">Implements:</dt>
+    <dd class="implements"><ul>
+        <?js data.implements.forEach(function(impl) { ?>
+            <li><?js= self.linkto(impl, self.htmlsafe(impl)) ?></li>
+        <?js }); ?>
+    </ul></dd>
+    <?js } ?>
+
+    <?js if (data.mixes && data.mixes.length) { ?>
+        <dt class="mixes">Mixes In:</dt>
+
+        <dd class="mixes"><ul>
+        <?js data.mixes.forEach(function(a) { ?>
+            <li><?js= self.linkto(a, a) ?></li>
+        <?js }); ?>
+        </ul></dd>
+    <?js } ?>
+
+    <?js if (data.deprecated) { ?>
+        <dt class="important tag-deprecated">Deprecated:</dt><?js
+            if (data.deprecated === true) { ?><dd class="yes-def tag-deprecated"><ul class="dummy"><li>Yes</li></ul></dd><?js }
+            else { ?><dd><ul class="dummy"><li><?js= data.deprecated ?></li></ul></dd><?js }
+        ?>
+    <?js } ?>
+
+    <?js if (data.author && author.length) {?>
+    <dt class="tag-author">Author:</dt>
+    <dd class="tag-author">
+        <ul><?js author.forEach(function(a) { ?>
+            <li><?js= self.resolveAuthorLinks(a) ?></li>
+        <?js }); ?></ul>
+    </dd>
+    <?js } ?>
+
+    <?js if (data.copyright) {?>
+    <dt class="tag-copyright">Copyright:</dt>
+    <dd class="tag-copyright"><ul class="dummy"><li><?js= copyright ?></li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.license) {?>
+    <dt class="tag-license">License:</dt>
+    <dd class="tag-license"><ul class="dummy"><li><?js= license ?></li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.defaultvalue) {?>
+    <dt class="tag-default">Default Value:</dt>
+    <dd class="tag-default"><ul class="dummy">
+            <li<?js= defaultObjectClass ?>><?js= data.defaultvalue ?></li>
+        </ul></dd>
+    <?js } ?>
+
+    <?js if (data.meta && self.outputSourceFiles) {?>
+    <dt class="tag-source">Source:</dt>
+    <dd class="tag-source"><ul class="dummy"><li>
+        <?js= self.linkto(meta.shortpath) ?>, <?js= self.linkto(meta.shortpath, 'line ' + meta.lineno, null, 'line' + meta.lineno) ?>
+    </li></ul></dd>
+    <?js } ?>
+
+    <?js if (data.tutorials && tutorials.length) {?>
+    <dt class="tag-tutorial">Tutorials:</dt>
+    <dd class="tag-tutorial">
+        <ul><?js tutorials.forEach(function(t) { ?>
+            <li><?js= self.tutoriallink(t) ?></li>
+        <?js }); ?></ul>
+    </dd>
+    <?js } ?>
+
+    <?js if (data.see && see.length) {?>
+    <dt class="tag-see">See:</dt>
+    <dd class="tag-see">
+        <ul><?js see.forEach(function(s) { ?>
+            <li><?js= self.linkto(s) ?></li>
+        <?js }); ?></ul>
+    </dd>
+    <?js } ?>
+
+    <?js if (data.todo && todo.length) {?>
+    <dt class="tag-todo">To Do:</dt>
+    <dd class="tag-todo">
+        <ul><?js todo.forEach(function(t) { ?>
+            <li><?js= t ?></li>
+        <?js }); ?></ul>
+    </dd>
+    <?js } ?>
+</dl>

+ 32 - 0
utils/docs/template/tmpl/exceptions.tmpl

@@ -0,0 +1,32 @@
+<?js
+    var data = obj;
+?>
+<?js if (data.description && data.type && data.type.names) { ?>
+<dl>
+    <dt>
+        <div class="param-desc">
+        <?js= data.description ?>
+        </div>
+    </dt>
+    <dd></dd>
+    <dt>
+        <dl>
+            <dt>
+                Type
+            </dt>
+            <dd>
+                <?js= this.partial('type.tmpl', data.type.names) ?>
+            </dd>
+        </dl>
+    </dt>
+    <dd></dd>
+</dl>
+<?js } else { ?>
+    <div class="param-desc">
+    <?js if (data.description) { ?>
+        <?js= data.description ?>
+    <?js } else if (data.type && data.type.names) { ?>
+        <?js= this.partial('type.tmpl', data.type.names) ?>
+    <?js } ?>
+    </div>
+<?js } ?>

+ 135 - 0
utils/docs/template/tmpl/layout.tmpl

@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>three.js docs</title>
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)"/>
+    <link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)" />
+    <link rel="stylesheet" type="text/css" href="/files/main.css">
+
+    <script src="scripts/fuse/fuse.js"></script>
+    <script src="scripts/prettify/prettify.js"></script>
+    <script src="scripts/prettify/lang-css.js"></script>
+
+    <link type="text/css" rel="stylesheet" href="styles/prettify-three.css">
+    <link type="text/css" rel="stylesheet" href="styles/page.css">
+
+    <!-- console sandbox -->
+    <script type="module">
+        import * as THREE from '/build/three.module.js';
+        window.THREE = THREE;
+    </script>
+</head>
+
+<body>
+
+<div id="panel">
+
+    <div id="header">
+        <h1><a href="https://threejs.org">three.js</a></h1>
+
+        <div id="sections">
+            <span class="selected">docs</span>
+            <a href="/examples/#webgl_animation_keyframes">examples</a>
+        </div>
+        <div id="expandButton"></div>
+    </div>
+
+    <div id="panelScrim"></div>
+
+    <div id="contentWrapper">
+        <div id="inputWrapper">
+            <input placeholder="" type="text" id="filterInput" autocorrect="off" autocapitalize="off" spellcheck="false" />
+            <div id="clearSearchButton"></div>
+        </div>
+        <div id="content">
+            <nav>
+                <?js= this.nav ?>
+            </nav>
+        </div>
+    </div>
+</div>
+
+<div id="viewer">
+    <div id="page">
+        <div id="page-content">
+            <h1><?js= title ?></h1>
+            <?js= content ?>
+        </div>
+        <?js= this.partial('search.tmpl')?>
+    </div>
+</div>
+
+<script> 
+
+const expandButton = document.getElementById( 'expandButton' );
+const clearSearchButton = document.getElementById( 'clearSearchButton' );
+const fliterInput = document.getElementById( 'filterInput' );
+
+// Functionality for hamburger button (on small devices)
+
+expandButton.onclick = function ( event ) {
+
+    event.preventDefault();
+    panel.classList.toggle( 'open' );
+
+};
+
+panelScrim.onclick = function ( event ) {
+
+    event.preventDefault();
+    panel.classList.toggle( 'open' );
+
+};
+
+// Functionality for search/filter input field
+
+fliterInput.onfocus = function () {
+
+    panel.classList.add( 'searchFocused' );
+
+};
+
+fliterInput.onblur = function () {
+
+    if ( fliterInput.value === '' ) {
+
+        panel.classList.remove( 'searchFocused' );
+
+    }
+
+};
+
+filterInput.oninput = function () {
+
+    const term = filterInput.value.trim();
+    search( term ); // defined in search.js
+
+};
+
+clearSearchButton.onclick = function () {
+
+    fliterInput.value = '';
+    fliterInput.focus();
+    hideSearch(); // defined in search.js
+
+};
+
+prettyPrint();
+
+console.log( [
+            '    __     __',
+            ' __/ __\\  / __\\__   ____   _____   _____',
+            '/ __/  /\\/ /  /___\\/ ____\\/ _____\\/ _____\\',
+            '\\/_   __/ /   _   / /  __/ / __  / / __  /_   __   _____',
+            '/ /  / / /  / /  / /  / / /  ___/ /  ___/\\ _\\/ __\\/ _____\\',
+            '\\/__/  \\/__/\\/__/\\/__/  \\/_____/\\/_____/\\/__/ /  / /  ___/',
+            '                                         / __/  /  \\__  \\',
+            '                                         \\/____/\\/_____/'
+].join( '\n' ) );
+</script>
+<script src="scripts/linenumber.js"></script>
+<script src="scripts/search.js"></script>
+</body>
+</html>

+ 4 - 0
utils/docs/template/tmpl/mainpage.tmpl

@@ -0,0 +1,4 @@
+<?js
+var data = obj;
+var self = this;
+?>

+ 33 - 0
utils/docs/template/tmpl/members.tmpl

@@ -0,0 +1,33 @@
+<?js
+var data = obj;
+var self = this;
+?>
+<div class="member">
+    <h4 class="name" id="<?js= id ?>"><?js= data.attribs + name + (data.signature ? data.signature : '') ?>
+    <a href="#<?js= id ?>" class="link-anchor">#</a>
+    </h4>
+
+    <?js if (data.summary) { ?>
+    <p class="summary"><?js= summary ?></p>
+    <?js } ?>
+
+    <?js if (data.description) { ?>
+    <div class="description">
+        <?js= data.description ?>
+    </div>
+    <?js } ?>
+
+    <?js= this.partial('details.tmpl', data) ?>
+
+    <?js if (data.fires && fires.length) { ?>
+        <h5>Fires:</h5>
+        <ul><?js fires.forEach(function(f) { ?>
+            <li><?js= self.linkto(f) ?></li>
+        <?js }); ?></ul>
+    <?js } ?>
+
+    <?js if (data.examples && examples.length) { ?>
+        <h5>Example<?js= examples.length > 1? 's':'' ?></h5>
+        <?js= this.partial('examples.tmpl', examples) ?>
+    <?js } ?>
+</div>

+ 131 - 0
utils/docs/template/tmpl/method.tmpl

@@ -0,0 +1,131 @@
+<?js
+var data = obj;
+var self = this;
+?>
+<?js if (data.kind !== 'module' && !data.hideconstructor) { ?>
+    <?js if (data.kind === 'class' && data.classdesc) { ?>
+    <h2>Constructor</h2>
+    <?js } ?>
+
+    <?js if (data.kind !== 'namespace') { ?>
+    <h4 class="name name-method" id="<?js= id ?>"><?js= data.attribs + (kind === 'class' ? 'new ' : '') +
+    name + (data.signature || '') ?>
+    <a href="#<?js= id ?>" class="link-anchor">#</a>
+    </h4>
+    <?js } ?>
+
+    <?js if (data.summary) { ?>
+    <p class="summary"><?js= summary ?></p>
+    <?js } ?>
+<?js } ?>
+
+<div class="method">
+    <?js if (data.kind !== 'module' && data.description && !data.hideconstructor) { ?>
+    <div class="description">
+        <?js= data.description ?>
+    </div>
+    <?js } ?>
+
+    <?js if (data.augments && data.alias && data.alias.indexOf('module:') === 0) { ?>
+        <h5>Extends:</h5>
+        <?js= self.partial('augments.tmpl', data) ?>
+    <?js } ?>
+
+    <?js if (kind === 'event' && data.type && data.type.names) {?>
+        <h5>Type:</h5>
+        <ul>
+            <li>
+                <?js= self.partial('type.tmpl', data.type.names) ?>
+            </li>
+        </ul>
+    <?js } ?>
+
+    <?js if (data['this']) { ?>
+        <h5>This:</h5>
+        <ul><li><?js= this.linkto(data['this'], data['this']) ?></li></ul>
+    <?js } ?>
+
+    <?js if (data.params && params.length && !data.hideconstructor) { ?>
+        <h5>Parameters:</h5>
+        <?js= this.partial('params.tmpl', params) ?>
+    <?js } ?>
+
+    <?js= this.partial('details.tmpl', data) ?>
+
+    <?js if (data.kind !== 'module' && data.requires && data.requires.length) { ?>
+    <h5>Requires:</h5>
+    <ul><?js data.requires.forEach(function(r) { ?>
+        <li><?js= self.linkto(r) ?></li>
+    <?js }); ?></ul>
+    <?js } ?>
+
+    <?js if (data.fires && fires.length) { ?>
+    <h5>Fires:</h5>
+    <ul><?js fires.forEach(function(f) { ?>
+        <li><?js= self.linkto(f) ?></li>
+    <?js }); ?></ul>
+    <?js } ?>
+
+    <?js if (data.listens && listens.length) { ?>
+    <h5>Listens to Events:</h5>
+    <ul><?js listens.forEach(function(f) { ?>
+        <li><?js= self.linkto(f) ?></li>
+    <?js }); ?></ul>
+    <?js } ?>
+
+    <?js if (data.listeners && listeners.length) { ?>
+    <h5>Listeners of This Event:</h5>
+    <ul><?js listeners.forEach(function(f) { ?>
+        <li><?js= self.linkto(f) ?></li>
+    <?js }); ?></ul>
+    <?js } ?>
+
+    <?js if (data.modifies && modifies.length) {?>
+    <h5>Modifies:</h5>
+    <?js if (modifies.length > 1) { ?><ul><?js
+        modifies.forEach(function(m) { ?>
+            <li><?js= self.partial('modifies.tmpl', m) ?></li>
+        <?js });
+    ?></ul><?js } else {
+        modifies.forEach(function(m) { ?>
+            <?js= self.partial('modifies.tmpl', m) ?>
+        <?js });
+    } } ?>
+
+    <?js if (data.exceptions && exceptions.length) { ?>
+    <h5>Throws:</h5>
+    <?js if (exceptions.length > 1) { ?><ul><?js
+        exceptions.forEach(function(r) { ?>
+            <li><?js= self.partial('exceptions.tmpl', r) ?></li>
+        <?js });
+    ?></ul><?js } else {
+        exceptions.forEach(function(r) { ?>
+            <?js= self.partial('exceptions.tmpl', r) ?>
+        <?js });
+    } } ?>
+
+    <?js if (data.returns && returns.length) { ?>
+    <h5>Returns:</h5>
+    <?js if (returns.length > 1) { ?><ul><?js
+        returns.forEach(function(r) { ?>
+            <li><?js= self.partial('returns.tmpl', r) ?></li>
+        <?js });
+    ?></ul><?js } else {
+        returns.forEach(function(r) { ?>
+            <?js= self.partial('returns.tmpl', r) ?>
+        <?js });
+    } } ?>
+
+    <?js if (data.yields && yields.length) { ?>
+    <h5>Yields:</h5>
+    <?js if (yields.length > 1) { ?><ul><?js
+        yields.forEach(function(r) { ?>
+            <li><?js= self.partial('returns.tmpl', r) ?></li>
+        <?js });
+    ?></ul><?js } else {
+        yields.forEach(function(r) { ?>
+            <?js= self.partial('returns.tmpl', r) ?>
+        <?js });
+    } } ?>
+
+</div>

+ 14 - 0
utils/docs/template/tmpl/modifies.tmpl

@@ -0,0 +1,14 @@
+<?js
+var data = obj || {};
+?>
+
+<?js if (data.type && data.type.names) {?>
+<dl>
+    <dt>
+        Type
+    </dt>
+    <dd>
+        <?js= this.partial('type.tmpl', data.type.names) ?>
+    </dd>
+</dl>
+<?js } ?>

+ 131 - 0
utils/docs/template/tmpl/params.tmpl

@@ -0,0 +1,131 @@
+<?js
+    var params = obj;
+
+    /* sort subparams under their parent params (like opts.classname) */
+    var parentParam = null;
+    params.forEach(function(param, i) {
+        var paramRegExp;
+
+        if (!param) {
+            return;
+        }
+
+        if (parentParam && parentParam.name && param.name) {
+            try {
+                paramRegExp = new RegExp('^(?:' + parentParam.name + '(?:\\[\\])*)\\.(.+)$');
+            }
+            catch (e) {
+                // there's probably a typo in the JSDoc comment that resulted in a weird
+                // parameter name
+                return;
+            }
+
+            if ( paramRegExp.test(param.name) ) {
+                param.name = RegExp.$1;
+                parentParam.subparams = parentParam.subparams || [];
+                parentParam.subparams.push(param);
+                params[i] = null;
+            }
+            else {
+                parentParam = param;
+            }
+        }
+        else {
+            parentParam = param;
+        }
+    });
+
+    /* determine if we need extra columns, "attributes" and "default" */
+    params.hasAttributes = false;
+    params.hasDefault = false;
+    params.hasName = false;
+
+    params.forEach(function(param) {
+        if (!param) { return; }
+
+        if (param.optional || param.nullable || param.variable) {
+            params.hasAttributes = true;
+        }
+
+        if (param.name) {
+            params.hasName = true;
+        }
+
+        if (typeof param.defaultvalue !== 'undefined') {
+            params.hasDefault = true;
+        }
+    });
+?>
+
+<table class="params">
+    <thead>
+    <tr>
+        <?js if (params.hasName) {?>
+        <th>Name</th>
+        <?js } ?>
+
+        <th>Type</th>
+
+        <?js if (params.hasAttributes) {?>
+        <th>Attributes</th>
+        <?js } ?>
+
+        <?js if (params.hasDefault) {?>
+        <th>Default</th>
+        <?js } ?>
+
+        <th class="last">Description</th>
+    </tr>
+    </thead>
+
+    <tbody>
+    <?js
+        var self = this;
+        params.forEach(function(param) {
+            if (!param) { return; }
+    ?>
+
+        <tr>
+            <?js if (params.hasName) {?>
+                <td class="name"><code><?js= param.name ?></code></td>
+            <?js } ?>
+
+            <td class="type">
+            <?js if (param.type && param.type.names) {?>
+                <?js= self.partial('type.tmpl', param.type.names) ?>
+            <?js } ?>
+            </td>
+
+            <?js if (params.hasAttributes) {?>
+                <td class="attributes">
+                <?js if (param.optional) { ?>
+                    &lt;optional><br>
+                <?js } ?>
+
+                <?js if (param.nullable) { ?>
+                    &lt;nullable><br>
+                <?js } ?>
+
+                <?js if (param.variable) { ?>
+                    &lt;repeatable><br>
+                <?js } ?>
+                </td>
+            <?js } ?>
+
+            <?js if (params.hasDefault) {?>
+                <td class="default">
+                <?js if (typeof param.defaultvalue !== 'undefined') { ?>
+                    <?js= self.htmlsafe(param.defaultvalue) ?>
+                <?js } ?>
+                </td>
+            <?js } ?>
+
+            <td class="description last"><?js= param.description ?><?js if (param.subparams) { ?>
+                <h6>Properties</h6>
+                <?js= self.partial('params.tmpl', param.subparams) ?>
+            <?js } ?></td>
+        </tr>
+
+    <?js }); ?>
+    </tbody>
+</table>

+ 108 - 0
utils/docs/template/tmpl/properties.tmpl

@@ -0,0 +1,108 @@
+<?js
+    var data = obj;
+    var props = data.subprops || data.properties;
+
+    /* sort subprops under their parent props (like opts.classname) */
+    var parentProp = null;
+    props.forEach(function(prop, i) {
+        if (!prop) { return; }
+        if ( parentProp && prop.name && prop.name.indexOf(parentProp.name + '.') === 0 ) {
+            prop.name = prop.name.substr(parentProp.name.length+1);
+            parentProp.subprops = parentProp.subprops || [];
+            parentProp.subprops.push(prop);
+            props[i] = null;
+        }
+        else {
+            parentProp = prop;
+        }
+    });
+
+    /* determine if we need extra columns, "attributes" and "default" */
+    props.hasAttributes = false;
+    props.hasDefault = false;
+    props.hasName = false;
+
+    props.forEach(function(prop) {
+        if (!prop) { return; }
+
+        if (prop.optional || prop.nullable) {
+            props.hasAttributes = true;
+        }
+
+        if (prop.name) {
+            props.hasName = true;
+        }
+
+        if (typeof prop.defaultvalue !== 'undefined' && !data.isEnum) {
+            props.hasDefault = true;
+        }
+    });
+?>
+
+<table class="props">
+    <thead>
+    <tr>
+        <?js if (props.hasName) {?>
+        <th>Name</th>
+        <?js } ?>
+
+        <th>Type</th>
+
+        <?js if (props.hasAttributes) {?>
+        <th>Attributes</th>
+        <?js } ?>
+
+        <?js if (props.hasDefault) {?>
+        <th>Default</th>
+        <?js } ?>
+
+        <th class="last">Description</th>
+    </tr>
+    </thead>
+
+    <tbody>
+    <?js
+        var self = this;
+        props.forEach(function(prop) {
+            if (!prop) { return; }
+    ?>
+
+        <tr>
+            <?js if (props.hasName) {?>
+                <td class="name"><code><?js= prop.name ?></code></td>
+            <?js } ?>
+
+            <td class="type">
+            <?js if (prop.type && prop.type.names) {?>
+                <?js= self.partial('type.tmpl', prop.type.names) ?>
+            <?js } ?>
+            </td>
+
+            <?js if (props.hasAttributes) {?>
+                <td class="attributes">
+                <?js if (prop.optional) { ?>
+                    &lt;optional><br>
+                <?js } ?>
+
+                <?js if (prop.nullable) { ?>
+                    &lt;nullable><br>
+                <?js } ?>
+                </td>
+            <?js } ?>
+
+            <?js if (props.hasDefault) {?>
+                <td class="default">
+                <?js if (typeof prop.defaultvalue !== 'undefined') { ?>
+                    <?js= self.htmlsafe(prop.defaultvalue) ?>
+                <?js } ?>
+                </td>
+            <?js } ?>
+
+            <td class="description last"><?js= prop.description ?><?js if (prop.subprops) { ?>
+                <h6>Properties</h6><?js= self.partial('properties.tmpl', prop) ?>
+            <?js } ?></td>
+        </tr>
+
+    <?js }); ?>
+    </tbody>
+</table>

+ 8 - 0
utils/docs/template/tmpl/returns.tmpl

@@ -0,0 +1,8 @@
+<?js
+var data = obj || {};
+if (data.description) {
+?>
+<div class="param-desc">
+    <?js= description ?>
+</div>
+<?js } ?>

+ 5 - 0
utils/docs/template/tmpl/search.tmpl

@@ -0,0 +1,5 @@
+<div id="search-content" style="display: none">
+   <h1>Search</h1>
+
+   <div id="search-result"></div>
+</div>

+ 8 - 0
utils/docs/template/tmpl/source.tmpl

@@ -0,0 +1,8 @@
+<?js
+    var data = obj;
+?>
+    <section>
+        <article>
+            <pre class="prettyprint source linenums"><code><?js= data.code ?></code></pre>
+        </article>
+    </section>

+ 7 - 0
utils/docs/template/tmpl/type.tmpl

@@ -0,0 +1,7 @@
+<?js
+    var data = obj;
+    var self = this;
+    data.forEach(function(name, i) { ?>
+<span class="param-type"><?js= self.linkto(name, self.htmlsafe(name)) ?></span>
+<?js if (i < data.length-1) { ?>|<?js } ?>
+<?js }); ?>

Some files were not shown because too many files changed in this diff

粤ICP备19079148号