SlexAxton/messageformat.js — pull request #73 by eemeli (2014-10-07 19:51:12+00:00)
Title: Simplify api

This builds on #68 (hence the high number of commits; really this is just two on top of #68), and is actually a consequence of getting my head stuck in the code when thinking about proper names for things. Overall, it should make it much easier to use messageformat.js from other libraries.

Currently, `MessageFormat.prototype` exposes the following methods: `parse`, `precompile`, `compile`, and `precompileObject`. That's really three too many, as we could just have `compile`. That is, assuming that if we're wanting to generate code that we can store and run later, the simplicity of using `compile().toString()` is worth the extra processing required to use a function as an intermediary.

So here I've:
* Moved `MessageFormat.prototype.parse` to be `MessageFormat._parse` — it only accesses the `mparser` and the input `message`, so there's no reason for it to be in the prototype.
* Renamed `MessageFormat.prototype.precompile` as `MessageFormat.prototype._precompile` and simplified its structure so it calls itself, rather than the internal `interpMFP` function.
* removed `MessageFormat.prototype.precompileObject`
* added functionality to `MessageFormat.prototype.compile`, such that it now takes two parameters, as follows:
```js
messages = "string" || { "key0": "string0", "key1": "string1", ... } || {
"ns0": {
"key0": "string0",
"key1": "string1",
...
},
"ns1": {
"key0": "string0",
"key1": "string1",
...
},
...
}

opt = null || {
"locale": null || {
"ns0": "lc0" || ["lc0", ...],
"ns1": "lc1" || ["lc1", ...],
...
},
"global": null || "module.exports" || "exports" || "i18n" || ...
}
```

If `messages` is a string, `compile()` works exactly as before, and ignores `opt`. If it's an object, however, the function that's returned by `compile()` depends on the value of `opt.global`:
```js
var s = 'var\n' + this.runtime.toString() + ';\n\n';
switch (opt.global || '') {
case 'exports':
var o = [];
for (var k in r) o.push('exports[' + JSON.stringify(k) + '] = ' + stringify(r[k]));
return new Function(s + o.join(';\n'));
case 'module.exports':
return new Function(s + 'module.exports = ' + stringify(r));
case '':
return new Function(s + 'return ' + stringify(r));
default:
return new Function('G', s + 'G[' + JSON.stringify(opt.global) + '] = ' + stringify(r));
}
```
Where `r` is an object with a matching structure to the input `messages`, but with each string replaced by the string representation of its generated function, and `stringify()` is a function that turns it into a JavaScript code representation.

Note that in these non-simple cases, the runtime functions are included within the returned function, so doing a `toString` results in fully independent executable code, unlike with the single-message case. Which is a large part of the point of the excercise, to be able to simplify what e.g. `bin/messageformat.js` needs to do.

In fact, with the `compile()` from this PR the only parts of `bin/messageformat.js` that aren't about parsing input options, logging or error checking, are roughly analogous to the following:
```js
// lc: array of locales to include
// global: name of global variable for output, or e.g. 'module.exports' or 'i18n'
// dir: working directory
// in_files: array of files to parse
// out_file: file to write to, or console if false
function parseMF(lc, global, dir, in_files, out_file) {
var mf = new MessageFormat(lc[0]), messages = {},
opt = { global: global, locale: {} };
lc.forEach(function(l){ MessageFormat.getPluralFunc(l); });
in_files.forEach(function(file){
var path = Path.join(dir, file),
ns = file.replace(/\.[^.]*$/, '').replace(/\\/g, '/');
messages[ns] = JSON.parse(fs.readFileSync(path));
file.split(/[.\/]+/).reverse().some(function(p){
if (p in MessageFormat.plurals) { opt.locale[ns] = p; return true; }
});
});
var str = '(' + mf.compile(messages, opt).toString() + '\n)(this);';
if (!out_file) console.log(str);
else fs.writeFileSync(Path.join(dir, out_file), str);
}
```