tusk

Documentation generator tool for node.js libraries

version 0.0.0 by Remy Loubradou licensed under MIT


Features

  • use the README, LICENCE files
  • code documentation
  • tweet button
  • github buttons
  • SEO friendly (send pull request to improve it :) )
  • support google analytics
  • support custom stylesheets and scripts

Ideas of features

  • code coverage documentation
  • test suite documentation
  • code complexity documentation
  • use the CHANGELOG file
  • files in the example[s] folder are used as samples in the documentation
  • travis badge
  • gemnasium badge

Usage

$ tusk --help
Usage: tusk [--input=FOLDER] [options]

Options:
  --input, -i  use the files contained in FOLDER to generate the documentation  [default: "/home/lbdremy/workspace/nodejs/tusk"]
  --styles     use the FILE(s) as the css file of the site                      [default: "/home/lbdremy/workspace/nodejs/tusk/bin/../lib/assets/stylesheets/normalize.css,/home/lbdremy/workspace/nodejs/tusk/bin/../lib/assets/stylesheets/simplegrid.css,/home/lbdremy/workspace/nodejs/tusk/bin/../lib/assets/stylesheets/github-markdown.css"]
  --scripts    use the FILE(s) as the javascript script(s) of the site          [default: "/home/lbdremy/workspace/nodejs/tusk/bin/../lib/assets/scripts/script.js"]
  --help, -h   show the help                                                    [boolean]

Configuration file .tusk.json: (it should be in the root directory of your module)

{
    "google-analytics-id" : "UA-XXXXXXXXXXX",
    "twitter-user" : "username"
}

Code documentation

index.js

bin/tusk.js

tusk = require('./../'),

Module dependencies


var tusk = require('./../'),
	optimist = require('optimist');

// Default styles and scripts
var defaultStyles = __dirname + '/../lib/assets/stylesheets/normalize.css,'
    + __dirname + '/../lib/assets/stylesheets/simplegrid.css,'
    + __dirname  + '/../lib/assets/stylesheets/github-markdown.css';
var defaultScripts = __dirname + '/../lib/assets/scripts/script.js';

// Build the command line tool and parse the arguments
var argv = optimist
	.usage('Usage: $0 [--input=FOLDER] [options]')
	.describe('input','use the files contained in FOLDER to generate the documentation')
    .describe('styles','use the FILE(s) as the css file of the site')
    .describe('scripts','use the FILE(s) as the javascript script(s) of the site')
    .describe('help','show the help')
    .alias('i','input')
    .alias('o','output')
    .alias('h','help')
    .boolean('help')
    .default('input',process.cwd())
    .default('styles',defaultStyles)
    .default('scripts',defaultScripts)
    .argv;

// Show the help
if(argv.help){
    console.log(optimist.help());
    process.exit(0);
}

// Document
tusk.document(argv,function(err,page){
    if(err) throw err;
    console.log(page);
});

lib/tusk.js

htmlifiers = require('./htmlifiers/'),

Module dependencies


var htmlifiers = require('./htmlifiers/'),
	parsers = require('./parsers/'),
	documenters = require('./documenters/'),
	consolidate = require('consolidate'),
	step = require('step'),
	utils = require('./utils'),
	fs = require('fs');

exports.htmlifiers = htmlifiers

Expose htmlifiers htmlifiers are responsible for tranforming js, json, markdown data into html.


exports.htmlifiers = htmlifiers;

exports.parsers = parsers

Expose parsers parsers are responsible for extracting relevant informations from the noise into js, json, markdown format.


exports.parsers = parsers;

exports.documenters = documenters

Exports documenters documenters are responsible for finding, fetching and transforming the known into readable and meaningful information in our case for the web.


exports.documenters = documenters;

exports.document = document

Export the document method main job of tusk


exports.document = document;

document(parts,done)

Document the given parts


function document(parts,done){
	step(
		function work(){
			var self = this;
			documenters.showcase(parts.input,this.parallel());
			documenters.readme(parts.input,this.parallel());
			documenters.licence(parts.input,this.parallel());
			documenters.lib(parts.input,this.parallel());
			var cssCallback = this.parallel();
			utils.readFiles(parts.styles.split(','),function(err,files){
				if(err) return cssCallback(err);
				var css = utils.compressCSS(files.join('\n'));
				cssCallback(null,css);
			});
			var jsCallback = this.parallel();
			utils.readFiles(parts.scripts.split(','),function(err,files){
				if(err) return jsCallback(err);
				var js = utils.compressJS(files.join('\n'));
				jsCallback(null,js);
			});
			var dotTuskCallback = this.parallel();
			fs.readFile(parts.input + '/.tusk.json',function(err,file){
				if(err) return dotTuskCallback(err);
				dotTuskCallback(null,JSON.parse(file));
			});
			documenters.githubButtons(parts.input,this.parallel());
		},
		function finalize(err,showcase,readme,licence,lib,style,script,dotTusk,githubButtons){
			if(err) throw err;
			var content = [readme,lib,licence].reduce(function(content,section){
				return content + section;
			},'');
			content = documenters.anchors(content);
			var packagejson = require(parts.input + '/package.json');
			// Find keywords
			var keywords = '';
			if(packagejson.keywords){
				keywords = packagejson.keywords.join(',') + ',' + packagejson.name
			}
			var locals = {
				title : packagejson.name,
				description : packagejson.description,
				author : utils.findAuthor(packagejson),
				keywords : keywords,
				showcase : showcase,
				menu : documenters.menu(content),
				content : content,
				script : script,
				style : style,
				dotTusk : dotTusk,
				githubButtons : githubButtons,
				packagejson : packagejson
			};
			consolidate.jade(__dirname + '/views/index.jade',locals,this);
		},
		done
	);
}

Parameters:

  • parts {Array} -
  • done {Function} -

lib/utils.js

exports.compressJS(code)

Compress the given javascript code


exports.compressJS = function compressJS(code){
	var parser = uglifyjs.parser;
	var uglify = uglifyjs.uglify;
	var ast = parser.parse(code); // parse code and get the initial AST
	//ast = uglify.ast_mangle(ast); // get a new AST with mangled names
	//ast = uglify.ast_squeeze(ast); // get an AST with compression optimizations
	return uglify.gen_code(ast); // compressed code here
};

Parameters:

  • code {String} - javascript code

Returns:

  • {String} javascrip code commpressed

exports.compressCSS(code)

Compress the given css code


exports.compressCSS = function compressCSS(code){
	return sqwish.minify(code,false);
};

Parameters:

  • code {String} - css code

Returns:

  • {String} css code compressed

exports.readFiles(paths,callback,callback(err),callback(,files))

Read files


exports.readFiles = function readFiles(paths,callback){
	step(function read(){
		var group = this.group();
		paths.forEach(function(path){
			fs.readFile(path,'utf8',group());
		});
	},callback)
};

Parameters:

  • paths {Array<String>} - a list of file system paths
  • callback {Function} -
  • callback(err) {Error} -
  • callback(,files) {Array} - a list containing the contain of each file system paths previously given

exports.readAtLeastOneFile(paths,callback,callback(err),callback(,file))

Read at least one file in the given paths


exports.readAtLeastOneFile = function readAtLeastOneFile(paths,callback){
	step(
		function readFiles(){
			var group = this.group();
			paths.forEach(function(path){
				fs.readFile(path,'utf8',group());
			});
		},
		function cleanup(err,files){
			var goodFile;
			files.forEach(function(file){
				if(file && !goodFile) goodFile = file;
			});
			if(!goodFile) throw new Error('Cannot read any of the given files: ' + paths.join(','));
			return goodFile;
		},
		callback
	);
};

Parameters:

  • paths {Array<String>} -
  • callback {Function} -
  • callback(err) {Error} -
  • callback(,file) {Array} -

exports.statAtLeastOne(paths,callback,callback(err),callback(,file))

Give at least one file status for the given paths


exports.statAtLeastOne = function statAtLeastOne(paths,callback){
	step(
		function stats(){
			var group = this.group();
			paths.forEach(function(path){
				fs.stat(path,group());
			});
		},
		function cleanup(err,stats){
			var goodStat;
			stats.forEach(function(stat){
				if(stat && !goodStat) goodStat = stat;
			});
			if(!goodStat) throw new Error('Cannot stat any of the given files: ' + paths.join(','));
			return goodStat;
		},
		callback
	);
};

Parameters:

  • paths {Array<String>} -
  • callback {Function} -
  • callback(err) {Error} -
  • callback(,file) {Array} -

exports.findAuthor(packagejson)

Find the author in the structure like package.json


exports.findAuthor = function findAuthor(packagejson){
	var author = 'Anonymous';
	if(typeof packagejson.author === 'string'){
		author = packagejson.author.replace(/<.*>/,'').trim();
	}else if(typeof packagejson.author === 'object'){
		author = packagejson.author.name;
	}
	return author;
};

Parameters:

  • packagejson {Object} -

Returns:

  • {String} author

lib/parsers/index.js

javascript = require('./javascript')

Module dependencies


var javascript = require('./javascript');

exports.javascript = javascript

Expose all parsers


exports.javascript = javascript;

lib/parsers/javascript.js

dox = require('dox')

Module dependencies


var dox = require('dox');

exports.parse(js)

Parse comments in the given string of js.


exports.parse = function(js){
	var options = {
		raw : true
	};
	return dox.parseComments(js.toString(),options);
}

Parameters:

  • js {string|Buffer}

Returns:

  • {Array} comments

lib/htmlifiers/index.js

markdown = require('./markdown'),

Module dependencies


var markdown = require('./markdown'),
	javascript = require('./javascript');

exports.markdown = markdown

Expose all htmlifiers


exports.markdown = markdown;
exports.javascript = javascript;

lib/htmlifiers/markdown.js

marked = require('marked'),

Module dependencies


var marked = require('marked'),
	highlighter = require('highlight.js');

exports.htmlify(md)

HTMLify the markdown file represented by md


exports.htmlify = function htmlify(md){
	return marked(md.toString());
};

Parameters:

  • md {string|Buffer} -

Returns:

  • {string} html

lib/htmlifiers/javascript.js

javascript = require('./../parsers/javascript'),

Module dependencies


var javascript = require('./../parsers/javascript'),
	highlighter = require('highlight.js'),
	marked = require('marked'),
	fs = require('fs'),
	jade = require('jade');

template = fs.readFileSync(__dirname + '/templates/javascript.jade','utf-8')

Fetch and compile the template


var template = fs.readFileSync(__dirname + '/templates/javascript.jade','utf-8');
var options = {
	pretty : true
};
var render = jade.compile(template,options);

exports.htmlify(js)

HTMLify the javascript file represented by js


exports.htmlify = function htmlify(js){
	var comments = javascript.parse(js);
	comments.forEach(function(comment){
		comment.resume = {};
		comment.resume.name = '';
		comment.resume.code = '';
		if(comment.ctx && (comment.ctx.type === 'declaration' || comment.ctx.type === 'property')){
			comment.resume.name = comment.ctx.string + ' = ' + comment.ctx.value;
		}else if( comment.ctx && (comment.ctx.type === 'function' || comment.ctx.type === 'method')){
			var parameters = [];
			comment.tags.forEach(function(tag){
				if(tag.type === 'param' && !tag.name.match(/(\[\]|\(\)|\.)/)) parameters.push(tag.name);
			});
			var string = comment.ctx.string.replace('()', '(' + parameters.join(',') + ')');
			comment.resume.name = string;
		}
		comment.resume.description = marked(comment.description.full);
		if(comment.code) comment.resume.code = highlighter.highlight('javascript',comment.code).value;
		comment.resume.parameters = [];
		comment.resume.returns = '';
		comment.tags.forEach(function(tag){
			if(tag.type === 'param'){
				var type =  '{' + tag.types.join('|') + '}';
				var parameter = tag.name + ' ' +  type + ' ' + tag.description;
				comment.resume.parameters.push(parameter);
			}else if(tag.type === 'return' || tag.type === 'returns'){
				var type =  '{' + tag.types.join('|') + '}';
				comment.resume.returns = type + ' ' + tag.description;
			}
		});
	});
	return render({ comments : comments });
};

Parameters:

  • js {string|Buffer} -

Returns:

  • {string} html

lib/htmlifiers/pack.js

fs = require('fs')

Module dependencies


var fs = require('fs');
	jade = require('jade'),
	utils = require('./../utils');

template = fs.readFileSync(__dirname + '/templates/package-json.jade','utf-8')

Fetch and compile the template


var template = fs.readFileSync(__dirname + '/templates/package-json.jade','utf-8');
var options = {
	pretty : true
};
var render = jade.compile(template,options);

exports.htmlify(json)

HTMLify the package.json file


exports.htmlify = function htmlify(json){
	var pack = JSON.parse(json);
	pack.author = utils.findAuthor(pack);
	return render(pack);
};

Parameters:

  • json {string} -

Returns:

  • {string} html

lib/documenters/index.js

examples = require('./examples'),

Module dependencies


var examples = require('./examples'),
	lib = require('./lib'),
	showcase = require('./showcase'),
	readme = require('./readme'),
	githubButtons = require('./github-buttons'),
	licence = require('./licence'),
	menu = require('./menu'),
	anchors = require('./anchors');

exports.examples = examples

Expose all documenters


exports.examples = examples;
exports.lib = lib;
exports.showcase = showcase;
exports.readme = readme;
exports.githubButtons = githubButtons;
exports.licence = licence;
exports.menu = menu;
exports.anchors = anchors;

lib/documenters/readme.js

markdown = require('./../htmlifiers/markdown'),

Module dependencies


var markdown = require('./../htmlifiers/markdown'),
	utils = require('./../utils'),
	step = require('step'),
	cheerio = require('cheerio'),
	utils = require('./../utils');

module.exports(cwd,callback)

Build a html piece from the README.md of the module in cwd


module.exports = function(cwd,callback){
	var paths = [
		cwd + '/README.md',
		cwd + '/README.markdown',
		cwd + '/README'
	];
	step(
		function read(err){
			utils.readAtLeastOneFile(paths,this);
		},
		function htmlify(err,file){
			if(err) throw err;
			var readme = markdown.htmlify(file);
			var $ = cheerio.load(readme);
			// Remove h1 tag and p tags after h1
			var $p = $('h1 + p');
			while($p[0]){
				$p.remove();
				$p = $('h1 + p');
			}
			$('h1').remove();
			readme = '<section id="readme">' + $.html() + '</section>';
			return readme;
		},
		function cleanupLicence(err,readme){
			if(err) throw err;
			var self = this;
			// Remove licence section if LICENCE-like files present
			var paths = [
				cwd + '/LICENCE.md',
				cwd + '/LICENCE.markdown',
				cwd + '/LICENCE',
				cwd + '/LICENSE.md',
				cwd + '/LICENSE.markdown',
				cwd + '/LICENSE'
			];
			utils.statAtLeastOne(paths,function(err,stats){
				// the LICENCE-like doesn't exist not a problem for us
				if(err) return self(null,readme);
				// the LICENCE-like file exists
				var $ = cheerio.load(readme);
				var isLicence = function(text){
					text = text.toLowerCase().trim();
					return (text === 'licence' || text === 'license');
				};
				$('h2').each(function(i,element){
					var $element = $(element);
					var text = $element.text();
					if(isLicence(text)) $element.attr('id','licence');
				});
				var $p = $('h2#licence + p');
				while($p[0]){
					$p.remove();
					$p = $('h2#licence + p');
				}
				$('h2#licence').remove();
				self(null,$.html());
			});
		},
		callback
	);
};

Parameters:

  • cwd {string} - current working directory
  • callback {Function} -

lib/documenters/anchors.js

cheerio = require('cheerio')

Module dependencies


var cheerio = require('cheerio');

module.exports(content)

Add anchors to every h2,h3,h4 tags in the given HTML content


module.exports = function(content){
	var $ = cheerio.load(content);
	var tags = ['h2','h3','h4'];
	tags.forEach(function(tag){
		$(tag).each(function(index,element){
			$(element).attr('id',$(element).text().toLowerCase().trim().replace(/ /g,'-'));
		});
	});
	return $.html();
};

Parameters:

  • content {string} - HTML

Returns:

  • {string} same HTML with anchors on h2,h3,h4

lib/documenters/examples.js

lib/documenters/showcase.js

pack = require('./../htmlifiers/pack'),

Module dependencies


var pack = require('./../htmlifiers/pack'),
	fs = require('fs'),
	step = require('step');

module.exports(cwd,callback)

Build a html piece to showcase the module in cwd


module.exports = function(cwd,callback){
	var path = cwd + '/package.json';
	step(
		function search(){
			fs.stat(path,this)
		},
		function retrieve(err,stats){
			if(err){
				console.warn('the package.json cannot be found!')
				// Bypass the callback chains
				callback();
			}else{
				fs.readFile(path,'utf8',this);
			}
		},
		function htmlify(err,file){
			if(err) throw err;
			return pack.htmlify(file);
		},
		callback
	);
};

Parameters:

  • cwd {string} - current working directory
  • callback {Function} -

lib/documenters/github-buttons.js

module.exports(cwd,callback)

Build a html piece containing the unofficial github buttons thanks to the package.json


module.exports = function(cwd,callback){
	var path = cwd + '/package.json';
	step(
		function retrieve(){
			fs.readFile(path,'utf8',this);
		},
		function htmlify(err,file){
			if(err) throw err;
			var urlRepo = githubFromPackage(JSON.parse(file));
			if(urlRepo){
				var urlRepoParsed = url.parse(urlRepo);
				var pathnameSplit = urlRepoParsed.pathname.slice(1).split('/');
				var github = {
					repo : pathnameSplit[1],
					user : pathnameSplit[0]
				};
				var buttonTypes = ['watch','fork'];
				var html = '';
				buttonTypes.forEach(function(buttonType){
					html += '<iframe src="http://ghbtns.com/github-btn.html?user=' + github.user + '&repo=' + github.repo + '&type=' + buttonType + '&count=true"allowtransparency="true" frameborder="0" scrolling="0" width="78" height="20"></iframe>'
				});
				return this(null,html);
			}
			var err = new Error('Cannot figure out the github url of this project thanks to the package.json');
			this(err);
		},
		callback
	);
};

Parameters:

  • cwd {String} - current working directory
  • callback {Function} -

lib/documenters/licence.js

markdown = require('./../htmlifiers/markdown'),

Module dependencies


var markdown = require('./../htmlifiers/markdown'),
	utils = require('./../utils'),
	step = require('step');

module.exports(cwd,callback)

Build a html piece from the README.md of the module in cwd


module.exports = function(cwd,callback){
	var paths = [
		cwd + '/LICENCE.md',
		cwd + '/LICENCE.markdown',
		cwd + '/LICENCE',
		cwd + '/LICENSE.md',
		cwd + '/LICENSE.markdown',
		cwd + '/LICENSE'
	];
	step(
		function read(err){
			utils.readAtLeastOneFile(paths,this);
		},
		function htmlify(err,file){
			if(err) throw err;
			var licence = markdown.htmlify(file);
			return '<section id="licence"><h2>Licence</h2>' + licence + '</section>';
		},
		callback
	);
};

Parameters:

  • cwd {string} - current working directory
  • callback {Function} -

lib/documenters/menu.js

cheerio = require('cheerio')

Module dependencies


var cheerio = require('cheerio');

module.exports(content)

Build a HTML piece from the given HTML content


module.exports = function(content){
	var $ = cheerio.load(content);
	var menu = '<ul>';
	var struct = [];
	$('*').each(function(i,element){
		if(element.name === 'h2'){
			struct.push({ h2 : element, h3 : [] });
		}else if(element.name === 'h3'){
			struct[struct.length - 1].h3.push(element);
		}
	});
	struct.forEach(function(part){
		var text = $(part.h2).text();
		var href = text.toLowerCase().trim().replace(/ /g,'-');
		menu += '<li><a href="#' + href + '">' + text + '</a></li>';
		if(part.h3.length > 0){
			menu += '<ul>';
			part.h3.forEach(function(element){
				var text = $(element).text();
				var href = text.toLowerCase().trim().replace(/ /g,'-');
				menu += '<li><a href="#' + href + '">' + text + '</a></li>';
			});
			menu += '</ul>';
		}
	});
	menu += '</ul>';
	return menu;
};

Parameters:

  • content {string} - HTML

Returns:

  • {string} HTML menu of the given `content`

lib/documenters/lib.js

javascript = require('./../htmlifiers/javascript'),

Module dependencies


var javascript = require('./../htmlifiers/javascript'),
	fs = require('fs'),
	step = require('step'),
	readdirp = require('readdirp');

module.exports(cwd,callback)

Build a html piece from the lib folder of the module in cwd


module.exports = function(cwd,callback){
	step(
		function find(){
			var options = {
				root : cwd,
				fileFilter : '*.js',
				directoryFilter : [ '!test', '!node_modules', '!tests', '!.git', '!examples', '!example', '!deps']
			};
			var stream = readdirp(options);
			var entries = [];
			stream.on('data',function(entry){
				entries.push(entry);
			});
			stream.on('error',this);
			var self = this;
			stream.on('end',function(){
				self(null,entries);
			});
		},
		function retrieve(err,entries){
			if(err) throw err;
			var self = this;
			entries.forEach(function(entry){
				var onEnd = self.parallel();
				fs.readFile(entry.fullPath,'utf8',function(err,file){
					if(!err) entry.content = file;
					onEnd(err,entry);
				});
			});
		},
		function htmlify(err){
			if(err) throw err;
			var section = '<section id="lib"><h2>Code documentation</h2>';
			for(var i = 1; i < arguments.length; i++){
				var entry = arguments[i];
				var html = javascript.htmlify(entry.content);
				section += '<h3 alt="' + entry.path + '"">' + entry.path  + '</h3>' + html;
			}
			return section +  '</section>';
		},
		callback
	);
};

Parameters:

  • cwd {string} - current working directory
  • callback {Function} -

lib/assets/scripts/script.js

Licence

Copyright © 2013, Remy Loubradou

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders X be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.

Except as contained in this notice, the name of the Remy Loubradou shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the Remy Loubradou.