Documenting JavaScript with Doxygen
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 – Doxygen, Sandcastle for C++, YUIDoc, JSDuck 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
1 |
FILTER_PATTERNS =*.js=doxygen.js |
in the doxygen configuration file. Lets say we want to document the following module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// @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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/// @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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
#!/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