meteor/meteor

View on GitHub
docs/scripts/api-box.js

Summary

Maintainability
A
1 hr
Test Coverage
/* global hexo */

var path = require('path');
var fs = require('fs');
var handlebars = require('handlebars');
var _ = require('underscore');
var parseTagOptions = require('./parseTagOptions');
var showdown  = require('showdown');
var converter = new showdown.Converter();

// can't put this file in this folder annoyingly
var html = fs.readFileSync(path.join(__dirname, '..', 'assets', 'api-box.html'), 'utf8');
var template = handlebars.compile(html);

if (!hexo.config.api_box || !hexo.config.api_box.data_file) {
  throw new Error("You need to provide the location of the api box data file in config.api_box.data_file");
}

var dataPath = path.join(hexo.base_dir, hexo.config.api_box.data_file);
var DocsData = require(dataPath);

hexo.extend.tag.register('apibox', function(args) {
  var name = args.shift();
  var options = parseTagOptions(args)

  var dataFromApi = apiData({ name: name });

  if (! dataFromApi) {
    throw new Error("Cannot render apibox without API data: " + name);
    return;
  }

  var defaults = {
    // by default, nest if it's a instance method
    nested: name.indexOf('#') !== -1,
    instanceDelimiter: '#'
  };
  var data = Object.assign({}, defaults, dataFromApi, options);

  data.id = data.longname.replace(/[.#]/g, "-");

  data.signature = signature(data, { short: false });
  data.title = signature(data, {
    short: true,
    instanceDelimiter: data.instanceDelimiter,
  });
  data.importName = importName(data);
  data.paramsNoOptions = paramsNoOptions(data);

  return template(data);
});


var apiData = function (options) {
  options = options || {};
  if (typeof options === "string") {
    options = {name: options};
  }

  var root = DocsData[options.name];

  if (! root) {
    console.log("API Data not found: " + options.name);
  }

  if (_.has(options, 'options')) {
    root = _.clone(root);
    var includedOptions = options.options.split(';');
    root.options = _.filter(root.options, function (option) {
      return _.contains(includedOptions, option.name);
    });
  }

  return root;
};

signature = function (data, options) {
  var escapedLongname = _.escape(data.longname);

  var paramsStr = '';

  if (!options.short) {
    if (data.istemplate || data.ishelper) {
      var params = data.params;

      var paramNames = _.map(params, function (param) {
        var name = param.name;

        name = name + "=" + name;

        if (param.optional) {
          return "[" + name + "]";
        }

        return name;
      });

      paramsStr = ' ' + paramNames.join(" ") + ' ';
    } else {
      // if it is a function, and therefore has arguments
      if (_.contains(["function", "class"], data.kind)) {
        var params = data.params;

        var paramNames = _.map(params, function (param) {
          if (param.optional) {
            return "[" + param.name + "]";
          }

          return param.name;
        });

        paramsStr= "(" + paramNames.join(", ") + ")";
      }
    }
  }

  if (data.istemplate) {
    return '{{> ' + escapedLongname + paramsStr + ' }}';
  } else if (data.ishelper){
    return '{{ ' + escapedLongname + paramsStr + ' }}';
  } else {
    if (data.kind === "class" && !options.short) {
      escapedLongname = 'new ' + escapedLongname;
    }

    // In general, if we are looking at an instance method, we want to show it as
    //   Something#foo or #foo (if short). However, when it's on something called
    //   `this`, we'll do the slightly weird thing of showing `this.foo` in both cases.
    if (data.scope === "instance") {
      // the class this method belongs to.
      var memberOfData = apiData(data.memberof) || apiData(`${data.memberof}#${data.memberof}`);

      // Certain instances are provided to the user in scope with a specific name
      // TBH I'm not sure what else we use instanceName for (why we are setting for
      // e.g. `reactiveVar` if we don't want to show it here) but we opt into showing it
      if (memberOfData.showinstancename) {
        escapedLongname = memberOfData.instancename + "." + data.name;
      } else if (options.short) {
        // Something#foo => #foo
        return options.instanceDelimiter + escapedLongname.split('#')[1];
      }
    }

    // If the user passes in a instanceDelimiter and we are a static method,
    // we are probably underneath a heading that defines the object (e.g. DDPRateLimiter)
    if (data.scope === "static" && options.instanceDelimiter && options.short) {
      // Something.foo => .foo
      return options.instanceDelimiter + escapedLongname.split('.')[1];
    }

    return escapedLongname + paramsStr;
  }
};

var importName = function(doc) {
  const noImportNeeded = !doc.module
    || doc.scope === 'instance'
    || doc.ishelper
    || doc.isprototype
    || doc.istemplate;

  // override the above we've explicitly decided to (i.e. Template.foo.X)
  if (!noImportNeeded || doc.importfrompackage) {
    if (doc.memberof) {
      return doc.memberof.split('.')[0];
    } else {
      return doc.name;
    }
  }
};

var paramsNoOptions = function (doc) {
  return _.reject(doc.params, function (param) {
    return param.name === "options";
  });
};

var typeLink = function (displayName, url) {
  return "<a href='" + url + "'>" + displayName + "</a>";
};

var toOrSentence = function (array) {
  if (array.length === 1) {
    return array[0];
  } else if (array.length === 2) {
    return array.join(" or ");
  }

  return _.initial(array).join(", ") + ", or " + _.last(array);
};

var typeNameTranslation = {
  "function": "Function",
  EJSON: typeLink("EJSON-able Object", "#ejson"),
  EJSONable: typeLink("EJSON-able Object", "#ejson"),
  "Tracker.Computation": typeLink("Tracker.Computation", "#tracker_computation"),
  MongoSelector: [
    typeLink("Mongo Selector", "#selectors"),
    typeLink("Object ID", "#mongo_object_id"),
    "String"
  ],
  MongoModifier: typeLink("Mongo Modifier", "#modifiers"),
  MongoSortSpecifier: typeLink("Mongo Sort Specifier", "#sortspecifiers"),
  MongoFieldSpecifier: typeLink("Mongo Field Specifier", "collections.html#fieldspecifiers"),
  JSONCompatible: "JSON-compatible Object",
  EventMap: typeLink("Event Map", "#eventmaps"),
  DOMNode: typeLink("DOM Node", "https://developer.mozilla.org/en-US/docs/Web/API/Node"),
  "Blaze.View": typeLink("Blaze.View", "#blaze_view"),
  Template: typeLink("Blaze.Template", "#blaze_template"),
  DOMElement: typeLink("DOM Element", "https://developer.mozilla.org/en-US/docs/Web/API/element"),
  MatchPattern: typeLink("Match Pattern", "#matchpatterns"),
  "DDP.Connection": typeLink("DDP Connection", "#ddp_connect")
};

handlebars.registerHelper('typeNames', function typeNames (nameList) {
  // change names if necessary
  nameList = _.map(nameList, function (name) {
    // decode the "Array.<Type>" syntax
    if (name.slice(0, 7) === "Array.<") {
      // get the part inside angle brackets like in Array<String>
      name = name.match(/<([^>]+)>/)[1];

      if (name && typeNameTranslation.hasOwnProperty(name)) {
        return "Array of " + typeNameTranslation[name] + "s";
      }

      if (name) {
        return "Array of " + name + "s";
      }

      console.log("no array type defined");
      return "Array";
    }

    if (typeNameTranslation.hasOwnProperty(name)) {
      return typeNameTranslation[name];
    }

    if (DocsData[name]) {
      return typeNames(DocsData[name].type);
    }

    return name;
  });

  nameList = _.flatten(nameList);

  return toOrSentence(nameList);
});

handlebars.registerHelper('markdown', function(text) {
  return converter.makeHtml(text);
});

handlebars.registerHelper('hTag', function() {
  return this.nested ? 'h3' : 'h2';
});