1/*
2 * Author: Heidi Pan <heidi.pan@intel.com>
3 * Copyright (c) 2015 Intel Corporation.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining
6 * a copy of this software and associated documentation files (the
7 * "Software"), to deal in the Software without restriction, including
8 * without limitation the rights to use, copy, modify, merge, publish,
9 * distribute, sublicense, and/or sell copies of the Software, and to
10 * permit persons to whom the Software is furnished to do so, subject to
11 * the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be
14 * included in all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 */
24
25// dependencies
26var peg     = require('pegjs')
27  , fs      = require('fs')
28  , path    = require('path')
29  , Promise = require('bluebird')
30  , _       = require('lodash')
31  , util    = require('util');
32
33
34// use promise-style programming rather than spaghetti callbacks
35Promise.promisifyAll(fs);
36
37
38var xml2js = {
39
40  // js-format specs
41  // MODULE: <module name>
42  // ENUMS: {
43  //   <enum name>: {
44  //     type: <enum type>,
45  //     description: <enum description>
46  //   }, ...
47  // }
48  // ENUMS_BY_GROUP: {
49  //   <enum type>: {
50  //     description: <enum group description>
51  //     members: [ <enum name>, ... ]
52  //   }, ...
53  // }
54  // METHODS: {
55  //   <method name>: {
56  //     description: <method description>,
57  //     params: {
58  //       <param name>: {
59  //         type: <param type>,
60  //         description: <param description >
61  //       }, ...
62  //     },
63  //     return: {
64  //       type: <return type>,
65  //       description: <return description>
66  //     }
67  //   }, ...
68  // }
69  // CLASSES: {
70  //   <class name>: {
71  //     description: <class description>,
72  //     parent: <parent class name>,
73  //     group: <group name>,
74  //     methods: { ... },
75  //     variables: {
76  //       <variable name>: {
77  //         type: <variable type>,
78  //         description: <variable description>
79  //       }
80  //     },
81  //     enums: { ... },
82  //     enums_by_group: { ... }
83  //   }, ...
84  // }
85  // CLASSGROUPS: {
86  //   <group name>: {
87  //     description: <group description>,
88  //     classes: [ <class name>, ... ],
89  //     enums: { ... },
90  //     enums_by_group: { ... }
91  //   }, ...
92  // }
93  MODULE: '',
94  ENUMS: {},
95  ENUMS_BY_GROUP: {},
96  METHODS: {},
97  CLASSES: {},
98  CLASSGROUPS: {},
99
100
101  // baseline c -> js type mapping
102  TYPEMAPS: {
103    '^(const)?\\s*(unsigned|signed)?\\s*(int|short|long|float|double|size_t|u?int\\d{1,2}_t)?$': 'Number',
104    '^bool$': 'Boolean',
105    '^(const)?\\s*(unsigned|signed)?\\s*(char|char\\s*\\*|std::string)$': 'String',  // TODO: verify that swig does this mapping
106    '^void\\s*\\(\\s*\\*\\s*\\)\\s*\\(\\s*void\\s*\\*\\)\\s*$': 'Function'
107  },
108
109
110  // custom c -> js type mapping for pointers
111  // ARRAY_TYPEMAPS: {
112  //    <pointer data type>: {
113  //      arrayType: <swig generated array type that will replace pointers of data type>,
114  //      classes: [ <class that contains arrayType>, ... ]
115  //    }, ...
116  // }
117  // POINTER_TYPEMAPS: {
118  //    <class that contains pointerType>: {
119  //      <c pointer data type>: <js swig generated pointer type that will replace pointers of data type>, ...
120  //    }, ...
121  // }
122  ARRAY_TYPEMAPS: {},
123  POINTER_TYPEMAPS: {},
124
125
126  // add command line options for this module
127  addOptions: function(opts) {
128    xml2js.opts = opts;
129    return opts
130      .option('-i, --inputdir [directory]', 'directory for xml files', __dirname + '/xml/mraa')
131      .option('-c, --custom [file]', 'json for customizations')
132      .option('-t, --typemaps [directory]', 'directory for custom pointer type maps')
133      .option('-g, --imagedir [directory]', 'directory to link to where the images will be kept', '')
134      .option('-s, --strict', 'leave out methods/variables if unknown type')
135  },
136
137
138  // parse doxygen xml -> js-format specs
139  // TODO: figure out whether we need to document any protected methods/variables
140  parse: function() {
141    var XML_GRAMMAR_SPEC = 'grammars/xml.peg';
142    var NAMESPACE_SPEC = xml2js.opts.inputdir + '/namespace' + xml2js.opts.module + '.xml';
143    var CLASS_SPEC = function(c) { return xml2js.opts.inputdir + '/' + c + '.xml'; }
144    var TYPES_SPEC = xml2js.opts.inputdir + '/types_8h.xml';
145    xml2js.MODULE = xml2js.opts.module;
146    return Promise.join(createXmlParser(XML_GRAMMAR_SPEC),
147                        xml2js.opts.typemaps ? initCustomPointerTypemaps(xml2js.opts.typemaps) : Promise.resolve(),
148                        fs.readFileAsync(NAMESPACE_SPEC, 'utf8'),
149                        fs.existsSync(TYPES_SPEC) ? fs.readFileAsync(TYPES_SPEC, 'utf8') : Promise.resolve(null),
150                        function(xmlparser, ignore, xml, xml_types) {
151      if (xml_types != null) {
152        _.extend(xml2js.ENUMS, getEnums(xmlparser.parse(xml_types)[0], false));
153        _.extend(xml2js.ENUMS_BY_GROUP, getEnums(xmlparser.parse(xml_types)[0], true));
154      }
155      var spec_c = xmlparser.parse(xml)[0];
156      _.extend(xml2js.ENUMS, getEnums(spec_c, false));
157      _.extend(xml2js.ENUMS_BY_GROUP, getEnums(spec_c, true));
158      _.extend(xml2js.METHODS, getMethods(spec_c));
159      _.each(getSubclassNames(spec_c), function(className) { xml2js.CLASSES[className] = {} });
160      var parseClasses = _.map(getSubclasses(spec_c), function(c) {
161        return fs.readFileAsync(CLASS_SPEC(c), 'utf8').then(function(xml) {
162          try {
163            var spec_c = xmlparser.parse(xml)[0];
164            var className = getName(spec_c);
165            _.extend(xml2js.CLASSES[className], {
166              description: getDescription(spec_c),
167              parent: getParent(spec_c, className),
168              enums: getEnums(spec_c, false, className),
169              enums_by_group: getEnums(spec_c, true, className),
170              variables: getVariables(spec_c, className),
171              methods: getMethods(spec_c, className)
172            });
173          } catch(e) {
174            console.log(e.toString() + ': class ' + className + ' was not parsed correctly.');
175          }
176        });
177      });
178      var parseGroups = fs.readdirAsync(xml2js.opts.inputdir).then(function(files) {
179        var groupxmlfiles = _.filter(files, function(fn) {
180          return ((path.extname(fn) == '.xml') && (path.basename(fn).search(/^group/) != -1));
181        });
182        return Promise.all(_.map(groupxmlfiles, function(fn) {
183          return fs.readFileAsync(xml2js.opts.inputdir + '/' + fn, 'utf8').then(function(xml) {
184            var spec_c = xmlparser.parse(xml)[0];
185            if (_.isEmpty(getSubmodules(spec_c))) {
186              var group = getName(spec_c);
187              var classes = getSubclassNames(spec_c);
188              xml2js.CLASSGROUPS[group] = {
189                description: getDescription(spec_c),
190                classes: classes
191              };
192              _.each(classes, function(c) {
193                if (_.has(xml2js.CLASSES, c)) {
194                  xml2js.CLASSES[c].group = group;
195                } else {
196                  console.log('Warning: Group ' + group + ' has unknown class ' + c);
197                }
198              });
199            }
200          });
201        }));
202      });
203      return Promise.all(parseClasses.concat(parseGroups));
204    }).then(function() {
205      if (!_.isEmpty(xml2js.CLASSGROUPS)) {
206        // try to categorize ungrouped classes, if any
207        var grouped = _.flatten(_.pluck(_.values(xml2js.CLASSGROUPS), 'classes'));
208        var ungrouped = _.difference(_.keys(xml2js.CLASSES), grouped);
209        _.each(ungrouped, function(c) {
210          _.each(findUsage(c), function(group) {
211            xml2js.CLASSGROUPS[group].classes.push(c);
212          });
213        });
214        grouped = _.flatten(_.pluck(_.values(xml2js.CLASSGROUPS), 'classes'));
215        ungrouped = _.difference(_.keys(xml2js.CLASSES), grouped);
216        // try to categorize ungrouped enums, if any
217        _.each(xml2js.ENUMS_BY_GROUP, function(enumGroupSpec, enumGroupName) {
218          _.each(findUsage(enumGroupName, true), function(c) {
219            xml2js.CLASSES[c].enums_by_group[enumGroupName] = enumGroupSpec;
220            _.each(enumGroupSpec.members, function(enumName) {
221              xml2js.CLASSES[c].enums[enumName] = xml2js.ENUMS[enumName];
222              delete xml2js.ENUMS[enumName];
223            });
224            delete xml2js.ENUMS_BY_GROUP[enumGroupName];
225          });
226        });
227      }
228    }).then(function() {
229      if (xml2js.opts.custom && fs.existsSync(xml2js.opts.custom)) {
230        return fs.readFileAsync(xml2js.opts.custom, 'utf8').then(function(custom) {
231          try {
232            customizeMethods(JSON.parse(custom));
233          } catch(e) {
234            console.log('invalid custom.json, ignored. ' + e.toString());
235          }
236        });
237      } else {
238        console.log(xml2js.opts.custom ? ('Error: No such customization file exists: ' + xml2js.opts.custom) : 'No customizations given.');
239      }
240    }).then(function() {
241      generateCustomPointerClasses();
242      validateMethods();
243      validateVars();
244      return _.pick(xml2js, 'MODULE', 'ENUMS', 'ENUMS_BY_GROUP', 'METHODS', 'CLASSES', 'CLASSGROUPS');
245    });
246  }
247};
248
249
250// create an xml parser
251function createXmlParser(XML_GRAMMAR_SPEC) {
252  return fs.readFileAsync(XML_GRAMMAR_SPEC, 'utf8').then(function(xmlgrammar) {
253    return peg.buildParser(xmlgrammar);
254  });
255}
256
257
258// configure c->js typemaps from custom swig directives
259// TODO: many built in assumptions based on current upm file structures & .i customizations
260function initCustomPointerTypemaps(typemapsdir) {
261  return fs.readdirAsync(typemapsdir).then(function(dirs) {
262    return Promise.all(_.map(dirs, function(dir) {
263      // get all js*.i directives from class-specific subdirectories, to be parsed below for %typemaps directives
264      return fs.readdirAsync(typemapsdir + '/' + dir).then(function(files) {
265        var directive = _.find(files, function(fn) {
266          return ((path.extname(fn) == '.i') && (path.basename(fn).search(/^js/) != -1));
267        });
268        var data = {};
269        if (directive) {
270          data[dir] = typemapsdir + '/' + dir + '/' + directive;
271        }
272        return data;
273      }).catch(function(e) {
274        // get all .i directives from top level directory, and parse for %array_class directives
275        if (e.code == 'ENOTDIR') {
276          var fn = dir;
277          if (path.extname(fn) == '.i') {
278            return fs.readFileAsync(typemapsdir + '/' + fn, 'utf8').then(function(directives) {
279              var arraytypes = _.filter(directives.split(/\n/), function(line) {
280                return (line.search(/^%array_class/) != -1);
281              });
282              _.each(arraytypes, function(arraytype) {
283                var parsed = arraytype.match(/%array_class\(([A-Za-z0-9_]+)[\s]*,[\s]*([A-Za-z0-9_]+)\)/);
284                if (parsed) {
285                  var from = parsed[1];
286                  var to = parsed[2];
287                  xml2js.ARRAY_TYPEMAPS[from] = { arrayType: to, classes: [] };
288                } else {
289                  console.log('Incorrectly parsed array_class from ' + fn + ': ' + arraytype);
290                }
291              });
292            });
293          }
294        } else {
295          throw e;
296        }
297      });
298    }));
299  }).then(function(__directivesFiles) {
300    // parse for %typemaps & %pointer_functions directives
301    var _directivesFiles = _.filter(__directivesFiles, function(data) { return !_.isEmpty(data); });
302    var directivesFiles = _.object(_.map(_directivesFiles, _.keys), _.flatten(_.map(_directivesFiles, _.values)));
303    return Promise.all(_.map(directivesFiles, function(directivesFn, className) {
304      return fs.readFileAsync(directivesFn, 'utf8').then(function(directives) {
305        var typemaps = _.filter(directives.split(/\n/), function(line) {
306          return (line.search(/^%typemap/) != -1);
307        });
308        _.each(typemaps, function(typemap) {
309          var parsed = typemap.match(/%typemap\((in|out)\)[\s]+([A-Za-z0-9_]+[\s]*[\*])/);
310          if (parsed) {
311            var dir = parsed[1]; // TODO: ignored for now
312            var type = normalizePointer(parsed[2]);
313            var datatype = getPointerDataType(type);
314            if (_.has(xml2js.ARRAY_TYPEMAPS, datatype)) {
315              xml2js.ARRAY_TYPEMAPS[datatype].classes.push(className);
316            } else {
317              console.log('Ignored typemap from ' + directivesFn + ': ' + typemap.replace('{', '') + ' (no %array_class directive found for ' + datatype + ')');
318            }
319          } else {
320            console.log('Ignored typemap from ' + directivesFn + ': ' + typemap.replace('{', '') + ' (only considering in/out typemaps of pointer types)');
321          }
322        });
323        var ptrfns = _.filter(directives.split(/\n/), function(line) {
324          return (line.search(/^%pointer_functions/) != -1);
325        });
326        _.each(ptrfns, function(ptrfn) {
327          var parsed = ptrfn.match(/%pointer_functions\(([A-Za-z0-9_]+)[\s]*,[\s]*([A-Za-z0-9_]+)\)/);
328          if (parsed) {
329            var from = parsed[1];
330            var to = parsed[2];
331            if (!_.has(xml2js.POINTER_TYPEMAPS, className)) {
332              xml2js.POINTER_TYPEMAPS[className] = {};
333            }
334            xml2js.POINTER_TYPEMAPS[className][from] = to;
335          }
336        });
337      });
338    }));
339  });
340}
341
342
343// generate class specs for custom pointer types
344function generateCustomPointerClasses() {
345  var arrayTypes = _.pluck(_.values(xml2js.ARRAY_TYPEMAPS), 'arrayType');
346  var pointerTypes = _.uniq(_.flatten(_.map(_.values(xml2js.POINTER_TYPEMAPS), _.values)));
347  _.each(arrayTypes, function(arrayType) {
348    var dataType = _.findKey(xml2js.ARRAY_TYPEMAPS, function(to) { return to.arrayType == arrayType; });
349    xml2js.CLASSES[arrayType] = {
350      description: 'Array of type ' + dataType + '.',
351      enums: {},
352      enums_by_group: {},
353      variables: {},
354      methods: {}
355    };
356    xml2js.CLASSES[arrayType].methods[arrayType] = {
357      description: 'Instantiates the array.',
358      params: {
359        nelements: {
360          type: 'Number',
361          description: 'number of elements in the array'
362        }
363      },
364      return: {}
365    };
366    xml2js.CLASSES[arrayType].methods.getitem = {
367      description: 'Access a particular element in the array.',
368      params: {
369        index: {
370          type: 'Number',
371          description: 'index of array to read from'
372        },
373      },
374      return: {
375        type: getType(dataType),
376        description: 'the value of the element found at the given index of the array'
377      }
378    };
379    xml2js.CLASSES[arrayType].methods.setitem = {
380      description: 'Modify a particular element in the array.',
381      params: {
382        index: {
383          type: 'Number',
384          description: 'index of array to write to'
385        },
386        value: {
387          type: getType(dataType),
388          description: 'the value to set the element found at the given index of the array'
389        }
390      },
391      return: {}
392    };
393  });
394  var pointerDataTypeMap = _.reduce(_.map(_.values(xml2js.POINTER_TYPEMAPS), _.invert), function(memo, typemap) {
395    return _.extend(memo, typemap);
396  }, {});
397  _.each(pointerTypes, function(pointerType) {
398    var dataType = pointerDataTypeMap[pointerType];
399    xml2js.CLASSES[pointerType] = {
400      description: 'Proxy object to data of type ' + dataType + '.',
401      enums: {},
402      enums_by_group: {},
403      variables: {},
404      methods: {}
405    };
406    xml2js.CLASSES[pointerType].methods[pointerType] = {
407      description: 'Instantiates the proxy object.',
408      params: {},
409      return: {}
410    };
411    xml2js.CLASSES[pointerType].methods.value = {
412      description: 'Get the value of the object.',
413      params: {},
414      return: {
415        type: getType(dataType),
416        description: 'the value of the object'
417      }
418    };
419    xml2js.CLASSES[pointerType].methods.assign = {
420      description: 'Set the value of the object.',
421      params: {
422        value: {
423          type: getType(dataType),
424          description: 'the value to set the object to'
425        }
426      },
427      return: {}
428    };
429  });
430}
431
432
433// search for usage of a type
434function findUsage(type, classOnly) {
435  var filterClasses = function(fn) { return _.without(_.map(xml2js.CLASSES, fn), undefined); };
436  var usesType = function(classSpec, className) {
437    var methodsOfType = (_.find(classSpec.methods, function(methodSpec, methodName) {
438      return ((!_.isEmpty(methodSpec.return) && methodSpec.return.type == type) ||
439              (_.contains(_.pluck(methodSpec.params, 'type'), type)));
440    }) != undefined);
441    var variablesOfType = _.contains(_.pluck(classSpec.variable, 'type'), type);
442    return ((methodsOfType || variablesOfType) ? className : undefined);
443  };
444  var extendsType = function(classSpec, className) {
445    return ((classSpec.parent == type) ? className : undefined);
446  };
447  var classes = _.union(filterClasses(usesType), filterClasses(extendsType));
448  if (classOnly) {
449    return classes;
450  } else {
451    return _.without(_.uniq(_.pluck(_.pick(xml2js.CLASSES, classes), 'group')), undefined);
452  }
453}
454
455
456// override autogenerated methods with custom configuration
457function customizeMethods(custom) {
458  _.each(custom, function(classMethods, className) {
459    _.extend(xml2js.CLASSES[className].methods, _.pick(classMethods, function(methodSpec, methodName) {
460      return isValidMethodSpec(methodSpec, className + '.' + methodName);
461    }));
462  });
463}
464
465
466// make sure methods have valid types, otherwise warn (& don't include if strict)
467function validateMethods() {
468  xml2js.METHODS = _.pick(xml2js.METHODS, function(methodSpec, methodName) {
469    return hasValidTypes(methodSpec, methodName);
470  });
471  _.each(xml2js.CLASSES, function(classSpec, className) {
472    var valid = _.pick(classSpec.methods, function(methodSpec, methodName) {
473      return hasValidTypes(methodSpec, className + '.' + methodName, className);
474    });
475    if (xml2js.opts.strict) {
476      xml2js.CLASSES[className].methods = valid;
477    }
478  });
479}
480
481
482// make sure variables have valid types, otherwise warn (& don't include if strict)
483function validateVars() {
484  _.each(xml2js.CLASSES, function(classSpec, className) {
485    var valid = _.pick(classSpec.variables, function(varSpec, varName) {
486      return ofValidType(varSpec, className + '.' + varName, className);
487    });
488    if (xml2js.opts.strict) {
489      xml2js.CLASSES[className].variables = valid;
490    }
491  });
492}
493
494
495// verify that the json spec is well formatted
496function isValidMethodSpec(methodSpec, methodName) {
497  var valid = true;
498  var printIgnoredMethodOnce = _.once(function() { console.log(methodName + ' from ' + path.basename(xml2js.opts.custom) + ' is omitted from JS documentation.'); });
499  function checkRule(rule, errMsg) {
500    if (!rule) {
501      printIgnoredMethodOnce();
502      console.log('  ' + errMsg);
503      valid = false;
504    }
505  }
506  checkRule(_.has(methodSpec, 'description'), 'no description given');
507  checkRule(_.has(methodSpec, 'params'), 'no params given (specify "params": {} for no params)');
508  _.each(methodSpec.params, function(paramSpec, paramName) {
509    checkRule(_.has(paramSpec, 'type'), 'no type given for param ' + paramName);
510    checkRule(_.has(paramSpec, 'description'), 'no description given for param ' + paramName);
511  });
512  checkRule(_.has(methodSpec, 'return'), 'no return given (specify "return": {} for no return value)');
513  checkRule(_.has(methodSpec.return, 'type'), 'no type given for return value');
514  checkRule(_.has(methodSpec.return, 'description'), 'no description given for return value');
515  return valid;
516}
517
518
519// get enum specifications
520function getEnums(spec_c, bygroup, parent) {
521  var spec_js = {};
522  var enumGroups = _.find(getChildren(spec_c, 'sectiondef'), function(section) {
523    var kind = getAttr(section, 'kind');
524    return ((kind == 'enum') || (kind == 'public-type'));
525  });
526  if (enumGroups) {
527    _.each(enumGroups.children, function(enumGroup) {
528      var enumGroupName = getText(getChild(enumGroup, 'name'), 'name');
529      var enumGroupDescription = getText(getChild(enumGroup, 'detaileddescription'), 'description');
530      var enumGroupVals = getChildren(enumGroup, 'enumvalue');
531      if (bygroup) {
532        spec_js[enumGroupName] = {
533          description: enumGroupDescription,
534          members: []
535        };
536      }
537      _.each(enumGroupVals, function(e) {
538        // TODO: get prefix as option
539        var enumName = getText(getChild(e, 'name'), 'name').replace(/^MRAA_/, '');
540        var enumDescription = getText(getChild(e, 'detaileddescription'), 'description');
541        if (!bygroup) {
542          spec_js[enumName] = {
543            type: enumGroupName,
544            description: enumDescription
545          };
546        } else {
547          spec_js[enumGroupName].members.push(enumName);
548        }
549      });
550    });
551  }
552  return spec_js;
553}
554
555
556// get the name for the module/group/class
557function getName(spec_c) {
558  return getText(getChild(spec_c, 'compoundname'), 'name').replace(xml2js.opts.module + '::', '');
559}
560
561
562// get the description for the module/group/class
563function getDescription(spec_c) {
564  return getText(getChild(spec_c, 'detaileddescription'), 'description');
565}
566
567
568// get the classes (xml file names) for the given module
569function getSubclasses(spec_c) {
570  return _.map(getChildren(spec_c, 'innerclass'), function(innerclass) {
571    return getAttr(innerclass, 'refid');
572  });
573}
574
575
576// get the classes (class names) for the given module
577function getSubclassNames(spec_c) {
578  return _.map(getChildren(spec_c, 'innerclass'), function(innerclass) {
579    return getText(innerclass).replace(xml2js.opts.module + '::', '');
580  });
581}
582
583
584// get the submodules (xml file names) for the given module
585function getSubmodules(spec_c) {
586  return _.map(getChildren(spec_c, 'innergroup'), function(innergroup) {
587    return getAttr(innergroup, 'refid');
588  });
589}
590
591
592// get parent class, if any
593function getParent(spec_c, className) {
594  var parent = getChild(spec_c, 'basecompoundref');
595  if (parent) {
596    parent = getText(parent);
597    if (!_.has(xml2js.CLASSES, parent)) {
598      console.log('WARNING: Class ' + className + ' has unknown parent class ' + parent);
599    }
600  }
601  return parent;
602}
603
604
605function hasParams(paramsSpec) {
606  return !(_.isEmpty(paramsSpec) ||
607           ((_.size(paramsSpec) == 1) && getText(getChild(paramsSpec[0], 'type')) == 'void'));
608}
609
610
611// get method specifications for top-level module or a given class
612// TODO: overloaded functions
613// TODO: functions w/ invalid parameter(s)/return
614function getMethods(spec_c, parent) {
615  var spec_js = {};
616  var methods = _.find(getChildren(spec_c, 'sectiondef'), function(section) {
617    var kind = getAttr(section, 'kind');
618    return ((kind == 'public-func') || (kind == 'func'));
619  });
620  if (methods) {
621    _.each(methods.children, function(method) {
622      var methodName = getText(getChild(method, 'name'), 'name');
623      if (methodName[0] != '~') { // filter out destructors
624        try {
625          var description = getChild(method, 'detaileddescription');
626          var methodDescription = getText(description, 'description');
627          var paramsSpec = getChildren(method, 'param');
628          var params = {};
629          if (hasParams(paramsSpec)) {
630              params = getParams(paramsSpec, getParamsDetails(description), methodName, parent);
631          }
632          var returnSpec = getChild(method, 'type');
633          var retval = {};
634          if (!_.isEmpty(returnSpec)) {
635            retval = getReturn(returnSpec, getReturnDetails(description), methodName, parent);
636          }
637          methodName = getUniqueMethodName(methodName, spec_js, parent);
638          spec_js[methodName] = {
639            description: methodDescription,
640            params: params,
641            return: retval
642          };
643        } catch(e) {
644          console.log((parent ? (parent + '.') : '') + methodName + ' is omitted from JS documentation.');
645          console.log('  ' + e.toString());
646        }
647      }
648    });
649  }
650  return spec_js;
651}
652
653
654// get a unique string to represent the name of an overloaded method
655function getUniqueMethodName(methodName, module, parent) {
656  if (methodName in module) {
657    do {
658      methodName += '!';
659    } while (methodName in module);
660  }
661  return methodName;
662}
663
664
665// get variable specifications for a class
666function getVariables(spec_c, parent) {
667  var spec_js = {};
668  var vars = _.find(getChildren(spec_c, 'sectiondef'), function(section) {
669    var kind = getAttr(section, 'kind');
670    return (kind == 'public-attrib');
671  });
672  if (vars) {
673    _.each(_.filter(vars.children, function(variable) {
674      return (getAttr(variable, 'kind') == 'variable');
675    }), function(variable) {
676      var varName = getText(getChild(variable, 'name'), 'name');
677      var varType = getType(getText(getChild(variable, 'type')), parent);
678      var varDescription = getText(getChild(variable, 'detaileddescription'));
679      spec_js[varName] = {
680        type: varType,
681        description: varDescription
682      }
683    });
684  }
685  return spec_js;
686}
687
688
689// get return value specs of a method
690function getReturn(spec_c, details, method, parent) {
691  var retType = getType(getText(spec_c, 'type'), parent);
692  var retDescription = (details ? getText(details, 'description') : '');
693  return ((retType == 'void') ? {} : {
694    type: retType,
695    description: retDescription
696  });
697}
698
699
700// get paramater specs of a method
701function getParams(spec_c, details, method, parent) {
702  var spec_js = {};
703  _.each(spec_c, function(param) {
704    try {
705      var paramType = getType(getText(getChild(param, 'type'), 'type'), parent);
706      var paramName = getText(getChild(param, 'declname'), 'name');
707      spec_js[paramName] = { type: paramType };
708    } catch(e) {
709      if (paramType == '...') {
710        spec_js['arguments'] = { type: paramType };
711      } else {
712        throw e;
713      }
714    }
715  });
716  _.each(details, function(param) {
717    var getParamName = function(p) { return getText(getChild(getChild(p, 'parameternamelist'), 'parametername'), 'name'); }
718    var paramName = getParamName(param);
719    var paramDescription = getText(getChild(param, 'parameterdescription'), 'description');
720    if (_.has(spec_js, paramName)) {
721      spec_js[paramName].description = paramDescription;
722    } else {
723      var msg = ' has documentation for an unknown parameter: ' + paramName + '. ';
724      var suggestions = _.difference(_.keys(spec_js), _.map(details, getParamName));
725      var msgAddendum = (!_.isEmpty(suggestions) ? ('Did you mean ' + suggestions.join(', or ') + '?') : '');
726      console.log('Warning: ' + (parent ? (parent + '.') : '') + method + msg + msgAddendum);
727    }
728  });
729  return spec_js;
730}
731
732
733// get the equivalent javascript type from the given c type
734function getType(type_c, parent) {
735  var type_js = type_c;
736  _.find(xml2js.TYPEMAPS, function(to, from) {
737    var pattern = new RegExp(from, 'i');
738    if (type_c.search(pattern) == 0) {
739      type_js = to;
740      return true;
741    }
742  });
743  if (isPointer(type_js)) {
744    var dataType = getPointerDataType(type_js);
745    var className = parent.toLowerCase();
746    if (_.has(xml2js.ARRAY_TYPEMAPS, dataType) && _.contains(xml2js.ARRAY_TYPEMAPS[dataType].classes, className)) {
747      type_js = xml2js.ARRAY_TYPEMAPS[dataType].arrayType;
748    } else if (_.has(xml2js.POINTER_TYPEMAPS, className) && _.has(xml2js.POINTER_TYPEMAPS[className], dataType)) {
749      type_js = xml2js.POINTER_TYPEMAPS[className][dataType];
750    } else if (_.has(xml2js.CLASSES, dataType)) { // TODO: verify that swig does this mapping
751      type_js = dataType;
752    } else {
753      type_js = dataType + ' &#42;'
754    }
755  }
756  return type_js;
757}
758
759
760// verify that all types associated with the method are valid
761function hasValidTypes(methodSpec, methodName, parent) {
762  var valid = true;
763  var msg = (xml2js.opts.strict ? ' is omitted from JS documentation.' : ' has invalid type(s).');
764  var printIgnoredMethodOnce = _.once(function() { console.log(methodName + msg); });
765  _.each(methodSpec.params, function(paramSpec, paramName) {
766    if (!isValidType(paramSpec.type, parent)) {
767      valid = false;
768      printIgnoredMethodOnce();
769      console.log('  Error: parameter ' + paramName + ' has invalid type ' + typeToString(paramSpec.type));
770    }
771  });
772  if (!_.isEmpty(methodSpec.return) && !isValidType(methodSpec.return.type, parent)) {
773    valid = false;
774    printIgnoredMethodOnce();
775    console.log('  Error: returns invalid type ' + typeToString(methodSpec.return.type));
776  }
777  return valid;
778}
779
780
781// verify that type of variable is valid
782function ofValidType(varSpec, varName, parent) {
783  if (isValidType(varSpec.type, parent)) {
784    return true;
785  } else {
786    var msgAddendum = (xml2js.opts.strict ? ' Omitted from JS documentation.' : '');
787    console.log('Error: ' + varName + ' is of invalid type ' + typeToString(varSpec.type) + '.' + msgAddendum);
788    return false;
789  }
790}
791
792
793// verify whether the given type is valid JS
794function isValidType(type, parent) {
795  return (_.contains(_.values(xml2js.TYPEMAPS), type) ||
796          _.has(xml2js.CLASSES, type) ||
797          _.has(xml2js.ENUMS_BY_GROUP, type) ||
798          _.contains(['Buffer', 'Function', 'mraa_result_t'], type) ||
799          _.has((parent ? xml2js.CLASSES[parent].enums_by_group : []), type) ||
800          isValidPointerType(type, parent));
801}
802
803
804function isValidPointerType(type, parent) {
805  var className = parent.toLowerCase();
806  var arrayTypemap = _.find(xml2js.ARRAY_TYPEMAPS, function(to) { return to.arrayType == type; });
807  var valid = ((arrayTypemap && _.contains(arrayTypemap.classes, className)) ||
808          (_.has(xml2js.POINTER_TYPEMAPS, className) && (_.contains(_.values(xml2js.POINTER_TYPEMAPS[className]), type))));
809  return valid;
810}
811
812
813// determines whether a type looks like a c pointer
814function isPointer(type) {
815  return (type.search(/\w+\s*(\*|&amp;)$/) != -1);
816}
817
818
819// remove extraneous whitespace from pointer types as canonical representation
820function normalizePointer(ptr) {
821  return ptr.replace(/\s*$/, '');
822}
823
824
825// get the data type of a pointer (e.g. int is the data type of int*)
826function getPointerDataType(ptr) {
827  return ptr.replace(/\s*(\*|&amp;)$/, '');
828}
829
830
831// print more human friendly type for error messages
832function typeToString(type) {
833  return type.replace('&#42;', '*');
834}
835
836
837// get the detailed description of a method's parameters
838function getParamsDetails(spec_c) {
839  var paras = getChildren(spec_c, 'para');
840  var details = _.find(_.map(paras, function(para) {
841    return getChild(para, 'parameterlist');
842  }), function(obj) { return (obj != undefined); });
843  return (details ? details.children : undefined);
844}
845
846
847// get the detailed description of a method's return value
848function  getReturnDetails(spec_c) {
849  var paras = getChildren(spec_c, 'para');
850  return _.find(_.map(paras, function(para) {
851    return getChild(para, 'simplesect');
852  }), function(obj) { return ((obj != undefined) && (getAttr(obj, 'kind') == 'return')); });
853}
854
855
856// get (and flatten) the text of the given object
857function getText(obj, why) {
858  // TODO: links ignored for now, patched for types for
859  var GENERATE_LINK = function(x) { return x + ' '; }
860  return _.reduce(obj.children, function(text, elem) {
861    if (_.isString(elem)) {
862      return text += elem.trim() + ' ';
863    } else if (_.isPlainObject(elem)) {
864      switch(elem.name) {
865        case 'para':
866          return text += getText(elem, why) + '  \n';
867        case 'ref':
868          return text += GENERATE_LINK(getText(elem, why));
869        case 'parameterlist':
870        case 'simplesect':
871          return text; // to be handled elsewhere
872        case 'programlisting':
873        case 'htmlonly':
874          return text; // ignored
875        // TODO: html doesn't seem to work for yuidoc, using markdown for now
876        case 'itemizedlist':
877          return text += '\n' + getText(elem, why) + '  \n  \n';
878        case 'listitem':
879          return text += '+ ' + getText(elem, why) + '\n';
880        case 'bold':
881          return text += '__' + getText(elem, why).trim() + '__ ';
882        case 'ulink':
883          return text += '[' + getText(elem, why).trim() + '](' + getAttr(elem, 'url').trim() + ') ';
884        case 'image':
885          // TODO: copy images over; hard coded for now
886          var fn = getAttr(elem, 'name');
887          return text += '  \n  \n![' + fn + '](' + xml2js.opts.imagedir + '/' + fn + ') ';
888        case 'linebreak':
889          return text += '  \n';
890        case 'ndash':
891          return text += '&ndash; ';
892        default:
893          // TODO: incomplete list of doxygen xsd implemented
894          console.warn('NYI Unknown Object Type: ' + elem.name);
895          return text;
896          //throw new Error('NYI Unknown Object Type: ' + elem.name);
897      }
898    } else {
899      throw new Error('NYI Unknown Type: ' + (typeof elem));
900    }
901  }, '').trim();
902}
903
904
905// get the value of attribute with the given name of the given object
906function getAttr(obj, name) {
907  return _.find(obj.attr, function(item) {
908    return item.name == name;
909  }).value;
910}
911
912
913// get the child object with the given name of the given object
914function getChild(obj, name) {
915  return _.find(obj.children, function(child) {
916    return child.name == name;
917  });
918}
919
920
921// get all children objects with the given name of the given object
922function getChildren(obj, name) {
923  return _.filter(obj.children, function(child) {
924    return child.name == name;
925  });
926}
927
928
929// debug helper: print untruncated object
930function printObj(obj) {
931  console.log(util.inspect(obj, false, null));
932}
933
934
935module.exports = xml2js;