UNPKG

uh

Version:

Community-controlled cheat sheets for every coder.

326 lines (290 loc) 9 kB
"use strict"; /** * Module dependencies. */ const _ = require('lodash'); const lev = require('levenshtein'); const request = require('request'); const util = { /** * Handles tabbed auto-completion based on * the doc index. Works perfectly. Looks ugly * as hell. Hey: It works. * * @param {String} text * @param {Integer} iteration * @param {Object} index * @return {String or Array} * @api public */ autocomplete: function(text, iteration, index, matchFn) { let commands = util.command.prepare(text, {}, index); let lastWord = String(commands[commands.length-1]).trim(); let otherWords = commands.slice(0, commands.length-1); let levels = 0; const possibilities = util.traverseIndex(_.clone(commands), index, function(arr, idx){ levels++; }); const match = matchFn(String(lastWord).trim(), possibilities); const exactMatch = (possibilities.indexOf(lastWord) > -1); if (match && levels !== otherWords.length + 1) { let space = (possibilities.indexOf(String(match).trim()) > -1) ? ' ' : ''; let result = String(otherWords.join(' ') + ' ' + match).trim() + space; return result; } else { let space = (levels === otherWords.length + 1) ? ' ' : ''; let original = commands.join(' ') + space; if (iteration > 1 && possibilities.length > 1) { return possibilities; } else if (iteration > 1 && possibilities.length === 1 && (otherWords.length !== levels)) { let result = original + possibilities[0] + ' '; return result; } else { return original; } } }, /** * Takes an existing array of words * and matches it against the index. * Whenever a word can be standardized * with the index, such as on casing, * it cleans up the word and returns it. * For example, * ['the', 'veryquick ', 'fox'] will become * ['the', 'veryQuick', 'fox'] * based on the index. * * @param {Array} arr * @param {Object} idx * @param {Function} each * @param {Array} results * @return {Array} results * @api public */ standardizeAgainstIndex(arr, idx, each, results) { results = results || []; each = each || function(){} let word = arr.shift(); let wordProper = void 0; // Use a levenshtein distance algorithm // to look for appriximate matches. If we feel // safe enough, automagically adopt the match. if (String(word).trim().length > 0) { let res = util.levenshteinCompare(word, idx); word = (res.distance === 0) ? res.key : (res.distance === 1 && res.difference > 3) ? res.key : (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? res.key : word; } if (idx[word]) { each(arr, idx[word]); results.push(word); return util.standardizeAgainstIndex(arr, idx[word], each, results); } else { if (word) { results.push(word); } return results; } }, levenshteinCompare(word, obj) { let keys = Object.keys(obj); let results = { firstKey: void 0, firstDistance: 1000, secondKey: void 0, secondDistance: 1000 } let first = { key: void 0, distance: 1000 }; let second = { key: void 0, distance: 1000 }; for (let i = 0; i < keys.length; ++i) { if (keys[i] === 'index') { continue; } let distance = lev(String(word).trim().toLowerCase(), String(keys[i]).trim().toLowerCase()); if (distance < results.firstDistance) { results.firstDistance = distance; results.firstKey = keys[i]; } else if (distance < results.secondDistance) { results.secondDistance = distance; results.secondKey = keys[i]; } } return ({ key: results.firstKey, distance: results.firstDistance, difference: results.secondDistance - results.firstDistance }) }, /** * Takes an existing array of words * and matches it against the index, returning * all available commands for the next * command, having matched x commands so far. * For example, * ['the', 'quick', 'brown'] will return * ['fox', 'dog', 'goat'] * based on the index, as the index has * three .md files in the `brown` folder. * * @param {Array} arr * @param {Object} idx * @param {Function} each * @return {Array} results * @api public */ traverseIndex(arr, idx, each) { each = each || function(){} let word = arr.shift(); if (idx[word]) { each(arr, idx[word]); return util.traverseIndex(arr, idx[word], each); } else { let items = []; for (let item in idx) { if (idx.hasOwnProperty(item) && String(item).slice(0, 3) !== '___') { var match = (String(word || '').toLowerCase() === String(item).slice(0, String(word || '').length).toLowerCase()); if (match) { items.push(item); } } } return items; } }, command: { /** * Takes a raw string entered by the user, * sanitizes it and returns it as an array * of words. * * @param {String} str * @return {Array} * @api public */ prepare(str, options, index) { //console.log(options) options = options || {} let all = []; let commands = (_.isArray(str)) ? str : String(str).trim().split(' '); for (let i = 0; i < commands.length; ++i) { var parts = commands[i].split('.'); for (let j = 0; j < parts.length; ++j) { let word = String(parts[j]) .trim() .replace(/\)/g, '') .replace(/\(/g, '') .replace(/\;/g, ''); all.push(word); } } let standardized = util.standardizeAgainstIndex(_.clone(all), index); return standardized; }, /** * Takes a raw string and converts it into * a ready URL root to try loading. * * @param {String} str * @return {String} * @api public */ buildPath(str, options, index) { let all = util.command.prepare(str, options, index); let indexObject = util.command.getIndex(_.clone(all), index); var response = { path: void 0, exists: false, suggestions: void 0, index: void 0 } if (!indexObject) { response.exists = false; } else { if (_.isArray(indexObject)) { response.suggestions = indexObject; } else { response.index = indexObject; response.exists = true; } } let path = all.join('/'); response.path = path; return response; }, /** * Returns the deepest index object * for a given array of commands. * * @param {Array} arr * @param {Object} idx * @param {Array} results * @return {Boolean} valid * @api public */ getIndex(arr, idx) { let word = arr.shift(); if (idx[word]) { return util.command.getIndex(arr, idx[word]); } else { if (!word) { if (idx['index']) { if (idx['index'] !== true && _.isObject(idx['index'])) { idx['index'].___isIndexFile = true; } return idx['index']; } else if (idx['___basic']) { return idx; } else { return Object.keys(idx); } } else { return void 0; } } }, /** * Takes the end string of command, * 'splice' in 'js array splice', * reads its index JSON, and compares * these to the passed in options in order * to determine the valid .md structure, i.e. * splice.md, splice.detail.md, splice.install.md, * etc. etc. etc. * * @param {Array} arr * @param {Object} idx * @param {Array} results * @return {Boolean} valid * @api public */ buildExtension(path, index, options) { let result; if (_.isObject(index) && index.___isIndexFile === true) { result = path + '/index.md'; } else if (options.detail && index.___detail) { result = path + '.detail.md'; } else if (options.install && index.___install) { result = path + '.install.md'; } else { result = path + '.md'; } return result; } }, requestMarkdown(path, cb) { request(path, function(err, response, body) { if (!err) { if (body === 'Not Found') { cb(void 0, `Markdown not found:\n${path}`); } else { cb(void 0, `${body}`); } } else { cb(err, ''); } }); } } module.exports = util;