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.