Skip to content

Latest commit

 

History

History
589 lines (513 loc) · 15.6 KB

lib_ast_digesters.js.md

File metadata and controls

589 lines (513 loc) · 15.6 KB

firedoc 1.9.2

Fireball is the game engine for the future.

File: ``

/**
 * A map of the default tag processors, keyed by the
 * tag name.  Multiple tags can use the same digester
 * by supplying the string name that points to the
 * implementation rather than a function.
 * @module digesters
 * @main digesters
 */

const _ = require('underscore');
const fs = require('graceful-fs');
const utils = require('../utils');

const REGEX_TYPE = /(.*?)\{(.*?)\}(.*)/;
const REGEX_GLOBAL_LINES = /\r\n|\n/g;
const REGEX_FIRSTWORD = /^\s*?(\[([^\[\]]+)\]\*?|[^\s]+)\s*\-?\s*(.*)/;
const REGEX_OPTIONAL = /^\[(.*)\]$/;

/**
 * Flatten a string, remove all line breaks and replace them with a token
 * @method implodeString
 * @private
 * @param {String} str The string to operate on
 * @return {String} The modified string
 */
function implodeString (str) {
  return (str || '').replace(REGEX_GLOBAL_LINES, '!~FIREDOC_LINE~!');
}

/**
 * Un-flatten a string, replace tokens injected with `implodeString`
 * @method implodeString
 * @private
 * @param {String} str The string to operate on
 * @return {String} The modified string
 */
function explodeString (str) {
  return (str || '').replace(/!~FIREDOC_LINE~!/g, '\n');
}

module.exports = {
  /**
   * Handle with params
   * @method params
   */
  'param': function (tagname, value, target, block) {
    target.params = target.params || [];
    if (!value) {
      this.warnings.push({
        message: 'param name/type/descript missing',
        line: utils.stringlog(block)
      });
      console.warn('param name/type/descript missing: ' + utils.stringlog(block));
      return;
    }

    var type, name, parts, optional, optdefault, parent, multiple, len, result,
      desc = implodeString(utils.safetrim(value)),
      match = REGEX_TYPE.exec(desc),
      host = target.params,
      type_;

    // Extract {type}
    if (match) {
      type_ = utils.safetrim(match[2]);
      type = utils.safetrim(match[2]);
      desc = utils.safetrim(match[1] + match[3]);
    }

    // extract the first word, this is the param name
    match = REGEX_FIRSTWORD.exec(desc);
    if (match) {
      name = utils.safetrim(explodeString(match[1]));
      desc = utils.safetrim(match[3]);
    }

    if (!name) {
      if (value && value.match(/callback/i)) {
        this.warnings.push({
          message: 'Fixing missing name for callback',
          line: utils.stringlog(block)
        });
        console.warn('Fixing missing name for callback:' + utils.stringlog(block));
        name = 'callback';
        type = 'Callback';
      } else {
        this.warnings.push({
          message: 'param name missing: ' + value,
          line: utils.stringlog(block)
        });
        console.warn('param name missing: ' + value + ':' + utils.stringlog(block));
        name = 'UNKNOWN';
      }
    }

    len = name.length - 1;
    if (name.charAt(len) === '*') {
        multiple = true;
        name = name.substr(0, len);
    }

    // extract [name], optional param
    if (name.indexOf('[') > -1) {
      match = REGEX_OPTIONAL.exec(name);
      if (match) {
        optional = true;
        name = utils.safetrim(match[1]);
        // extract optional=defaultvalue
        parts = name.split('=');
        if (parts.length > 1) {
          name = parts[0];
          optdefault = parts[1];
          //Add some shortcuts for object/array defaults
          if (optdefault.toLowerCase() === 'object') {
            optdefault = '{}';
          }
          if (optdefault.toLowerCase() === 'array') {
            optdefault = '[]';
          }
        }
      }
    }

    // This should run after the check for optional parameters
    // and before the check for child parameters
    // because the signature for 0..n params is [...args]
    if (name.substr(0, 3) === '...') {
      multiple = true;
      name = name.substr(3);
    }

    // parse object.prop, indicating a child property for object
    if (name.indexOf('.') > -1) {
      match = name.split('.');
      parent = utils.safetrim(match[0]);
      _.each(target.params, function (param) {
        if (param.name === parent) {
          param.props = param.props || [];
          host = param.props;
          match.shift();
          name = utils.safetrim(match.join('.'));
          if (match.length > 1) {
            var pname = name.split('.')[0], par;
            _.each(param.props, function (o) {
              if (o.name === pname) {
                par = o;
              }
            });
            if (par) {
              match = name.split('.');
              match.shift();
              name = match.join('.');
              par.props = par.props || [];
              host = par.props;
            }
          }
        }
      });
    }

    result = {
      name: name,
      description: explodeString(desc)
    };

    if (type) {
      // find types from classitems
      var item = _.findWhere(this.members, {'name': type});
      if (!item && this.context.clazz) {
        item = this.classes[this.context.clazz].types[type];
      }
      if (!item && this.context.module) {
        item = this.modules[this.context.module].types[type];
      }
      // finded the type
      if (item && item.params) {
        // Dont remove the clone, because the item.params will be
        // used by multiple results, so that we need to clone a new
        // one for its own usage.
        result.description = result.description || item.description;
        result.props = _.clone(item.params);
        result.type = type_;
      } else {
        result.type = type;
      }
    }

    if (optional) {
      result.optional = true;
      if (optdefault) {
        result.optdefault = optdefault;
      }
    }

    if (multiple) {
      result.multiple = true;
    }

    // localize the description
    result.description = utils.localize(result.description);

    // push localized result to host
    host.push(result);
  },

  /**
   * Handle with return
   * @method return
   */
  'return': function (tagname, value, target, block) {
    var desc = implodeString(utils.safetrim(value)),
      type,
      match = REGEX_TYPE.exec(desc),
      result = {};

    if (match) {
      type = utils.safetrim(match[2]);
      desc = utils.safetrim(match[1] + match[3]);
    }

    result = {
      description: utils.unindent(explodeString(desc))
    };

    if (type) {
      result.type = type;
    }

    // remove the fist char '-' for @return tag
    result.description = result.description.replace(/^\s?-\s?/, '');

    // localize the description
    result.description = utils.localize(result.description);

    target[tagname] = result;
  },

  /**
   * Handle with throws
   * @property throws
   */
  'throws': 'return',

  'injects': 'return',

  // trying to overwrite the constructor value is a bad idea
  'constructor': function (tagname, value, target, block) {
    target.isConstructor = true;
  },

  // A key bock type for declaring modules and submodules
  // subsequent class and member blocks will be assigned
  // to this module.
  'module': function (tagname, value, target, block) {
    this.context.module = value;
    if (target._raw.process) {
      target.process = utils.fmtProcess(target._raw.process);
    }
    if (target._raw && target._raw.main === value) {
      this.context.mainModule = {
        tag: tagname,
        name: value,
        file: target.file,
        line: target.line,
        type: 'modules',
        description: utils.localize(target.description)
      };
    }
    var mainModule = this.context.mainModule;
    var currModule = this.modules[value];

    if (!currModule._main && target._raw.main === currModule.name) {
      utils.defineReadonly(currModule, 'file', mainModule.file);
      utils.defineReadonly(currModule, 'line', mainModule.line);
    }
    return currModule;
  },

  //Setting the description for the module..
  'main': function (tagname, value, target, block) {
    var o = target;
    o.mainName = value;
    o.tag = 'main';
    o.itemtype = 'main';
    o.description = utils.localize(o.description);
    o._main = true;
    this.context.mainModule = o;
  },

  // accepts a single project definition for the source tree
  'project': function (tagname, value, target, block) {
    this.project.name = value;
    this.project.description = this.project.description || value;
  },
  
  // accepts a single project logo definition
  'logo': function (tagname, value, target, block) {
    this.project.logo = value;
  },

  // A key bock type for declaring submodules.  subsequent class and
  // member blocks will be assigned to this submodule.
  'submodule': function (tagname, value, target, block) {
    this.context.submodule = value;

    var host = this.modules[value];
    var clazz = this.context.clazz;
    var parent = this.context.module;
    if (parent) {
      host.module = parent;
      var parentModule = this.context.ast.modules[parent];
      if (parentModule) {
        this.context.ast.modules[parent].submodules[host.name] = host;
      }
    }
    if (clazz && this.classes[clazz]) {
      this.classes[clazz].submodule = value;
    }
    return host;
  },

  // this is a way to abstract the definitions of callback arguments
  'callback': function (tagname, value, target, block) {
    target.name = value;
    target.isTypeDef = true;
  },

  // A key bock type for declaring classes, subsequent
  // member blocks will be assigned to this class
  'class': function (tagname, value, target, block) {
    var self = this;
    var fullname, host, parent;

    // set the process and attach the process on `target`
    if (target._raw.process) {
      target.process = utils.fmtProcess(target._raw.process);
    } else {
      var mod = this.modules[this.context.module];
      if (mod) {
        target.process = mod.process;
      }
    }

    if (target._raw.extends) {
      var extended = target._raw.extends;
      if (!extended) {
        console.warn('usage: `@extends <class>`, but only found `@extends`');
      } else if (!this.inheritedMembers.length) {
        this.inheritedMembers.push([extended, value]);
      } else {
        var needNewItem = true;
        var item, at;
        _.some(this.inheritedMembers, function (member) {
          item = member;
          at = member.indexOf(extended);
          if (member.length - 1 === at) {
            return true;  // break
          }
          if (member[at + 1] === value) {
            needNewItem = false;
            return true;
          }
          if (at !== -1) {
            return true;
          }
        }, this);
        if (needNewItem) {
          if (extended !== item[item.length - 1]) {
            var newItem = item.slice(0, at + 1);
            newItem.push(value);
            self.inheritedMembers.push(newItem);
          } else {
            item.push(value);
          }
        }
      }
    }
    
    this.context.clazz = value;

    fullname = this.context.clazz;
    host = this.classes[fullname] || {};
    parent = this.context.module;

    if (parent && host) {
      host.module = parent;
    }

    // set `is_enum` when the tagname is "enum"
    if (tagname === 'enum') {
      host.isEnum = true;
      host.type = 'enums';
    } else {
      host.isEnum = false;
      host.type = 'classes';
    }

    //Merge host and target in case the class was defined in a "for" tag
    //before it was defined in a "class" tag
    host = _.extend(host, target);
    this.classes[fullname] = host;
    parent = this.context.submodule;
    if (parent) {
      host.submodule = parent;
    }

    // localize
    host.description = utils.localize(host.description);
    target.description = host.description;
    return host;
  },

  // just defer from class in their names
  'enum': 'class',

  // change 'const' to final property
  'const': function (tagname, value, target, block) {
    target.itemtype = 'property';
    target.name = value;
    /*jshint sub:true */
    target['final'] = '';
  },

  // supported classitems
  'property': function (tagname, value, target, block) {
    var match, name, desc, type;

    target.itemtype = tagname;
    target.name = value;

    if (target._raw.process) {
      target.process = utils.fmtProcess(target._raw.process);
    }

    if (tagname === 'property') {
      var propM = value.match(/^\{(.+)\} ([a-zA-Z0-9_]+)\s*\-?\s*(.+)?$/);
      if (propM && propM.length === 4) {
        value = propM[2];
        target.type = propM[1];
        target.name = propM[2];
        target.description = propM[3];
      }
    }

    if (!target.type) {
      desc = implodeString(utils.safetrim(value));
      match = REGEX_TYPE.exec(desc);

      // Extract {type}
      if (match) {
        type = utils.safetrim(match[2]);
        name = utils.safetrim(match[1] + match[3]);
        target.type = type;
        target.name = name;
      }
    }

    // localize the description
    target.description = utils.localize(target.description);
  },
  'method': 'property',
  'attribute': 'property',
  'config': 'property',
  'event': 'property',

  // access fields
  'public': function (tagname, value, target, block) {
    target.access = tagname;
    target.tagname = value;
  },
  'private': 'public',
  'protected': 'public',
  'inner': 'public',

  // tags that can have multiple occurances in a single block
  'todo': function (tagname, value, target, block) {
    if (!_.isArray(target[tagname])) {
      target[tagname] = [];
    }
    //If the item is @tag one,two
    if (value.indexOf(',') > -1) {
      value = value.split(',');
    } else {
      value = [value];
    }

    value.forEach(function (v) {
      v = utils.safetrim(v);
      target[tagname].push(v);
    });
  },
  'extension_for': 'extensionfor',
  'extensionfor': function (tagname, value, target, block) {
    var clazz = this.context.clazz;
    if (this.classes[clazz]) {
      this.classes[clazz].extension_for.push(value);
    }
  },
  'example': function (tagname, value, target, block) {
    if (value) {
      var linkMatch = value.match(/\{@link (.+)\}/);
      if (linkMatch && linkMatch.length === 2) {
        var relative = utils.safetrim(linkMatch[1]);
        var examplePath = process.cwd() + '/' + relative;
        if (fs.existsSync(examplePath)) {
          value = fs.readFileSync(examplePath, 'utf8');
          value = '```' + value;
        } else {
          value = '```Not found for the example path: ' + relative;
        }
      }
    }

    if (!_.isArray(target[tagname])) {
      target[tagname] = [];
    }

    var e = value;
    block.forEach(function (v) {
      if (v.tag === 'example') {
        if (v.value.indexOf(value) > -1) {
          e = v.value;
        }
      }
    });

    target[tagname].push(e);
  },
  'url': 'todo',
  'icon': 'todo',
  'see': 'todo',
  'requires': 'todo',
  'knownissue': 'todo',
  'uses': 'todo',
  'category': 'todo',
  'unimplemented': 'todo',

  genericValueTag: function (tagname, value, target, block) {
    target[tagname] = value;
  },

  'author': 'genericValueTag',
  'contributor': 'genericValueTag',
  'since': 'genericValueTag',

  'deprecated': function (tagname, value, target, block) {
    target.deprecated = true;
    if (typeof value === 'string' && value.length) {
      target.deprecationMessage = value;
    }
  },

  // updates the current class only (doesn't create
  // a new class definition)
  'for': function (tagname, value, target, block) {
    var ns, file, mod;
    this.context.clazz = value;

    ns = ((this.classes[value]) ? this.classes[value].namespace : '');
    this.context.namespace = ns;

    file = this.context.file;
    if (file) {
      this.files[file].fors[value] = 1;
    }

    mod = this.context.module;
    if (mod) {
      this.modules[mod].fors[value] = 1;
    }

    mod = this.context.submodule;
    if (mod) {
      this.modules[mod].fors[value] = 1;
    }
  }
};