/* 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 "" + displayName + ""; }; 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." syntax if (name.slice(0, 7) === "Array.<") { // get the part inside angle brackets like in Array 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'; });