Documenting JavaScript with Doxygen

by Dimitar Trendafilov October. 01, 12 0 Comment

As you already know (Coherent UI announcement) we are developing a large C++ and JavaScript project. We have documentation for both programming languages. The main requirements for the documentation are:

  • Application Programming Interface (API) references and general documentation such as quick start and detailed guides
  • cross references between the API references and the guides
  • accessible online and off line
  • easy markup language

There are a lot documentation tools for each language – DoxygenSandcastle for C++, YUIDocJSDuck for JavaScript. Our project API is primary in C++, so we choose Doxygen. It is great for C++projects, but it doesn’t support JavaScript. There are some scripts that solve this by converting JavaScript to C++ or Java. Unfortunately they do not support the modules pattern or have inconvenient syntax for the documentation. Our JavaScript API consists mostly of modules, so we wrote a simple doxygen filter for our documentation. A doxygen filter is a program that is invoked with the name of a file, and its output is used by doxygen to create the documentation for that file. To enable filters for specific file extension add

FILTER_PATTERNS =*.js=doxygen.js

in the doxygen configuration file. Lets say we want to document the following module:

/// @file Sync.js
/// @namespace Sync
/// Module for loading and storing data
 
var Sync = {
	/// @function Sync.load
	/// Loads an resource
	/// @param {String} id the GUID of the resource
	/// @param {Function} success callback to be executed with the data on suceess
	/// @param {Function} error callback to be executed with error description in case of failure
	/// Loads an resource
	load : function (id, success, error) {
	},
 
	/// @function Sync.store
	/// Store an resource
	/// @param {String} id the GUID of the resource
	/// @param {Object} data the resource
	/// @param {Function} success callback to be executed when the store operation has completed successfully
	/// @param {Function} error callback to be executed with error description in case of failure
	/// Store an resource
	store : function (id, data, success, error) {
	},
};

The filtered output looks like:

/// @file Sync.js
/// @namespace Sync
/// Module for loading and storing data
namespace Sync {
 
/// Loads an resource
/// @param id the GUID of the resource
/// @param success callback to be executed with the data on suceess
/// @param error callback to be executed with error description in case of failure
/// Loads an resource
undefined load(String id, Function success, Function error);
}
 
namespace Sync {
 
/// Store an resource
/// @param id the GUID of the resource
/// @param data the resource
/// @param success callback to be executed when the store operation has completed successfully
/// @param error callback to be executed with error description in case of failure
/// Store an resource
undefined store(String id, Object data, Function success, Function error);
}

A nice surprise is that when you want to link to Sync.load you can use `Sync.load`. The only annoying C++ artifacts in the JavaScript documentation are the “Sync namespace” and using “::” as resolution operator, but they can be fixed by a simple find / replace script. The doxygen.js filter is available at https://gist.github.com/3767879.

#!/usr/bin/env node
var fs = require('fs')
 
var functionName = /^s+///s+@functions+(.*)$/;
var type = /^(s+)///s+@params+{(w*)}s+(.+?)(s+.*)$/;
var param = /^(s+)///s+@params+(.+?)s/;
var resultType = /^(s+)///s+@returns+{(w+)}(s+.*)$/;
 
function Section()
{
	this.name = '';
	this.result = 'undefined';
	this.args = [];
	this.comments = [];
	this.namespaces = [];
}
 
Section.prototype.handle_function = function (line) {
	this.namespaces = line.match(functionName)[1].split('.') || [];
	this.name = this.namespaces.pop();
};
 
Section.prototype.handle_param = function (line) {
	var paramType = 'Object';
	var name = '';
	var m = line.match(type);
	var r = line;
	if (m) {
		paramType = m[2];
		name = m[3];
		r = m[1] + '/// @param ' + name + m[4];
	}
	else {
		m = line.match(param);
		name = m[2];
	}
	this.args.push({name: name, type: paramType});
	this.comments.push(r);
};
 
Section.prototype.handle_return = function (line) {
	this.result = 'undefined';
	var m = line.match(resultType);
	var r = line;
	if (m) {
		this.result = m[2];
		r = m[1] + '/// @return ' + m[3];
	}
	this.comments.push(r);
};
 
Section.prototype.Generate = function () {
	var doc = [];
 
	this.namespaces.forEach(function (namespace) {
		doc.push('namespace ' + namespace + ' {n');
	});
 
	this.comments.forEach(function (c) {
		doc.push(c);
	});
 
	var args = [];
 
	this.args.forEach(function (argument) {
		args.push(argument.type + ' ' + argument.name);
	});
 
	if (this.name) {
		doc.push(this.result + ' ' + this.name + '(' + args.join(', ') + ');');
	}
 
	this.namespaces.forEach(function (namespace) {
			doc.push('}n');
	});
	return doc.join('n');
};
 
Section.prototype.handle_line = function (line) {
	this.comments.push(line);
};
 
function writeLine(line) {
	process.stdout.write(line + 'n');
}
 
fs.readFile(process.argv[2], 'utf8', function (err, data) {
	var lines = data.split('n');
	var comment = /^s*////;
	var directive = /@(w+)s+(.*)$/;
	var inside = false;
	var section = new Section();
	lines.forEach(function(line) {
		if (line.match(comment)) {
			var d = line.match(directive);
			if (d) {
				var handle = Section.prototype['handle_' + d[1]] || Section.prototype.handle_line;
				handle.call(section, line);
			} else {
				section.handle_line(line);
			}
			inside = true;
		} else if (inside) {
			writeLine(section.Generate());
			inside = false;
			section = new Section();
		}
	});
});

Follow Dimitar on Twitter: @DimitarNT

Tags:
Social Shares

Related Articles

Leave a Comment