moxygen
Version:
Doxygen XML to Markdown documentation converter
498 lines (437 loc) • 16.8 kB
JavaScript
/**
* Original work Copyright (c) 2016 Philippe FERDINAND
* Modified work Copyright (c) 2016 Kam Low
*
* @license MIT
**/
'use strict';
var fs = require('fs');
var log = require('winston');
var path = require('path');
var xml2js = require('xml2js');
var Compound = require('./compound');
var helpers = require('./helpers');
var markdown = require('./markdown');
function toMarkdown(element, context) {
var s = '';
context = context || [];
switch (typeof element) {
case 'string':
s = element;
break;
case 'object':
if (Array.isArray(element)) {
element.forEach(function (value, key) {
s += toMarkdown(value, context);
});
}
else {
// opening the element
switch (element['#name']) {
case 'ref': return s + markdown.link(toMarkdown(element.$$), module.exports.resolveRef(element.$.refid), true);
case '__text__': s = element._; break;
case 'emphasis': s = '*'; break;
case 'bold': s = '**'; break;
case 'parametername':
case 'computeroutput': s = '`'; break;
case 'parameterlist':
if (element.$.kind == 'exception') {
s = '\n#### Exceptions\n'
}
else {
s = '\n#### Parameters\n'
}
break;
case 'parameteritem': s = '* '; break;
case 'programlisting': s = '\n```cpp\n'; break;
case 'itemizedlist': s = '\n\n'; break;
case 'listitem': s = '* '; break;
case 'sp': s = ' '; break;
case 'heading': s = '## '; break;
case 'xrefsect': s += '\n> '; break;
case 'simplesect':
if (element.$.kind == 'attention') {
s = '> ';
}
else if (element.$.kind == 'return') {
s = '\n#### Returns\n'
}
else if (element.$.kind == 'see') {
s = '\n**See also**: '
}
else {
console.assert(element.$.kind + ' not supported.');
}
break;
case 'formula':
s = trim(element._);
if (s.startsWith('$') && s.endsWith('$')) return s;
if (s.startsWith('\\[') && s.endsWith('\\]'))
s = trim(s.substring(2, s.length - 2));
return '\n$$\n' + s + '\n$$\n';
case 'xreftitle':
case 'entry':
case 'row':
case 'ulink':
case 'codeline':
case 'highlight':
case 'table':
case 'para':
case 'parameterdescription':
case 'parameternamelist':
case 'xrefdescription':
case 'verbatim':
case 'hruler':
case undefined:
break;
default:
console.error(false, element['#name'] + ': not yet supported.');
}
// recurse on children elements
if (element.$$) {
s += toMarkdown(element.$$, context);
}
// closing the element
switch (element['#name']) {
case 'parameterlist':
case 'para': s += '\n\n'; break;
case 'emphasis': s += '*'; break;
case 'bold': s += '**'; break;
case 'parameteritem': s += '\n'; break;
case "computeroutput": s += '`'; break;
case 'parametername': s += '` '; break;
case 'entry': s = markdown.escape.cell(s) + '|'; break;
case 'programlisting': s += '```\n'; break;
case 'codeline': s += '\n'; break;
case 'ulink': s = markdown.link(s, element.$.url); break;
case 'itemizedlist': s += '\n'; break;
case 'listitem': s += '\n'; break;
case 'entry': s = ' | '; break;
case 'xreftitle': s += ': '; break;
case 'row':
s = '\n' + markdown.escape.row(s);
if (element.$$ && element.$$[0].$.thead == "yes") {
element.$$.forEach(function (th, i) {
s += (i ? ' | ' : '\n') + '---------';
});
}
break;
}
}
break;
default:
console.assert(false);
}
return s;
}
function trim(text) {
return text.replace(/^[\s\t\r\n]+|[\s\t\r\n]+$/g, '');
}
function copy(dest, property, def) {
dest[property] = trim(toMarkdown(def[property]));
}
function summary(dest, def) {
// set from briefdescription or first paragraph of detaileddescription
var summary = trim(toMarkdown(def['briefdescription']));
if (!summary) {
summary = trim(toMarkdown(def['detaileddescription']));
if (summary) {
var firstSentence = summary.split('\n', 1)[0]; //.split('. ').first;
if (firstSentence)
summary = firstSentence;
}
}
dest['summary'] = summary;
}
module.exports = {
// All references indexed by refid
references: {},
// The root compound
root: new Compound(),
parseMembers: function (compound, props, membersdef) {
// copy all properties
Object.keys(props).forEach(function(prop) {
compound[prop] = props[prop];
});
this.references[compound.refid] = compound;
if (membersdef) {
membersdef.forEach(function (memberdef) {
var member = { name: memberdef.name[0] };
compound.members.push(member);
Object.keys(memberdef.$).forEach(function(prop) {
member[prop] = memberdef.$[prop];
});
this.references[member.refid] = member;
}.bind(this));
}
},
parseMember: function (member, section, memberdef) {
log.verbose('Processing member ' + member.kind + ' ' + member.name);
member.section = section;
copy(member, 'briefdescription', memberdef);
copy(member, 'detaileddescription', memberdef);
summary(member, memberdef);
var m = [];
switch (member.kind) {
case 'signal':
case 'slot':
m = m.concat(['{', member.kind, '} ']);
case 'function':
m = m.concat(memberdef.$.prot, ' '); // public, private, ...
if (memberdef.templateparamlist) {
m.push('template<');
if (memberdef.templateparamlist.length > 0 && memberdef.templateparamlist.param) {
memberdef.templateparamlist[0].param.forEach(function (param, argn) {
m = m.concat(argn == 0 ? [] : ',');
m = m.concat([toMarkdown(param.type)]);
m = m.concat(param.declname ? [' ', toMarkdown(param.declname)] : []);
});
}
m.push('> \n');
}
m = m.concat(memberdef.$.inline == 'yes' ? ['inline', ' '] : []);
m = m.concat(memberdef.$.static == 'yes' ? ['static', ' '] : []);
m = m.concat(memberdef.$.virt == 'virtual' ? ['virtual', ' '] : []);
m = m.concat(toMarkdown(memberdef.type), ' ');
m = m.concat(memberdef.$.explicit == 'yes' ? ['explicit', ' '] : []);
// m = m.concat(memberdef.name[0]._);
m = m.concat(markdown.link(member.name, '#' + member.refid, true));
m = m.concat('(');
if (memberdef.param) {
memberdef.param.forEach(function (param, argn) {
m = m.concat(argn == 0 ? [] : ',');
m = m.concat([toMarkdown(param.type)]);
m = m.concat(param.declname ? [' ', toMarkdown(param.declname)] : []);
});
}
m = m.concat(')');
m = m.concat(memberdef.$['const'] == 'yes' ? [' ', 'const'] : []);
m = m.concat(memberdef.argsstring[0]._.match(/noexcept$/) ? ' noexcept' : '');
m = m.concat(memberdef.argsstring[0]._.match(/=\s*delete$/) ? ' = delete' : '');
m = m.concat(memberdef.argsstring[0]._.match(/=\s*default/) ? ' = default' : '');
break;
case 'variable':
m = m.concat(memberdef.$.prot, ' '); // public, private, ...
m = m.concat(memberdef.$.static == 'yes' ? ['static', ' '] : []);
m = m.concat(memberdef.$.mutable == 'yes' ? ['mutable', ' '] : []);
m = m.concat(toMarkdown(memberdef.type), ' ');
// m = m.concat(memberdef.name[0]._);
m = m.concat(markdown.link(member.name, '#' + member.refid, true));
break;
case 'property':
m = m.concat(['{', member.kind, '} ']);
m = m.concat(toMarkdown(memberdef.type), ' ');
// m = m.concat(memberdef.name[0]._);
m = m.concat(markdown.link(member.name, '#' + member.refid, true));
break;
case 'enum':
member.enumvalue = [];
if (memberdef.enumvalue) {
memberdef.enumvalue.forEach(function (param, argn) {
var enumvalue = {}
copy(enumvalue, 'name', param);
copy(enumvalue, 'briefdescription', param);
copy(enumvalue, 'detaileddescription', param);
summary(enumvalue, param);
member.enumvalue.push(enumvalue);
});
}
// m.push(member.kind + ' ' + member.name);
m = m.concat([member.kind, ' ', markdown.link(member.name, '#' + member.refid, true)]);
break;
default:
// m.push(member.kind + ' ' + member.name);
m = m.concat([member.kind, ' ', markdown.link(member.name, '#' + member.refid, true)]);
break;
}
member.proto = helpers.inline(m);
},
assignToNamespace: function (compound, child) {
if (compound.name != child.namespace)
console.assert('namespace mismatch: ', compound.name, '!=', child.namespace);
// namespaces take ownership of the child compound
if (child.parent)
delete child.parent.compounds[child.name];
compound.compounds[child.name] = child;
child.parent = compound;
},
assignNamespaceToGroup: function (compound, child) {
// add the namespace to the group
compound.compounds[child.name] = child;
// remove namespace clildren from direct group children
Object.keys(child.compounds).forEach(function(id) {
delete compound.compounds[id];
});
},
assignClassToGroup: function (compound, child) {
// add the namespace to the group
// if the child already belongs to a child namespace it will be removed
// on the call to `assignNamespaceToGroup`
compound.compounds[child.name] = child;
// add a groupid and reference to the compound and all it's members
child.groupid = compound.id;
child.groupname = compound.name;
child.members.forEach(function (member) {
member.groupid = compound.id;
member.groupname = compound.name;
});
},
parseCompound: function (compound, compounddef) {
log.verbose('Processing compound ' + compound.name);
copy(compound, 'briefdescription', compounddef);
copy(compound, 'detaileddescription', compounddef);
summary(compound, compounddef);
if (compounddef.sectiondef) {
compounddef.sectiondef.forEach(function (section) {
// switch (section.$['kind']) {
// case 'define':
// case 'enum':
// case 'friend':
// case 'public-attrib':
// case 'public-func':
// case 'protected-attrib':
// case 'protected-func':
// case 'private-attrib':
// case 'private-func':
if (section.memberdef) {
section.memberdef.forEach(function (memberdef) {
var member = this.references[memberdef.$.id];
if (compound.kind == 'group') {
member.groupid = compound.id;
}
else if (compound.kind == 'file') {
// add free members defined inside files in the default
// namespace to the root compound
this.root.members.push(member);
}
this.parseMember(member, section.$['kind'], memberdef);
}.bind(this));
}
// break;
//
// default:
// console.assert(true);
// }
}.bind(this));
}
compound.proto = helpers.inline([compound.kind, ' ', markdown.link(compound.name, '#' + compound.refid, true)]);
return;
},
preprocessCompound: function (compound, compounddef) {
log.verbose('Preprocessing compound ' + compound.name);
Object.keys(compounddef.$).forEach(function(prop) {
compound[prop] = compounddef.$[prop];
});
compound.fullname = compounddef.compoundname[0]._;
if (compounddef.basecompoundref) {
compounddef.basecompoundref.forEach(function (basecompoundref) {
compound.basecompoundref.push({
prot: basecompoundref.$.prot,
name: basecompoundref._,
});
});
}
// kind specific parsing
switch (compound.kind) {
case 'class':
case 'struct':
case 'union':
case 'typedef':
// set namespace reference
var nsp = compound.name.split('::');
compound.namespace = nsp.splice(0, nsp.length - 1).join('::');
break;
case 'file':
// NOTE: to handle free functions in the default namespace we would
// parse add all contained members to the root compound.
break;
case 'namespace':
case 'group':
// handle innerclass for groups and namespaces
if (compounddef.innerclass) {
compounddef.innerclass.forEach(function (innerclassdef) {
if (compound.kind == 'namespace') {
// log.verbose('Assign ' + innerclassdef.$.refid + ' to namespace ' + compound.name);
if (this.references[innerclassdef.$.refid])
this.assignToNamespace(compound, this.references[innerclassdef.$.refid]);
}
else if (compound.kind == 'group') {
// log.verbose('Assign ' + innerclassdef.$.refid + ' to group ' + compound.name);
if (this.references[innerclassdef.$.refid])
this.assignClassToGroup(compound, this.references[innerclassdef.$.refid]);
}
}.bind(this));
}
// handle innernamespace for groups and namespaces
if (compounddef.innernamespace) {
compound.innernamespaces = [];
compounddef.innernamespace.forEach(function (namespacedef) {
if (compound.kind == 'group') {
// log.verbose('Assign namespace ' + namespacedef.$.refid + ' to group ' + compound.name);
this.assignNamespaceToGroup(compound, this.references[namespacedef.$.refid]);
}
}.bind(this));
}
break;
default:
console.assert(true);
}
return;
},
parseIndex: function (root, index, options, callback) {
var compounds = [], defs = [];
var processTogether = function(compound, def) {
this.preprocessCompound(compound, def);
defs.push([compound, def]);
if (compounds.length == defs.length) {
defs.forEach(function(item) {
this.resolveRef = helpers.resolveRef(options, item[0], this.references);
this.parseCompound(item[0], item[1]);
}.bind(this));
callback(null, this.root); // TODO: return errors properly
}
}.bind(this);
index.forEach(function (element) {
var compound = root.find(element.name[0], true);
this.parseMembers(compound, element.$, element.member);
if (compound.kind !== 'file') { // && compound.kind !== 'file'
compounds.push(compound);
}
}.bind(this));
compounds.forEach(function (compound) {
var doxygen;
var xmlParser = new xml2js.Parser({
explicitChildren: true,
preserveChildrenOrder: true,
charsAsChildren: true
});
log.verbose('Parsing ' + path.join(options.directory, compound.refid + '.xml'));
doxygen = fs.readFileSync(path.join(options.directory, compound.refid + '.xml'), 'utf8');
xmlParser.parseString(doxygen, function (err, data) {
if (err) {
log.verbose('warning - parse error for file' , path.join(options.directory, compound.refid + '.xml'))
return;
}
processTogether(compound, data.doxygen.compounddef[0]);
}.bind(this));
}.bind(this));
},
loadIndex: function (options, callback) {
log.verbose('Parsing ' + path.join(options.directory, 'index.xml'));
fs.readFile(path.join(options.directory, 'index.xml'), 'utf8', function(err, data) {
if (err) {
callback('Failed to load Doxygen XML: ' + err);
return;
}
var xmlParser = new xml2js.Parser();
xmlParser.parseString(data, function (err, result) {
if (err) {
callback('Failed to parse Doxygen XML: ' + err);
return;
}
this.root.kind = 'index';
this.parseIndex(this.root, result.doxygenindex.compound, options, callback);
}.bind(this));
}.bind(this));
}
};