From 24d61cb2284b77f9671b9a111ff5d88e385d80fc Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 20 Oct 2017 13:01:37 +0200 Subject: [PATCH] Add template for document generation with 'jsdoc'. (#3497) * Add template for document generation with 'jsdoc'. In essence, it defines the subdirectory `docs` as a `jsdoc` template. Benefits: - allows the usage of partials, in order to DRY common parts of the html files. - makes available the jsdoc-comments, for addition into the documentation. - enables extraction of data from the source code. For example, the list of edge endpoints `['arrow', 'bar', 'circle']` can now be extracted from the source and inserted into the documentation on generation. In this initial version, the only file that has been changed is `docs/data/dataset.html`. In here, partials have been added to illustrate how common page elements can be DRY'd. The template has been set up in such a way, that resource files will be copied and that html files can pass through unchanged if no special template tags (``) are used. This allows for a gradual transition of the html files to templates. **Usage:** `gulp docs` - The result files are placed in subdirectory `gen/docs/`. **NOTE:** The release procedure will have to be adjusted by adding this gulp command. The docs-files will then have to be taken from `gen/docs`. * Edits to docs/README * Adjusted layout of README.md * Further edits to README.md * Removed pre tags again in README.md - don't work in code block * Linted the gulpfile * Added proof of concept for docs generation from source --- .gitignore | 1 + docs/README.md | 231 +++++++++++++++++++++++++++++++++++++++ docs/data/dataset.html | 87 +-------------- docs/network/index.html | 18 +++ docs/publish.js | 118 ++++++++++++++++++++ docs/tmpl/html-head.tmpl | 40 +++++++ docs/tmpl/navbar.tmpl | 49 +++++++++ gulpfile.js | 37 ++++++- jsdoc.json | 3 + lib/network/Network.js | 20 ++++ package.json | 1 + 11 files changed, 523 insertions(+), 82 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/publish.js create mode 100644 docs/tmpl/html-head.tmpl create mode 100644 docs/tmpl/navbar.tmpl create mode 100644 jsdoc.json diff --git a/.gitignore b/.gitignore index d9a5e809..b56f5ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ npm-debug.log # temporary files .*.sw[op] .commits.tmp +gen/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d64a0332 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,231 @@ +# Command line usage example + +``` +jsdoc -c jsdoc.json -r -t docs -d gen/docs lib +``` + +- `-c`: use this config file for `jsdoc` +- `-r`: Recurse into subdirectories of the specified source directories +- `-t`: Use template in path `docs` +- `-d`: Generated html files put in `gen/docs` +- Source files to parse are taken from directory `lib` + + +## Notes + +The template generation is set up so that: + + - Files ending in `.tmpl` are skipped + - All non-html files are plain copied + - html-files *can* contain `` tags, but this is not required + + +## Intention + +The `docs` directory is treated as a `jsdoc` template, in which the html-files are the template files. This allows for a gradual adaptation of the html-files to templates; unchanged html-files will pass through `jsdoc` unchanged. + +The added value of using `jsdoc` for documentation generation, is that the complete documentation information, as collected by `jsdoc` from the source, is available for usage. This way, it's possible to insert technical notes from the source code into the documentation. + +---- + +# Usage of and Notes on Source Code + +This section contains notes on the usage of `jsdoc` functionality, to aid with the handling of its generated data. + + +## Parameters of `publish()` + +### Parameter `taffyData` + + A table containing *all* data collected from the source code, related to jsdoc generation. See below for more info and example outputs. + +### Parameter `opt` + +Example of `opt` variable: + +```js +{ + "_":["../github/vis/lib/network/"], + "configure":"jsdoc.json", + "recurse":true, + "template":"/home/wim/projects/jsdoc/default", + "destination":"./out/", + "encoding":"utf8" +} +``` + +### Parameter `tutorial` + +This does not appear to be of use for the generation of `vis.js` documentation. + +Example of `tutorial` variable: + +```js +{ + "longname":"", + "name":"", + "title":"", + "content":"", + "parent":null, + "children":[], + "_tutorials":{} +} +``` + +## Global variable `env` + +This contains addition info for the current execution of `jsdoc`. Example of `env` variable: + +```js +{ + "run":{"start":"2017-09-16T05:06:45.621Z","finish":null}, + "args":["-c","jsdoc.json","-r","-t","default","../github/vis/lib/network/"], + "conf":{ + "plugins":["/usr/lib/node_modules/jsdoc/plugins/markdown.js"], + "recurseDepth":10, + "source":{"includePattern":".+\\.js(doc|x)?$","excludePattern":""}, + "sourceType":"module", + "tags":{"allowUnknownTags":true,"dictionaries":["jsdoc","closure"]}, + "templates":{"monospaceLinks":false,"cleverLinks":false} + }, + "dirname":"/usr/lib/node_modules/jsdoc", + "pwd":"/home/wim/projects/jsdoc", + "opts":{ <> }, + "sourceFiles":[ <> ], + "version":{"number":"3.5.4","revision":"Fri, 04 Aug 2017 22:05:27 GMT"} +} +``` + + +## taffyData + +This is a parameter to `publish()`. It's a table containing *all* data collected from the source code, related to jsdoc generation. + +I can't find any way to return a list of fields for the data items in the taffyDB docs, therefore below there are examples of items, for better understanding of usage. + +Example usage: + +```js + var data = taffyData; + var tmp = data().filter({name:'Label'}).get(); +``` + +Returns an array with all items with `name === 'Label'`. Example output of one of these items, for a class: + +*In these examples, block comment endings are redacted to ' * /'* + +```js +{ + "comment":"/**\n * A Label to be used for Nodes or Edges.\n * /", + "meta":{ + "range":[3770,41303], + "filename":"Label.js", + "lineno":167, + "columnno":0, + "path":"/home/wim/projects/github/vis/lib/network/modules/components/shared", + "code":{ + "id":"astnode100065034", + "name":"Label", + "type":"ClassDeclaration", + "paramnames":["body","options","edgelabel"] + } + }, + "classdesc":" +A Label to be used for Nodes or Edges. + +", + "name":"Label", + "longname":"Label", + "kind":"class", + "scope":"global", + "params":[ + {"type":{"names":["Object"]},"name":"body"}, + {"type":{"names":["Object"]},"name":"options"}, + {"type":{"names":["boolean"]},"optional":true,"defaultvalue":false,"name":"edgelabel"} + ], + "___id":"T000002R005289", + "___s":true +} +``` + +Example of item for an instance method: + +```js + var tmp = data().filter({name:'_drawText'}).get(); +``` + +Full output returned: + +```js +[{ + "comment":"/**\n *\n * @param {CanvasRenderingContext2D} ctx\n * @param {boolean} selected\n * @param {boolean} hover\n * @param {number} x\n * @param {number} y\n * @param {string} [baseline='middle']\n * @private\n * /", + "meta":{ + "range":[20060,22269], + "filename":"Label.js", + "lineno":652, + "columnno":2, + "path":"/home/wim/projects/github/vis/lib/network/modules/components/shared", + "code":{ + "id":"astnode100066427", + "name":"Label#_drawText", + "type":"MethodDefinition", + "paramnames":["ctx","selected","hover","x","y","baseline"] + }, + "vars":{"":null} + }, + "params":[ + {"type":{"names":["CanvasRenderingContext2D"]},"name":"ctx"}, + {"type":{"names":["boolean"]},"name":"selected"}, + {"type":{"names":["boolean"]},"name":"hover"}, + {"type":{"names":["number"]},"name":"x"}, + {"type":{"names":["number"]},"name":"y"}, + {"type":{"names":["string"]},"optional":true,"defaultvalue":"'middle'","name":"baseline"} + ], + "access":"private", + "name":"_drawText", + "longname":"Label#_drawText", + "kind":"function", + "memberof":"Label", + "scope":"instance", + "___id":"T000002R005388", + "___s":true +}] +``` + +## `jsdoc` template rendering + +See `function createRenderer(fromDir, data)` in code for usage. + +There are two calls for rendering templates: + + - `var html = renderer.render(inFile, docData);` + - `var html = renderer.partial(inFile, docData);` + +The difference is that `render()` will use a default layout template, if present, which will encapsulate all html. This can be set by: + +```js + renderer.layout = 'path/to/default/layout.tmpl'; +``` + +Parameter `docData` is a hash which is used to pass parameters into a template. The standard way of using this appear to be: + +``` + +``` + +But it also appear to be possible to use the elements of docData directly: + +```js +var docData = { + myTitle: 'Hello, pussycat!' +}; +``` + +Within the template: + +``` + +``` diff --git a/docs/data/dataset.html b/docs/data/dataset.html index 4c8a958c..b78fb2fe 100644 --- a/docs/data/dataset.html +++ b/docs/data/dataset.html @@ -1,86 +1,11 @@ - - - - - - - - - DataSet - vis.js - A dynamic, browser based visualization library. - - - - - - - - - - - - - - - - - - - - - + + - - - - +

DataSet

diff --git a/docs/network/index.html b/docs/network/index.html index 28199c4e..23ce1cfb 100644 --- a/docs/network/index.html +++ b/docs/network/index.html @@ -1,3 +1,6 @@ + @@ -609,6 +612,21 @@ var locales = { The options object is explained in full below. + + + + + This comes from the source! + + + Returns: + + + + + findNode( String/Number nodeId) diff --git a/docs/publish.js b/docs/publish.js new file mode 100644 index 00000000..f500e366 --- /dev/null +++ b/docs/publish.js @@ -0,0 +1,118 @@ +/*============================================================================== + Demo template, showing how documentation can be generated for `vis.js`. + + ------------------------------------------------------------------------------ + ## Notes on `jsdoc` code + + // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness + // doesn't try to hand them out later + indexUrl = helper.getUniqueFilename('index'); + // don't call registerLink() on this one! 'index' is also a valid longname + + globalUrl = helper.getUniqueFilename('global'); + helper.registerLink('global', globalUrl); + + ============================================================================== */ +'use strict'; +//var taffy = require('taffydb').taffy; // not really required here, left for reference + +// Internal modules of `jsdoc` are available here. +// This is not the complete list, there may be more useful stuff in jsdoc +// For all modules scan in: '/usr/lib/node_modules/jsdoc/lib/jsdoc/' (or similar on your system) +var fs = require('jsdoc/fs'); +var path = require('jsdoc/path'); +var template = require('jsdoc/template'); + + +/** + * Set up the template rendering engine. + */ +function createRenderer(fromDir, data) { + var renderer = new template.Template(fromDir); // Param is the template source directory. + // All template files are relative to this directory! + /** + * Example helper method + * + * This can be called from within a template as follows: + * + * ``` + * + * ... + * + * ``` + * + * / + renderer.helper = function(val) { + return 'this is a helper! ' + val; + }; + */ + + /** + * Retrieves jsdoc info for the passed instance method. + */ + renderer.getComment = function(methodName) { + var tmp = data().filter({longname: methodName}).get()[0]; + //console.log(JSON.stringify(tmp)); + + // Some restructuring, to adapt it to the docs layout + // This needs some work to make it handle 0 and > 1 parameters + var param = tmp.params[0]; + var prototype = tmp.name + '(' + param.type.names.join('|') + ' ' + param.name + ')'; + var returns = tmp.returns[0].type.names; + + return { + prototype: prototype, + returns: returns, + description: tmp.description + } + }; + + return renderer; +} + + +/** + Entry point for the template. + + This is called from `jsdoc` during execution + + @param {TAFFY} taffyData See . + @param {object} opts + @param {Tutorial} tutorials + */ +exports.publish = function(taffyData, opts, tutorials) { + //console.log(JSON.stringify(opts, null, 2)); + + var fromDir = path.resolve(opts.template); + var toDir = path.join(opts.destination); + var renderer = createRenderer(fromDir, taffyData); + + var docFiles = fs.ls(fromDir, 3); + docFiles.forEach(function(fileName) { + // Template filenames need to be relative to template source dir + var relName = path.relative(fromDir, fileName); + var outFile = path.join(toDir, relName); + + if (/publish.js$/.test(fileName)) return; // Skip self + if (/README.md$/.test(fileName)) return; // Skip own README + if (/\.tmpl$/.test(fileName)) return; // Skip .tmpl files; these are used as partials only + + if (!/\.html$/.test(fileName)) { + // Just plain copy over non-html files + var tmpDir = fs.toDir(outFile); + fs.mkPath(tmpDir); + fs.copyFileSync(fileName, tmpDir); + return; + } + + // Render html files as templates + //console.log(relName); + var html = renderer.partial(relName, {}); + fs.mkPath(fs.toDir(outFile)); + fs.writeFileSync(outFile, html, 'utf8'); + }); + + //console.log(JSON.stringify(env, null, 2)); +}; diff --git a/docs/tmpl/html-head.tmpl b/docs/tmpl/html-head.tmpl new file mode 100644 index 00000000..7fd3adb8 --- /dev/null +++ b/docs/tmpl/html-head.tmpl @@ -0,0 +1,40 @@ + + + + + + + + + + <?js= title ?> + + + + + + + + + + + + + + + + + + + + + +' purposely missing here, so that more stuff can be added to header */ ?> \ No newline at end of file diff --git a/docs/tmpl/navbar.tmpl b/docs/tmpl/navbar.tmpl new file mode 100644 index 00000000..b1f8da08 --- /dev/null +++ b/docs/tmpl/navbar.tmpl @@ -0,0 +1,49 @@ + + + + \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 5e87f423..4076d81c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,6 +10,7 @@ var webpack = require('webpack'); var uglify = require('uglify-js'); var rimraf = require('rimraf'); var argv = require('yargs').argv; +var child_exec = require('child_process').exec; var ENTRY = './index.js'; var HEADER = './lib/header.js'; @@ -29,7 +30,12 @@ var INDIVIDUAL_CSS_BUNDLES = [ {entry: ['./lib/shared/**/*.css', './lib/network/**/*.css'], filename: 'vis-network.min.css'} ]; -// generate banner with today's date and correct version + +/** + * Generate banner with today's date and correct version + * + * @returns {string} banner text + */ function createBanner() { var today = gutil.date(new Date(), 'yyyy-mm-dd'); // today, formatted as yyyy-mm-dd var version = require('./package.json').version; @@ -39,6 +45,7 @@ function createBanner() { .replace('@@version', version); } + var bannerPlugin = new webpack.BannerPlugin({ banner: createBanner(), entryOnly: true, @@ -92,6 +99,12 @@ var uglifyConfig = { // create a single instance of the compiler to allow caching var compiler = webpack(webpackConfig); +/** + * Callback for handling errors for a compiler run + * + * @param {object} err + * @param {objects} stats + */ function handleCompilerCallback (err, stats) { if (err) { gutil.log(err.toString()); @@ -232,5 +245,27 @@ gulp.task('lint', function () { }); +// Generate the documentation files +gulp.task('docs', function(cb) { + var targetDir = 'gen/docs'; + + // Not sure if this is the best way to handle 'cb'; at least it works. + var hasError = false; + var onError = function(error) { + if (error !== undefined && error !== null) { + console.error('Error while running task: ' + error); + hasError = true; + cb(); + } + } + + rimraf(__dirname + '/' + targetDir, onError); // Clean up previous generation + + if (!hasError) { + var params = '-c ./jsdoc.json -r -t docs -d ' + targetDir; + child_exec('node ./node_modules/jsdoc/jsdoc.js ' + params + ' lib', undefined, cb); + } +}); + // The default task (called when you run `gulp`) gulp.task('default', ['clean', 'bundle', 'minify']); diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 00000000..df0756c6 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,3 @@ +{ + "plugins": ["plugins/markdown"] +} diff --git a/lib/network/Network.js b/lib/network/Network.js index 486a8a21..b45baf0a 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -470,7 +470,27 @@ Network.prototype.isActive = function () { Network.prototype.setSize = function() {return this.canvas.setSize.apply(this.canvas,arguments);}; Network.prototype.canvasToDOM = function() {return this.canvas.canvasToDOM.apply(this.canvas,arguments);}; Network.prototype.DOMtoCanvas = function() {return this.canvas.DOMtoCanvas.apply(this.canvas,arguments);}; + + +/** + * Nodes can be in clusters. Clusters can also be in clusters. This function returns and array of + * nodeIds showing where the node is. + * + * If any nodeId in the chain, especially the first passed in as a parameter, is not present in + * the current nodes list, an empty array is returned. + * + * Example: + * cluster 'A' contains cluster 'B', + * cluster 'B' contains cluster 'C', + * cluster 'C' contains node 'fred'. + * `jsnetwork.clustering.findNode('fred')` will return `['A','B','C','fred']`. + * + * @param {string|number} nodeId + * @returns {Array} + */ Network.prototype.findNode = function() {return this.clustering.findNode.apply(this.clustering,arguments);}; + + Network.prototype.isCluster = function() {return this.clustering.isCluster.apply(this.clustering,arguments);}; Network.prototype.openCluster = function() {return this.clustering.openCluster.apply(this.clustering,arguments);}; Network.prototype.cluster = function() {return this.clustering.cluster.apply(this.clustering,arguments);}; diff --git a/package.json b/package.json index be72b0af..867be4a8 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "gulp-eslint": "^4.0.0", "gulp-rename": "^1.2.2", "gulp-util": "^3.0.8", + "jsdoc": "^3.5.5", "jsdom": "9.12.0", "jsdom-global": "^2.1.1", "merge-stream": "^1.0.1",