From 65a19d1779c6d04287634128c54e4ad16fcca8e8 Mon Sep 17 00:00:00 2001 From: Mihai Tudor Panu Date: Wed, 20 May 2015 10:38:19 -0700 Subject: [PATCH] jsdoc: scripts for generating node.js documentation Signed-off-by: Mihai Tudor Panu --- doxy/node/docgen.js | 60 +++ doxy/node/generators/jsdoc/conf.json | 7 + doxy/node/generators/jsdoc/generator.js | 88 ++++ doxy/node/generators/ternjs/generator.js | 110 +++++ doxy/node/generators/yuidoc/conf.json | 8 + doxy/node/generators/yuidoc/generator.js | 117 +++++ doxy/node/grammars/xml.peg | 45 ++ doxy/node/tolower.js | 125 +++++ doxy/node/xml2js.js | 575 +++++++++++++++++++++++ 9 files changed, 1135 insertions(+) create mode 100644 doxy/node/docgen.js create mode 100644 doxy/node/generators/jsdoc/conf.json create mode 100644 doxy/node/generators/jsdoc/generator.js create mode 100644 doxy/node/generators/ternjs/generator.js create mode 100644 doxy/node/generators/yuidoc/conf.json create mode 100644 doxy/node/generators/yuidoc/generator.js create mode 100644 doxy/node/grammars/xml.peg create mode 100644 doxy/node/tolower.js create mode 100644 doxy/node/xml2js.js diff --git a/doxy/node/docgen.js b/doxy/node/docgen.js new file mode 100644 index 00000000..4a358914 --- /dev/null +++ b/doxy/node/docgen.js @@ -0,0 +1,60 @@ +/* + * Author: Heidi Pan + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var xml2js = require('./xml2js') + , fs = require('fs') + , Promise = require('bluebird') + , opts = require('commander') + , _ = require('lodash') + , mkdirp = require('mkdirp'); + + +// parse command line arguments +_.extend(opts, { addOptions: function(module) { return module.addOptions(opts); } }); +opts + .option('-m, --module [module]', 'module name for which to build documentation', 'mraa') + .option('-f, --formats [formats]', 'format for js comments', 'jsdoc,yuidoc,ternjs') + .option('-o, --outdir [directory]', 'top directory to build documentation', __dirname + '/jsdoc') + .addOptions(xml2js) + .parse(process.argv); + + +// use promise-style programming rather than spaghetti callbacks +Promise.promisifyAll(fs); + +// TODO: create directory structure if doesn't exist +var formats = opts.formats.split(','); +formats.forEach(function(format){ + mkdirp('jsdoc/' + format + '/' + opts.module); +}); + +// main +xml2js.parse().then(function(specjs) { + Promise.all(_.map(formats, function(format) { + var generateDocs = require(__dirname + '/generators/' + format + '/generator'); + var outFile = opts.outdir + '/' + format + '/' + specjs.MODULE + '/doc.js'; + return fs.writeFileAsync(outFile, generateDocs(specjs)); + })); +}); diff --git a/doxy/node/generators/jsdoc/conf.json b/doxy/node/generators/jsdoc/conf.json new file mode 100644 index 00000000..a2f37cca --- /dev/null +++ b/doxy/node/generators/jsdoc/conf.json @@ -0,0 +1,7 @@ +{ + "templates": { + "default": { + "outputSourceFiles": false + } + } +} \ No newline at end of file diff --git a/doxy/node/generators/jsdoc/generator.js b/doxy/node/generators/jsdoc/generator.js new file mode 100644 index 00000000..81cce67a --- /dev/null +++ b/doxy/node/generators/jsdoc/generator.js @@ -0,0 +1,88 @@ +/* + * Author: Heidi Pan + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var _ = require('lodash'); + + +// generate JSDoc-style documentation +function generateDocs(specjs) { + var docs = GENERATE_MODULE(specjs.MODULE); + docs = _.reduce(specjs.METHODS, function(memo, methodSpec, methodName) { + return memo += GENERATE_METHOD(methodName, methodSpec); + }, docs); + docs = _.reduce(specjs.ENUMS, function(memo, enumSpec, enumName) { + return memo += GENERATE_ENUM(enumName, enumSpec); + }, docs); + docs = _.reduce(specjs.CLASSES, function(memo, classSpec, parentClass) { + return _.reduce(classSpec.methods, function(memo, methodSpec, methodName) { + return memo += GENERATE_METHOD(methodName, methodSpec, parentClass); + }, memo); + }, docs); + return docs; +} + + +// comment wrapper around entire spec +function GENERATE_DOC(text) { + return '/**\n' + text + ' */\n'; +} + + +// generate module spec +function GENERATE_MODULE(module) { + return GENERATE_DOC('@module ' + module + '\n'); +} + + +// generate method spec with parent module/class +function GENERATE_METHOD(name, spec, parent) { + return GENERATE_DOC(spec.description + '\n' + + '@method ' + name + '\n' + + '@instance\n' + + (parent ? ('@memberof ' + parent + '\n') : '') + + _.reduce(spec.params, function(memo, paramSpec, paramName) { + return '@param {' + paramSpec.type + '} ' + paramName + ' ' + paramSpec.description + '\n'; + }, '') + + ( !_.isEmpty(spec.return) ? ('@return {' + spec.return.type + '} ' + spec.return.description + '\n') : '')); +} + + +// generate enum spec +function GENERATE_ENUM(name, spec) { + return GENERATE_DOC(spec.description + '\n\n' + + '@var ' + name + '\n' + + '@type Enum(' + spec.type + ')\n' + + '@instance\n'); +} + + +// TODO +// generate link spec +function GENERATE_LINK(text) { + return '{@link ' + text + '}'; +} + + +module.exports = generateDocs; diff --git a/doxy/node/generators/ternjs/generator.js b/doxy/node/generators/ternjs/generator.js new file mode 100644 index 00000000..7bbc0a00 --- /dev/null +++ b/doxy/node/generators/ternjs/generator.js @@ -0,0 +1,110 @@ +/* + * Author: Heidi Pan + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var _ = require('lodash'); + + +// generate json for ternjs input +function generateDocs(specjs) { + var docs = GENERATE_MODULE(specjs.MODULE); + GENERATE_TYPE = (function(enums) { + return function(type) { + return (_.contains(enums, type) ? ('Enum ' + type) : type); + } + })(_.keys(specjs.ENUMS_BY_GROUP)); + _.each(specjs.ENUMS, function(enumSpec, enumName) { + _.extend(docs[specjs.MODULE], GENERATE_ENUM(enumName, enumSpec)); + }); + _.each(specjs.METHODS, function(methodSpec, methodName) { + _.extend(docs[specjs.MODULE], GENERATE_METHOD(methodName, methodSpec)); + }); + _.each(specjs.CLASSES, function(classSpec, parentClass) { + var constructor = classSpec.methods[parentClass]; + _.extend(docs[specjs.MODULE], GENERATE_METHOD(parentClass, constructor ? constructor : { params: {}, return: {}, description: '' } )); + _.each(classSpec.enums, function(enumSpec, enumName) { + _.extend(docs[specjs.MODULE][parentClass], GENERATE_ENUM(enumName, enumSpec)); + }); + docs[specjs.MODULE][parentClass].prototype = {}; + _.each(_.omit(classSpec.methods, parentClass), function(methodSpec, methodName) { + _.extend(docs[specjs.MODULE][parentClass].prototype, GENERATE_METHOD(methodName, methodSpec)); + }); + _.each(classSpec.variables, function(variableSpec, variableName) { + _.extend(docs[specjs.MODULE][parentClass].prototype, GENERATE_VARIABLE(variableName, variableSpec)); + }); + }); + return JSON.stringify(docs, null, 2); +} + + +// generate module spec +function GENERATE_MODULE(module) { + var docs = { '!name': module + 'library' }; + docs[module] = {}; + return docs; +} + + +// generate method spec +function GENERATE_METHOD(name, spec) { + var doc = {}; + doc[name] = { + '!type': 'fn(' + GENERATE_PARAMS(spec.params) + ')' + GENERATE_RETURN(spec.return), + '!doc': spec.description + } + return doc; +} + + +// generate parameter signatures for method +function GENERATE_PARAMS(spec) { + return _.map(spec, function(paramSpec, paramName) { + return paramName + ': ' + paramSpec.type; + }).join(', '); +} + + +// generate return signature for method +function GENERATE_RETURN(spec) { + return (_.isEmpty(spec) ? '' : (' -> ' + spec.type)); +} + + +// generate enum spec +function GENERATE_ENUM(name, spec) { + var doc = {}; + doc[name] = 'Enum ' + spec.type ; + return doc; +} + + +// generate variable spec +function GENERATE_VARIABLE(name, spec) { + var doc = {}; + doc[name]= spec.type ; + return doc; +} + + +module.exports = generateDocs; diff --git a/doxy/node/generators/yuidoc/conf.json b/doxy/node/generators/yuidoc/conf.json new file mode 100644 index 00000000..002d0ab8 --- /dev/null +++ b/doxy/node/generators/yuidoc/conf.json @@ -0,0 +1,8 @@ +{ + "name": "UPM", + "description": "The UPM API: High Level Sensor Library for Intel IoT Devices Using MRAA", + "logo": "http://upload.wikimedia.org/wikipedia/commons/8/8c/Transparent.png", + "options": { + "outdir": "./html/node" + } +} \ No newline at end of file diff --git a/doxy/node/generators/yuidoc/generator.js b/doxy/node/generators/yuidoc/generator.js new file mode 100644 index 00000000..7217c050 --- /dev/null +++ b/doxy/node/generators/yuidoc/generator.js @@ -0,0 +1,117 @@ +/* + * Author: Heidi Pan + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var _ = require('lodash'); + + +// generate YuiDocs-style documentation +function generateDocs(specjs) { + var docs = GENERATE_MODULE(specjs.MODULE); + GENERATE_TYPE = (function(enums) { + return function(type) { + return (_.contains(enums, type) ? ('Enum ' + type) : type); + } + })(_.keys(specjs.ENUMS_BY_GROUP)); + docs = _.reduce(specjs.METHODS, function(memo, methodSpec, methodName) { + return memo += GENERATE_METHOD(methodName, methodSpec); + }, docs); + docs = _.reduce(specjs.ENUMS, function(memo, enumSpec, enumName) { + return memo += GENERATE_ENUM(enumName, enumSpec); + }, docs); + docs = _.reduce(specjs.CLASSES, function(memo, classSpec, parentClass) { + return memo + + GENERATE_CLASS(parentClass, classSpec.description) + + _.reduce(classSpec.methods, function(memo, methodSpec, methodName) { + return memo += GENERATE_METHOD(methodName, methodSpec, parentClass); + }, '') + + _.reduce(classSpec.variables, function(memo, variableSpec, variableName) { + return memo += GENERATE_VAR(variableName, variableSpec, parentClass); + }, '') + + _.reduce(classSpec.enums, function(memo, enumSpec, enumName) { + return memo += GENERATE_ENUM(enumName, enumSpec, parentClass); + }, ''); + }, docs); + return docs; +} + + + +// comment wrapper around entire spec +function GENERATE_DOC(text) { + return '/**\n' + text + ' */\n'; +} + + +// generate module spec +function GENERATE_MODULE(module) { + return GENERATE_DOC('@module ' + module + '\n'); +} + + +// generate class spec +function GENERATE_CLASS(name, description) { + return GENERATE_DOC(description + '\n' + + '@class ' + name + '\n'); +} + + +// generate method spec with parent module/class +function GENERATE_METHOD(name, spec, parent) { + return GENERATE_DOC(spec.description + '\n' + + '@method ' + name + '\n' + + (parent ? ('@for ' + parent + '\n') : '@for common\n') + + _.reduce(spec.params, function(memo, paramSpec, paramName) { + return memo + '@param {' + GENERATE_TYPE(paramSpec.type) + '} ' + paramName + ' ' + paramSpec.description + '\n'; + }, '') + + ( !_.isEmpty(spec.return) ? ('@return {' + GENERATE_TYPE(spec.return.type) + '} ' + spec.return.description + '\n') : '')); +} + + +// generate enum spec +function GENERATE_ENUM(name, spec, parent) { + return GENERATE_DOC(spec.description + '\n' + + '@property ' + name + '\n' + + '@type Enum ' + spec.type + '\n' + + '@for ' + (parent ? parent : 'common') + '\n'); +} + + +// generate variable specs +function GENERATE_VAR(name, spec, parent) { + return GENERATE_DOC(spec.description + '\n' + + '@property ' + name + '\n' + + '@type ' + spec.type + '\n' + + '@for ' + parent + '\n'); +} + + +// TODO +// generate link spec +function GENERATE_LINK(text) { + return '{{#crossLink "' + text + '"}}{{/crossLink}}'; +} + + +module.exports = generateDocs; diff --git a/doxy/node/grammars/xml.peg b/doxy/node/grammars/xml.peg new file mode 100644 index 00000000..bca96d51 --- /dev/null +++ b/doxy/node/grammars/xml.peg @@ -0,0 +1,45 @@ +document + = _ ignore* _ "" body:elements _ "" _ { return body; } + + +elements + = element* + +element + = _ "<" startTag:id _ attr:attr* _ ">" _ children:elements _ "" _ { + if (startTag != endTag) { + throw new Error("Expected but found."); + } + return {name: startTag, attr: attr, children: children } + } + / "<" tag:id _ attr:attr* _ "/>" _ { + return {name: tag, attr: attr } + } + / _ text:text _ { return text } + +ignore + = "" { return } + +attr + = name:id _ "=" _ value:string { return { name:name, value:value } } + +string + = '"' '"' _ { return ""; } + / "'" "'" _ { return ""; } + / '"' text:quoted '"' _ { return text; } + / "'" text:quoted "'" _ { return text; } + +quoted + = chars:[^<>'" \t\n\r]+ { return chars.join(""); } + +text + = chars:[^<> \t\n\r]+ { return chars.join(""); } + +id + = chars:[^<>/'"=? \t\n\r]+ { return chars.join(""); } + +_ "whitespace" + = whitespace* + +whitespace + = [ \t\n\r] diff --git a/doxy/node/tolower.js b/doxy/node/tolower.js new file mode 100644 index 00000000..c9c24217 --- /dev/null +++ b/doxy/node/tolower.js @@ -0,0 +1,125 @@ +/* + * Author: Dina M Suehiro + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var opts = require('commander'), // for command line args + fs = require('fs'), // for file system access + path = require('path'); // for file path parsing + +// parse command line arguments +opts + .option('-i, --inputdir [directory]', 'product documents directory', __dirname + '/docs/yuidoc/upm') + .parse(process.argv); + +// Set to true for console output +var debug = true; + +// Global arrays tracking the files that have been renamed +var originalFiles = []; +var renamedFiles = []; + +// Filter to get html files from different directories +var rootFiles = getHtmlFilenames(opts.inputdir); +var classesFiles = getHtmlFilenames(opts.inputdir + "/classes"); +var modulesFiles = getHtmlFilenames(opts.inputdir + "/modules"); + +// Rename files in the classes directory to have lower-cased file names. +renameFiles(classesFiles); + +classesFiles = getHtmlFilenames(opts.inputdir + "/classes"); + +// Go through the html files and update links to reflect the file names that we changed. +renameLinks(rootFiles); +renameLinks(classesFiles); +renameLinks(modulesFiles); + +// Helper function that returns paths to the html files in the specified directory +function getHtmlFilenames (directory) +{ + return fs.readdirSync(directory).map(function (file) { + return path.join(directory, file); + }).filter(function (file) { + return fs.statSync(file).isFile(); + }).filter(function (file) { + return path.extname(file).toLowerCase() == ".html"; + }); +} + +// Goes through the files and renames them to be lower-cased and tracks them the +// renamed files in the originalFiles[] and renamedFiles[] arrays. +function renameFiles(files) +{ + files.forEach(function (file) + { + var originalName = path.basename(file); + var newFileName = originalName.toLowerCase(); + var directory = path.dirname(file); + if (originalName != newFileName) + { + fs.renameSync(file, directory + "/" + newFileName); //, function(err) + + if (debug) + console.log('Renamed: %s --> %s', originalName, newFileName); + + originalFiles.push(originalName); + renamedFiles.push(newFileName); + } + }); +} + +// Helper function goes through the specified files and does a file/replace of the +// originalFiles to the renamedFiles so that the .html links match what has been renamed. +function renameLinks (files) +{ + if (originalFiles.length <= 0) + { + if (debug) + console.log("No links to rename."); + return; + } + + files.forEach(function (file) + { + // Read the file + data = fs.readFileSync(file, 'ascii'); + + // Find/replace the file names that were renamed + for (var i = 0; i < originalFiles.length; i++) + { + var findString = '/' + originalFiles[i] + '\"'; + var replaceString = '/' + renamedFiles[i] + '\"'; + + data = data.replace(findString, replaceString); + } + + // Write back + fs.writeFile(file, data, 'ascii', function (err) { + if (err) + throw err; + }); + + if (debug) + console.log('Renamed links in: %s', file); + }); +} diff --git a/doxy/node/xml2js.js b/doxy/node/xml2js.js new file mode 100644 index 00000000..8c6102ce --- /dev/null +++ b/doxy/node/xml2js.js @@ -0,0 +1,575 @@ +/* + * Author: Heidi Pan + * Copyright (c) 2015 Intel Corporation. + * + * 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 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. + */ + +// dependencies +var peg = require('pegjs') + , fs = require('fs') + , path = require('path') + , Promise = require('bluebird') + , _ = require('lodash') + , util = require('util'); + + +// use promise-style programming rather than spaghetti callbacks +Promise.promisifyAll(fs); + + +var xml2js = { + + // js-format specs + // MODULES: + // ENUMS: { + // : { + // type: , + // description: + // }, ... + // } + // ENUMS_BY_GROUP: { + // : { + // description: + // members: [ , ... ] + // }, ... + // } + // METHODS: { + // : { + // description: , + // params: { + // : { + // type: , + // description: + // }, ... + // }, + // return: { + // type: , + // description: + // } + // }, ... + // } + // CLASSES: { + // : { + // description: , + // methods: { ... }, + // variables: { + // : { + // type: , + // description: + // } + // }, + // enums: { ... }, + // enums_by_group: { ... } + // }, ... + // } + MODULE: '', + ENUMS: {}, + ENUMS_BY_GROUP: {}, + METHODS: {}, + CLASSES: {}, + + + // c -> js type mapping + TYPEMAPS: { + '^(const)?\\s*(unsigned|signed)?\\s*(int|short|long|float|double|size_t|u?int\\d{1,2}_t)?$': 'Number', + '^bool$': 'Boolean', + '^(const)?\\s*(unsigned|signed)?\\s*(char|char\\s*\\*|std::string)$': 'String' // TODO: verify that String mappings work + }, + + + // add command line options for this module + addOptions: function(opts) { + xml2js.opts = opts; + return opts + .option('-i, --inputdir [directory]', 'directory for xml files', __dirname + '/xml/mraa') + .option('-c, --custom [file]', 'json for customizations', __dirname + '/custom.json') + .option('-s, --strict', 'leave out methods/variables if unknown type') + }, + + + // parse doxygen xml -> js-format specs + // TODO: figure out whether we need to document any protected methods/variables + parse: function() { + var XML_GRAMMAR_SPEC = 'grammars/xml.peg'; + var NAMESPACE_SPEC = xml2js.opts.inputdir + '/namespace' + xml2js.opts.module + '.xml'; + var CLASS_SPEC = function(c) { return xml2js.opts.inputdir + '/' + c + '.xml'; } + var TYPES_SPEC = xml2js.opts.inputdir + '/types_8h.xml'; + xml2js.MODULE = xml2js.opts.module; + return Promise.join(createXmlParser(XML_GRAMMAR_SPEC), + fs.readFileAsync(NAMESPACE_SPEC, 'utf8'), + fs.existsSync(TYPES_SPEC) ? fs.readFileAsync(TYPES_SPEC, 'utf8') : Promise.resolve(null), + function(xmlparser, xml, xml_types) { + if (xml_types != null) { + _.extend(xml2js.ENUMS, getEnums(xmlparser.parse(xml_types)[0], false)); + _.extend(xml2js.ENUMS_BY_GROUP, getEnums(xmlparser.parse(xml_types)[0], true)); + } + var spec_c = xmlparser.parse(xml)[0]; + _.extend(xml2js.ENUMS, getEnums(spec_c, false)); + _.extend(xml2js.ENUMS_BY_GROUP, getEnums(spec_c, true)); + _.extend(xml2js.METHODS, getMethods(spec_c)); + return Promise.all(_.map(getClasses(spec_c), function(c) { + return fs.readFileAsync(CLASS_SPEC(c), 'utf8').then(function(xml) { + try { + var spec_c = xmlparser.parse(xml)[0]; + var className = getClassName(spec_c); + xml2js.CLASSES[className] = { + description: getClassDescription(spec_c), + enums: getEnums(spec_c, false, className), + enums_by_group: getEnums(spec_c, true, className), + variables: getVariables(spec_c, className), + methods: getMethods(spec_c, className), + }; + } catch(e) { + console.log(e.toString() + ': ' + c + ' was not parsed correctly.'); + } + }); + })).then(function() { + if (fs.existsSync(xml2js.opts.custom)) { + return fs.readFileAsync(xml2js.opts.custom, 'utf8').then(function(custom) { + try { + customizeMethods(JSON.parse(custom)); + } catch(e) { + console.log('invalid custom.json, ignored. ' + e.toString()); + } + }); + } else { + console.log((xml2js.opts.custom == __dirname + '/custom.json') ? 'No customizations given.' : 'Error: No such customization file exists: ' + xml2js.opts.custom); + } + }).then(function() { + validateMethods(); + validateVars(); + return _.pick(xml2js, 'MODULE', 'ENUMS', 'ENUMS_BY_GROUP', 'METHODS', 'CLASSES'); + }); + }); + } + +}; + + +// create an xml parser +function createXmlParser(XML_GRAMMAR_SPEC) { + return fs.readFileAsync(XML_GRAMMAR_SPEC, 'utf8').then(function(xmlgrammar) { + return peg.buildParser(xmlgrammar); + }); +} + + +// override autogenerated methods with custom configuration +function customizeMethods(custom) { + _.each(custom, function(classMethods, className) { + _.extend(xml2js.CLASSES[className].methods, _.pick(classMethods, function(methodSpec, methodName) { + return isValidMethodSpec(methodSpec, className + '.' + methodName); + })); + }); +} + +// make sure methods have valid types, otherwise warn (& don't include if strict) +function validateMethods() { + xml2js.METHODS = _.pick(xml2js.METHODS, function(methodSpec, methodName) { + hasValidTypes(methodSpec, methodName); + }); + _.each(xml2js.CLASSES, function(classSpec, className) { + var valid = _.pick(classSpec.methods, function(methodSpec, methodName) { + return hasValidTypes(methodSpec, className + '.' + methodName, className); + }); + if (xml2js.opts.strict) { + xml2js.CLASSES[className].methods = valid; + } + }); +} + + +// make sure variables have valid types, otherwise warn (& don't include if strict) +function validateVars() { + _.each(xml2js.CLASSES, function(classSpec, className) { + var valid = _.pick(classSpec.variables, function(varSpec, varName) { + return ofValidType(varSpec, className + '.' + varName, className); + }); + if (xml2js.opts.strict) { + xml2js.CLASSES[className].variables = valid; + } + }); +} + + +// verify that the json spec is well formatted +function isValidMethodSpec(methodSpec, methodName) { + var valid = true; + var printIgnoredMethodOnce = _.once(function() { console.log(methodName + ' from ' + path.basename(xml2js.opts.custom) + ' is omitted from JS documentation.'); }); + function checkRule(rule, errMsg) { + if (!rule) { + printIgnoredMethodOnce(); + console.log(' ' + errMsg); + valid = false; + } + } + checkRule(_.has(methodSpec, 'description'), 'no description given'); + checkRule(_.has(methodSpec, 'params'), 'no params given (specify "params": {} for no params)'); + _.each(methodSpec.params, function(paramSpec, paramName) { + checkRule(_.has(paramSpec, 'type'), 'no type given for param ' + paramName); + checkRule(_.has(paramSpec, 'description'), 'no description given for param ' + paramName); + }); + checkRule(_.has(methodSpec, 'return'), 'no return given (specify "return": {} for no return value)'); + checkRule(_.has(methodSpec.return, 'type'), 'no type given for return value'); + checkRule(_.has(methodSpec.return, 'description'), 'no description given for return value'); + return valid; +} + + +// get enum specifications +function getEnums(spec_c, bygroup, parent) { + var spec_js = {}; + var enumGroups = _.find(getChildren(spec_c, 'sectiondef'), function(section) { + var kind = getAttr(section, 'kind'); + return ((kind == 'enum') || (kind == 'public-type')); + }); + if (enumGroups) { + _.each(enumGroups.children, function(enumGroup) { + var enumGroupName = getText(getChild(enumGroup, 'name'), 'name'); + var enumGroupDescription = getText(getChild(enumGroup, 'detaileddescription'), 'description'); + var enumGroupVals = getChildren(enumGroup, 'enumvalue'); + if (bygroup) { + spec_js[enumGroupName] = { + description: enumGroupDescription, + members: [] + }; + } + _.each(enumGroupVals, function(e) { + // TODO: get prefix as option + var enumName = getText(getChild(e, 'name'), 'name').replace(/^MRAA_/, ''); + var enumDescription = getText(getChild(e, 'detaileddescription'), 'description'); + if (!bygroup) { + spec_js[enumName] = { + type: enumGroupName, + description: enumDescription + }; + } else { + spec_js[enumGroupName].members.push(enumName); + } + }); + }); + } + return spec_js; +} + + +// get the classes (xml file names) for the given module +function getClasses(spec_c) { + return _.map(getChildren(spec_c, 'innerclass'), function(innerclass) { + return getAttr(innerclass, 'refid'); + }); +} + + +// get the description of the class +function getClassDescription(spec_c) { + return getText(getChild(spec_c, 'detaileddescription'), 'description'); +} + + +function hasParams(paramsSpec) { + return !(_.isEmpty(paramsSpec) || + ((_.size(paramsSpec) == 1) && getText(getChild(paramsSpec[0], 'type')) == 'void')); +} + + +// get method specifications for top-level module or a given class +// TODO: overloaded functions +// TODO: functions w/ invalid parameter(s)/return +function getMethods(spec_c, parent) { + var spec_js = {}; + var methods = _.find(getChildren(spec_c, 'sectiondef'), function(section) { + var kind = getAttr(section, 'kind'); + return ((kind == 'public-func') || (kind == 'func')); + }); + if (methods) { + _.each(methods.children, function(method) { + var methodName = getText(getChild(method, 'name'), 'name'); + if (methodName[0] != '~') { // filter out destructors + try { + var description = getChild(method, 'detaileddescription'); + var methodDescription = getText(description, 'description'); + var paramsSpec = getChildren(method, 'param'); + var params = {}; + if (hasParams(paramsSpec)) { + params = getParams(paramsSpec, getParamsDetails(description), (parent ? (parent + '.') : '') + methodName); + } + var returnSpec = getChild(method, 'type'); + var retval = {}; + if (!_.isEmpty(returnSpec)) { + retval = getReturn(returnSpec, getReturnDetails(description)); + } + spec_js[methodName] = { + description: methodDescription, + params: params, + return: retval + }; + } catch(e) { + console.log((parent ? (parent + '.') : '') + methodName + ' is omitted from JS documentation.'); + console.log(' ' + e.toString()); + } + } + }); + } + return spec_js; +} + + +// get variable specifications for a class +function getVariables(spec_c, parent) { + var spec_js = {}; + var vars = _.find(getChildren(spec_c, 'sectiondef'), function(section) { + var kind = getAttr(section, 'kind'); + return (kind == 'public-attrib'); + }); + if (vars) { + _.each(_.filter(vars.children, function(variable) { + return (getAttr(variable, 'kind') == 'variable'); + }), function(variable) { + var varName = getText(getChild(variable, 'name'), 'name'); + var varType = getType(getText(getChild(variable, 'type'))); + var varDescription = getText(getChild(variable, 'detaileddescription')); + spec_js[varName] = { + type: varType, + description: varDescription + } + }); + } + return spec_js; +} + + +// get return value specs of a method +function getReturn(spec_c, details) { + var retType = getType(getText(spec_c, 'type')); + var retDescription = (details ? getText(details, 'description') : ''); + return ((retType == 'void') ? {} : { + type: retType, + description: retDescription + }); +} + + +// get paramater specs of a method +function getParams(spec_c, details, method) { + var spec_js = {}; + _.each(spec_c, function(param) { + try { + var paramType = getType(getText(getChild(param, 'type'), 'type')); + var paramName = getText(getChild(param, 'declname'), 'name'); + spec_js[paramName] = { type: paramType }; + } catch(e) { + if (paramType == '...') { + spec_js['arguments'] = { type: paramType }; + } else { + throw e; + } + } + }); + _.each(details, function(param) { + var getParamName = function(p) { return getText(getChild(getChild(p, 'parameternamelist'), 'parametername'), 'name'); } + var paramName = getParamName(param); + var paramDescription = getText(getChild(param, 'parameterdescription'), 'description'); + if (_.has(spec_js, paramName)) { + spec_js[paramName].description = paramDescription; + } else { + var msg = ' has documentation for an unknown parameter: ' + paramName + '. '; + var suggestions = _.difference(_.keys(spec_js), _.map(details, getParamName)); + var msgAddendum = (!_.isEmpty(suggestions) ? ('Did you mean ' + suggestions.join(', or ') + '?') : ''); + console.log('Warning: ' + method + msg + msgAddendum); + } + }); + return spec_js; +} + + +// get the equivalent javascript type from the given c type +function getType(type_c) { + var type_js = type_c; + _.find(xml2js.TYPEMAPS, function(to, from) { + var pattern = new RegExp(from, 'i'); + if (type_c.search(pattern) == 0) { + type_js = to; + } + }); + // TODO: temporary solution + // remove extra whitespace from pointers + // permanent solution would be to get rid of pointers all together + if (type_js.search(/\S+\s*\*$/) != -1) { + type_js = type_js.replace(/\s*\*$/, '*'); + } + return type_js; +} + + +// verify that all types associated with the method are valid +function hasValidTypes(methodSpec, methodName, parent) { + var valid = true; + var msg = (xml2js.opts.strict ? ' is omitted from JS documentation.' : ' has invalid type(s).'); + var printIgnoredMethodOnce = _.once(function() { console.log(methodName + msg); }); + _.each(methodSpec.params, function(paramSpec, paramName) { + if (!isValidType(paramSpec.type, parent)) { + valid = false; + printIgnoredMethodOnce(); + console.log(' Error: parameter ' + paramName + ' has invalid type ' + paramSpec.type); + } + }); + if (!_.isEmpty(methodSpec.return) && !isValidType(methodSpec.return.type, parent)) { + valid = false; + printIgnoredMethodOnce(); + console.log(' Error: returns invalid type ' + methodSpec.return.type); + } + return valid; +} + + +// verify that type of variable is valid +function ofValidType(varSpec, varName, parent) { + if (isValidType(varSpec.type, parent)) { + return true; + } else { + var msgAddendum = (xml2js.opts.strict ? ' Omitted from JS documentation.' : ''); + console.log('Error: ' + varName + ' is of invalid type ' + varSpec.type + '.' + msgAddendum); + return false; + } +} + + +// verify whether the given type is valid JS +// TODO: check class-specific types +function isValidType(type, parent) { + return (_.contains(_.values(xml2js.TYPEMAPS), type) || + _.has(xml2js.CLASSES, type) || + _.has(xml2js.ENUMS_BY_GROUP, type) || + _.contains(['Buffer', 'Function', 'mraa_result_t'], type) || + _.has((parent ? xml2js.CLASSES[parent].enums_by_group : []), type)); +} + + +// determines whether a type looks like a c pointer +function isPointer(type) { + return (type.search(/\w+\s*\*/) != -1); +} + + +// get the detailed description of a method's parameters +function getParamsDetails(spec_c) { + var paras = getChildren(spec_c, 'para'); + var details = _.find(_.map(paras, function(para) { + return getChild(para, 'parameterlist'); + }), function(obj) { return (obj != undefined); }); + return (details ? details.children : undefined); +} + + +// get the detailed description of a method's return value +function getReturnDetails(spec_c) { + var paras = getChildren(spec_c, 'para'); + return _.find(_.map(paras, function(para) { + return getChild(para, 'simplesect'); + }), function(obj) { return ((obj != undefined) && (getAttr(obj, 'kind') == 'return')); }); +} + + +// get (and flatten) the text of the given object +function getText(obj, why) { + // TODO: links ignored for now, patched for types for + var GENERATE_LINK = function(x) { return x + ' '; } + return _.reduce(obj.children, function(text, elem) { + if (_.isString(elem)) { + return text += elem.trim() + ' '; + } else if (_.isPlainObject(elem)) { + switch(elem.name) { + case 'para': + return text += getText(elem, why) + ' \n'; + case 'ref': + return text += GENERATE_LINK(getText(elem, why)); + case 'parameterlist': + case 'simplesect': + return text; // to be handled elsewhere + case 'programlisting': + case 'htmlonly': + return text; // ignored + // TODO: html doesn't seem to work, using markdown for now + case 'itemizedlist': + return text += '\n' + getText(elem, why) + '\n'; + case 'listitem': + return text += '+ ' + getText(elem, why) + '\n'; + case 'bold': + return text += '__' + getText(elem, why).trim() + '__ '; + case 'ulink': + return text += '[' + getText(elem, why).trim() + '](' + getAttr(elem, 'url').trim() + ') '; + case 'image': + // TODO: copy images over; hard coded for now + var fn = getAttr(elem, 'name'); + return text += ' \n \n![' + fn + '](' + '../../../../docs/images/' + fn + ') '; + case 'linebreak': + return text += ' \n'; + case 'ndash': + return text += '– '; + default: + // TODO: incomplete list of doxygen xsd implemented + throw new Error('NYI Unknown Object Type: ' + elem.name); + } + } else { + throw new Error('NYI Unknown Type: ' + (typeof elem)); + } + }, '').trim(); +} + + +// get the value of attribute with the given name of the given object +function getAttr(obj, name) { + return _.find(obj.attr, function(item) { + return item.name == name; + }).value; +} + + +// get the child object with the given name of the given object +function getChild(obj, name) { + return _.find(obj.children, function(child) { + return child.name == name; + }); +} + + +// get all children objects with the given name of the given object +function getChildren(obj, name) { + return _.filter(obj.children, function(child) { + return child.name == name; + }); +} + + +// get the class name from its xml spec +function getClassName(spec_c) { + return getText(getChild(spec_c, 'compoundname'), 'name').replace(xml2js.opts.module + '::', ''); +} + + +// debug helper: print untruncated object +function printObj(obj) { + console.log(util.inspect(obj, false, null)); +} + + +module.exports = xml2js;