'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var globby = require('globby');
var path = require('path');
var fs = require('fs');
var async = require('async');
var YAML = require('js-yaml');
var mkdirp = require('mkdirp');
var debug = require('depurar')('locutus');
var indentString = require('indent-string');
var _ = require('lodash');
var esprima = require('esprima');
var Util = function () {
function Util(argv) {
_classCallCheck(this, Util);
if (!argv) {
argv = [];
}
this.__src = path.dirname(__dirname);
this.__root = path.dirname(path.dirname(__dirname));
this.__test = path.dirname(path.dirname(__dirname)) + '/test';
this.globals = {};
this.pattern = [this.__src + '/**/**/*.js', '!**/index.js', '!**/_util/**'];
this.concurrency = 8;
this.authorKeys = ['original by', 'improved by', 'reimplemented by', 'parts by', 'bugfixed by', 'revised by', 'input by'];
this.langDefaults = {
c: {
order: 1,
function_title_template: '[language]\'s [category].[function] in JavaScript',
human: 'C',
packageType: 'header file',
inspiration_urls: ['the C math.h documentation', 'the C math.h source'],
function_description_template: 'Here’s what our current JavaScript equivalent to [language]\'s [function] found in the [category].h header file looks like.'
},
golang: {
order: 2,
function_title_template: '[language]\'s [category].[function] in JavaScript',
human: 'Go',
packageType: 'package',
inspiration_urls: ['Go strings documentation', 'Go strings source', 'Go strings examples source', 'GopherJS'],
function_description_template: 'Here’s what our current JavaScript equivalent to [language]\'s [category].[function] looks like.'
},
python: {
order: 3,
function_title_template: '[language]\'s [category].[function] in JavaScript',
human: 'Python',
packageType: 'module',
inspiration_urls: ['the Python 3 standard library string page'],
function_description_template: 'Here’s what our current JavaScript equivalent to [language]\'s [category].[function] looks like.'
},
ruby: {
order: 4,
function_title_template: '[language]\'s [category].[function] in JavaScript',
human: 'Ruby',
packageType: 'module',
inspiration_urls: ['the Ruby core documentation'],
function_description_template: 'Here’s what our current JavaScript equivalent to [language]\'s [category].[function] looks like.'
},
php: {
order: 5,
function_title_template: '[language]\'s [function] in JavaScript',
human: 'PHP',
packageType: 'extension',
inspiration_urls: ['the PHP string documentation', 'the PHP string source', 'a PHP str_pad test'],
function_description_template: 'Here’s what our current JavaScript equivalent to [language]\'s [function] looks like.',
alias: ['/categories/', '/categories/array/', '/categories/bc/', '/categories/ctype/', '/categories/datetime/', '/categories/exec/', '/categories/filesystem/', '/categories/funchand/', '/categories/i18n/', '/categories/index/', '/categories/info/', '/categories/json/', '/categories/math/', '/categories/misc/', '/categories/net/', '/categories/network/', '/categories/pcre/', '/categories/strings/', '/categories/url/', '/categories/var/', '/categories/xdiff/', '/categories/xml/', '/functions/index/', '/functions/', '/packages/', '/packages/index/']
}
};
this.allowSkip = argv.indexOf('--noskip') === -1;
this._reindexBuffer = {};
this._injectwebBuffer = {};
}
_createClass(Util, [{
key: 'injectweb',
value: function injectweb(cb) {
var self = this;
this._runFunctionOnAll(this._injectwebOne, function (err) {
if (err) {
return cb(err);
}
for (var indexHtml in self._injectwebBuffer) {
debug('writing: ' + indexHtml);
fs.writeFileSync(indexHtml, self._injectwebBuffer[indexHtml], 'utf-8');
}
});
}
}, {
key: 'reindex',
value: function reindex(cb) {
var self = this;
self._reindexBuffer = {};
self._runFunctionOnAll(self._reindexOne, function (err) {
if (err) {
return cb(err);
}
for (var indexJs in self._reindexBuffer) {
var requires = self._reindexBuffer[indexJs];
requires.sort();
debug('writing: ' + indexJs);
fs.writeFileSync(indexJs, requires.join('\n') + '\n', 'utf-8');
}
});
}
}, {
key: 'writetests',
value: function writetests(cb) {
this._runFunctionOnAll(this._writetestOne, cb);
}
}, {
key: '_runFunctionOnAll',
value: function _runFunctionOnAll(runFunc, cb) {
var self = this;
var q = async.queue(function (fullpath, callback) {
self._load.bind(self, fullpath, {}, function (err, params) {
if (err) {
return callback(err);
}
runFunc.bind(self, params, callback)();
})();
}, self.concurrency);
debug({
pattern: self.pattern
});
var files = globby.sync(self.pattern);
q.push(files);
q.drain = cb;
}
}, {
key: '_reindexOne',
value: function _reindexOne(params, cb) {
var fullpath = this.__src + '/' + params.filepath;
var dir = path.dirname(fullpath);
var basefile = path.basename(fullpath, '.js');
var indexJs = dir + '/index.js';
var module = basefile;
if (basefile === 'Index2') {
module = 'Index';
}
if (!this._reindexBuffer[indexJs]) {
this._reindexBuffer[indexJs] = [];
}
var line = 'module.exports[\'' + module + '\'] = require(\'./' + basefile + '\')';
this._reindexBuffer[indexJs].push(line);
return cb(null);
}
}, {
key: '_injectwebOne',
value: function _injectwebOne(params, cb) {
var authors = {};
this.authorKeys.forEach(function (key) {
if (params.headKeys[key]) {
authors[key] = _.flattenDeep(params.headKeys[key]);
}
});
var langPath = [this.__root, '/website/source/', params.language].join('');
var langIndexPath = langPath + '/index.html';
var catPath = langPath + '/' + params.category;
var catIndexPath = catPath + '/' + 'index.html';
var funcPath = catPath + '/' + params.func_name + '.html';
if (!this._injectwebBuffer[langIndexPath]) {
var langTitle = '';
langTitle += this.langDefaults[params.language].human + ' ';
langTitle += this.langDefaults[params.language].packageType + 's ';
langTitle += ' in JavaScript';
var langData = Object.assign({}, this.langDefaults[params.language], {
warning: 'This file is auto generated by `npm run web:inject`, do not edit by hand',
type: 'language',
layout: 'language',
language: params.language,
title: langTitle
});
this._injectwebBuffer[langIndexPath] = '---' + '\n' + YAML.safeDump(langData).trim() + '\n' + '---' + '\n';
}
if (!this._injectwebBuffer[catIndexPath]) {
var catTitle = '';
catTitle += this.langDefaults[params.language].human + '\'s ';
catTitle += params.category + ' ';
catTitle += this.langDefaults[params.language].packageType + ' ';
catTitle += ' in JavaScript';
var catData = {
warning: 'This file is auto generated by `npm run web:inject`, do not edit by hand',
type: 'category',
layout: 'category',
language: params.language,
category: params.category,
title: catTitle
};
this._injectwebBuffer[catIndexPath] = '---' + '\n' + YAML.safeDump(catData).trim() + '\n' + '---' + '\n';
}
var functionTitle = this.langDefaults[params.language].function_title_template.replace(/\[language]/g, this.langDefaults[params.language].human).replace(/\[category]/g, params.category).replace(/\[function]/g, params.func_name).replace(/\[functiondashed]/g, params.func_name.replace(/_/g, '-'));
var functionDescription = this.langDefaults[params.language].function_description_template.replace(/\[language]/g, this.langDefaults[params.language].human).replace(/\[category]/g, params.category).replace(/\[function]/g, params.func_name).replace(/\[functiondashed]/g, params.func_name.replace(/_/g, '-'));
var funcData = {
warning: 'This file is auto generated by `npm run web:inject`, do not edit by hand',
examples: (params.headKeys.example || []).map(function (lines, i) {
return lines.join('\n');
}),
estarget: (params.headKeys.estarget || []).map(function (lines, i) {
return lines.join('\n');
}).join('\n').trim() || 'es5',
returns: (params.headKeys.returns || []).map(function (lines, i) {
return lines.join('\n');
}),
dependencies: [],
authors: authors || {},
notes: (params.headKeys.note || []).map(function (lines, i) {
return lines.join('\n');
}),
type: 'function',
layout: 'function',
title: functionTitle,
description: functionDescription,
function: params.func_name,
category: params.category,
language: params.language,
permalink: params.language + '/' + params.category + '/' + params.func_name + '/',
alias: ['/functions/' + params.language + '/' + params.func_name + '/', '/functions/' + params.category + '/' + params.func_name + '/', '/' + params.language + '/' + params.func_name + '/']
};
if (params.language === 'php') {
funcData.alias.push('/functions/' + params.func_name + '/');
}
var buf = '---' + '\n' + YAML.safeDump(funcData).trim() + '\n' + '---' + '\n';
buf += '{% codeblock lang:javascript %}' + params.code + '{% endcodeblock %}';
mkdirp(path.dirname(funcPath), function (err) {
if (err) {
throw new Error('Could not mkdir for ' + funcPath + '. ' + err);
}
fs.writeFile(funcPath, buf, 'utf-8', cb);
});
}
}, {
key: '_addRequire',
value: function _addRequire(name, relativeSrcForTest) {
return ['var ', name, ' = require(\'', relativeSrcForTest, '\') // eslint-disable-line no-unused-vars,camelcase'].join('');
}
}, {
key: '_writetestOne',
value: function _writetestOne(params, cb) {
var self = this;
if (!params.func_name) {
throw new Error('No func_name in ' + JSON.stringify(params));
}
if (!params.headKeys) {
throw new Error('No headKeys in ' + params.func_name);
}
if (!params.headKeys.example) {
throw new Error('No example in ' + params.func_name);
}
var basename = path.basename(params.filepath);
var subdir = path.dirname(params.filepath);
var testpath = this.__test + '/languages/' + subdir + '/test-' + basename;
var testdir = path.dirname(testpath);
var relativeSrcForTestDir = path.relative(testdir, self.__src);
var relativeTestFileForRoot = path.relative(self.__root, testpath);
// console.log(relativeSrcForTestDir)
// process.exit(1)
var testProps = '';
if (params.headKeys.test) {
testProps = params.headKeys.test[0][0];
}
var describeSkip = '';
if (self.allowSkip && testProps.indexOf('skip-all') !== -1) {
describeSkip = '.skip';
}
var codez = [];
codez.push('// warning: This file is auto generated by `npm run build:tests`');
codez.push('// Do not edit by hand!');
// Add globals
for (var global in self.globals) {
codez.push('var ' + global + ' = ' + self.globals[global]);
}
// Set timezone for testing dates
// Not ideal: http://stackoverflow.com/questions/8083410/how-to-set-default-timezone-in-node-js
codez.push('process.env.TZ = \'UTC\'');
codez.push('var ' + 'expect' + ' = require(\'chai\').expect');
// Add language-wide dependencies
// @todo: It would be great if we could remove this
if (params.language === 'php') {
codez.push(self._addRequire('ini_set', relativeSrcForTestDir + '/' + 'php/info/ini_set'));
codez.push(self._addRequire('ini_get', relativeSrcForTestDir + '/' + 'php/info/ini_get'));
if (params.func_name === 'localeconv') {
codez.push(self._addRequire('setlocale', relativeSrcForTestDir + '/' + 'php/strings/setlocale'));
}
if (params.func_name === 'i18n_loc_get_default') {
codez.push(self._addRequire('i18n_loc_set_default', relativeSrcForTestDir + '/' + 'php/i18n/i18n_loc_set_default'));
}
}
// Add the main function to test
codez.push(self._addRequire(params.func_name, relativeSrcForTestDir + '/' + params.filepath));
codez.push('');
codez.push(['describe', describeSkip, '(\'src/', params.filepath, ' (tested in ', relativeTestFileForRoot, ')\', function () {'].join(''));
// Run each example
for (var i in params.headKeys.example) {
if (!params.headKeys.returns[i] || !params.headKeys.returns[i].length) {
throw new Error('There is no return for example ' + i, test, params);
}
var humanIndex = parseInt(i, 10) + 1;
var itSkip = '';
if (self.allowSkip && testProps.indexOf('skip-' + humanIndex) !== -1) {
itSkip = '.skip';
}
codez.push([' it', itSkip, '(\'should pass example ', humanIndex, '\', function (done) {'].join(''));
var body = [];
var testExpected = params.headKeys.returns[i].join('\n');
body.push('var expected = ' + testExpected);
// Execute line by line (see date.js why)
// We need result be the last result of the example code
for (var j in params.headKeys.example[i]) {
if (parseInt(j, 10) === params.headKeys.example[i].length - 1) {
// last action gets saved
body.push('var result = ' + params.headKeys.example[i][j].replace('var $result = ', ''));
} else {
body.push(params.headKeys.example[i][j]);
}
}
body.push('expect(result).to.deep.equal(expected)');
body.push('done()');
codez.push(indentString(body.join('\n'), ' ', 4));
codez.push(' })');
}
codez.push('})');
codez.push('');
var code = codez.join('\n');
// Write to disk
mkdirp(testdir, function (err) {
if (err) {
throw new Error(err);
}
debug('writing: ' + testpath);
fs.writeFile(testpath, code, 'utf-8', cb);
});
}
// Environment-specific file opener. function name needs to
// be translated to code. The difficulty is in finding the
// category.
}, {
key: '_opener',
value: function _opener(fileOrName, requesterParams, cb) {
var self = this;
var pattern;
var language = requesterParams.language || '*';
if (path.basename(fileOrName, '.js').indexOf('.') !== -1) {
// periods in the basename, like: unicode.utf8.RuneCountInString or strings.sprintf
pattern = self.__src + '/' + language + '/' + fileOrName.replace(/\./g, '/') + '.js';
} else if (fileOrName.indexOf('/') === -1) {
// no slashes, like: sprintf
pattern = self.__src + '/' + language + '/*/' + fileOrName + '.js';
} else if (fileOrName.substr(0, 1) === '/') {
// absolute path, like: /Users/john/code/locutus/php/strings/sprintf.js
pattern = fileOrName;
} else {
// relative path, like: php/strings/sprintf.js
pattern = self.__src + '/' + fileOrName;
}
pattern = pattern.replace('golang/strings/Index.js', 'golang/strings/Index2.js');
debug('loading: ' + pattern);
var files = globby.sync(pattern, {});
if (files.length !== 1) {
var msg = 'Found ' + files.length + ' occurances of ' + fileOrName + ' via pattern: ' + pattern;
return cb(new Error(msg));
}
var filepath = files[0];
if (path.basename(filepath) === 'index.js') {
return cb(null);
}
if (!filepath) {
return cb(new Error('Could not find ' + pattern));
}
fs.readFile(filepath, 'utf-8', function (err, code) {
if (err) {
return cb(new Error('Error while opening ' + filepath + '. ' + err));
}
return cb(null, filepath, code);
});
}
}, {
key: '_load',
value: function _load(fileOrName, requesterParams, cb) {
var self = this;
self._opener(fileOrName, requesterParams, function (err, fullpath, code) {
if (err) {
return cb(err);
}
var filepath = path.relative(self.__src, fullpath);
self._parse(filepath, code, cb);
});
}
}, {
key: '_findDependencies',
value: function _findDependencies(fileOrName, requesterParams, dependencies, cb) {
var self = this;
if (!requesterParams.headKeys['depends on'] || !requesterParams.headKeys['depends on'].length) {
if (cb) {
cb(null, {});
}
return;
}
var i;
var depCodePath;
var loaded = 0;
for (i in requesterParams.headKeys['depends on']) {
depCodePath = requesterParams.headKeys['depends on'][i][0];
self._load(depCodePath, requesterParams, function (err, params) {
if (err) {
return cb(err);
}
dependencies[depCodePath] = params;
self._findDependencies(depCodePath, params, dependencies);
if (cb && ++loaded === requesterParams.headKeys['depends on'].length) {
cb(null, dependencies);
}
});
}
}
}, {
key: '_parse',
value: function _parse(filepath, code, cb) {
if (!code) {
return cb(new Error('Unable to parse ' + filepath + '. Received no code'));
}
if (filepath.indexOf('/') === -1) {
return cb(new Error('Parse only accepts relative filepaths. Received: \'' + filepath + '\''));
}
var parts = filepath.split('/');
var language = parts.shift();
var codepath = parts.join('.');
var name = parts.pop();
var category = parts.join('.');
var ast = esprima.parseScript(code, { comment: true, loc: true, range: true });
// find module.exports in the code
var moduleExports = ast.body.filter(function (node) {
try {
var leftArg = node.expression.left;
var rightArg = node.expression.right;
return leftArg.object.name === 'module' && leftArg.property.name === 'exports' && rightArg.type === 'FunctionExpression' && rightArg.id.type === 'Identifier' && !!rightArg.id.name;
} catch (err) {
return false;
}
});
// if file contains more than one export, fail
if (moduleExports.length !== 1) {
return cb(Error('File ' + filepath + ' is allowed to contain exactly one module.exports'));
}
// get the only export
var exp = moduleExports[0];
// look for function name and param list
var funcName = exp.expression.right.id.name;
var funcParams = exp.expression.right.params.map(function (p) {
return p.name;
});
// remember the lines where the function is defined
var funcLoc = exp.expression.right.loc;
// since comments are not included in the AST
// but are offered in ast.comments
// remember the location of first function body statement/expression
var firstFuncBodyElementLoc = exp.expression.right.body.body[0].loc;
// get all line comments which are located between function signature definition
// and first function body element
var headComments = ast.comments.filter(function (c) {
return c.type === 'Line' && c.loc.start.line >= funcLoc.start.line && c.loc.end.line <= firstFuncBodyElementLoc.start.line;
}).map(function (c) {
return c.value.trim();
});
if (headComments.length === 0) {
var msg = 'Unable to parse ' + filepath + '. Did not find any comments in function definition';
return cb(new Error(msg));
}
var headKeys = this._headKeys(headComments);
var params = {
headKeys: headKeys,
name: name,
filepath: filepath,
codepath: codepath,
// code: code,
language: language,
category: category,
func_name: funcName,
func_arguments: funcParams
};
this._findDependencies(filepath, params, {}, function (err, dependencies) {
if (err) {
return cb(err);
}
params.dependencies = dependencies;
return cb(null, params);
});
}
}, {
key: '_headKeys',
value: function _headKeys(headLines) {
var i;
var keys = {};
var match = [];
var dmatch = [];
var key = '';
var val = '';
var num = 0;
for (i in headLines) {
if (!(match = headLines[i].match(/^\s*\W?\s*([a-z 0-9]+)\s*:\s*(.*)\s*$/))) {
continue;
}
key = match[1];
val = match[2];
if (dmatch = key.match(/^(\w+)\s+(\d+)$/)) {
// Things like examples and notes can be grouped
key = dmatch[1];
num = dmatch[2] - 1;
} else {
num = 0;
}
if (!keys[key]) {
keys[key] = [];
}
if (!keys[key][num]) {
keys[key][num] = [];
}
keys[key][num].push(val);
}
return keys;
}
}]);
return Util;
}();
module.exports = Util;
//# sourceMappingURL=util.js.map