babelute
Version:
Internal Domain Specific (Multi)Modeling javascript framework
204 lines (174 loc) • 7.41 kB
JavaScript
/**
* Babelute core
*
* @author Gilles Coomans
* @licence MIT
* @copyright 2016-2017 Gilles Coomans
*/
import assert from 'assert'; // removed in production
import Lexem from './lexem';
import extend from './utils/extends';
/**
* Babelute subclass(es) instances : for holding array of lexems (i.e. a sentence) written through the DSL's API.
*
* Will be the base class for all DSLs handlers.
*
* Babelute API and lexems Naming Conventions :
*
* - any "meta-language" method (aka any method that handle the sentence it self - appending new lexem, changing current lexicon, sentences translations, ...)
* must start with and underscore : e.g. _append, _lexicon, _if, _each, _eachLexem, _translate...
* - any "pragmatics output related" method should start with a '$' and should be named with followed format : e.g. .$myLexiconToMyOutputType(...)
* - any DSL lexems (so any other "api"'s method) should start with a simple alphabetic char : e.g. .myLexem(), .description(), .title(), ...
*
* @public
*/
export default class Babelute {
/**
* construct a babelute instance
* @param {?Array} lexems array of lexems for init. (only for internal use)
*/
constructor(lexems = null) {
assert(!lexems || Array.isArray(lexems), 'Babelute\'s constructor accept only an array of lexems as argument (optionaly)');
/**
* the array where lexems are stored
* @type {Array}
*/
this._lexems = lexems || [];
/**
* useful marker for fast instanceof replacement (frame/multiple-js-runtime friendly)
* @type {Boolean}
*/
this.__babelute__ = true;
}
/**
* The absolute Babelute atom method : add a lexem to babelute's array
* @public
* @param {String} lexiconName the current lexicon name
* @param {String} name the lexem's name
* @param {Array|arguments} args the lexem's arguments (either an array or maybe directly the arguments object from when lexem is called)
* @return {Babelute} the current Babelute instance
*/
_append(lexiconName, name, args) {
this._lexems.push(new Lexem(lexiconName, name, args));
return this;
}
/**
* conditional sentences concatenation.
*
* Apply modification at sentence writing time (aka the babelute does not contains the _if lexems. _if has immediatly been applied).
*
* @public
* @param {*} condition any value that will be casted to Boolean (!!)
* @param {Babelute} babelute which sentence to insert if !!condition === true
* @param {?Babelute} elseBabelute which sentence to insert if !!condition === false
* @return {Babelute} the current Babelute instance
*/
_if(condition, babelute, elseBabelute = null) {
assert(babelute instanceof Babelute, '._if meta-api need an babelute instance as second argument');
assert(!elseBabelute || elseBabelute instanceof Babelute, '._if meta-api need an babelute instance as third argument (optional)');
if (condition)
this._lexems = this._lexems.concat(babelute._lexems);
else if (elseBabelute)
this._lexems = this._lexems.concat(elseBabelute._lexems);
return this;
}
/**
* For each item from array : execute function and concatenate returned babelute sentence to current one.
* Provided function must return a babelute.
*
* Apply modification at sentence writing time (aka the babelute does not contains the _each lexems. _each has immediatly been applied).
*
* @public
* @param {Array} array the array to iterate on
* @param {Function} func the function to handle each item. it must return a babelute.
* @return {Babelute} the current Babelute instance
*/
_each(array, func) {
assert(!array || Array.isArray(array), '._each meta-api need an array (or iterable with bracket access) as first argument');
assert(typeof func === 'function', '._each meta-api need a function as second argument');
if (array)
array.forEach((item, index) => {
const b = func(item, index);
assert(b instanceof Babelute, '._each need a function that return a babelute');
this._lexems.push.apply(this._lexems, b._lexems);
});
return this;
}
/**
* Use a babelute (another sentence) at this point in the current sentence
* @public
* @param {string|Babelute} babelute Either a string formatted as 'mylexicon:myMethod' (which gives the lexem's method to call), or a Babelute instance (which will be inserted in current sentence)
* @param {?...args} args the optional arguments to use when calling lexem (only if first argument is a string)
* @return {Babelute} the current Babelute instance
* @throws {Error} If lexicon not found (when first arg is string)
* @throws {Error} If method not found in lexicon (when first arg is string)
*/
/* istanbul ignore next */
_use(babelute, ...args) { // eslint-disable-line no-unused-vars
// will be implemented in lexicon
}
/**
* Change current lexicon for next lexems
* @public
* @param {string} lexiconName the lexicon to use
* @return {Babelute} a new Babelute from lexicon (i.e. with lexicon's API)
* @throws {Error} If lexicon not found with lexiconName
*/
/* istanbul ignore next */
_lexicon(lexiconName) { // eslint-disable-line no-unused-vars
// will be implemented in lexicon
}
/**
* transform a sentence through a function. This function must return a new sentence.
* @param {Function} handler a function that receive the current sentence as argument and that return a new sentence.
* @return {Babelute} the new sentence
*/
_transform(handler) {
return handler(this);
}
/**
* translate each lexems
* @param {[type]} handler [description]
* @return {[type]} [description]
*/
_translateLexems(handler) {
return this._transform((sentence) => {
const b = new Babelute();
sentence._lexems.forEach((lexem) => b._use(handler(lexem)));
return b;
});
}
/**
* translate current sentence's lexems through a Lexicon or a map of Lexicon.
*
* @param {Lexicon|Object} lexicon the Lexicon (or a map of Lexicon) to get translation from
* @param {Boolean} firstLevel true if should produce FirstLevel translation (i.e. targetLexicon.FirstLevel). False otherwise.
* @return {Babelute} a new Babelute instance with translated lexems.
*/
/* istanbul ignore next */
_translateLexemsThrough(lexicon, firstLevel = false) { // eslint-disable-line no-unused-vars
// will be implemented in Lexicon
}
// static extends(BaseClass, ...apis) {
// assert(BaseClass === Babelute || (BaseClass.prototype instanceof Babelute), 'Babelute.extends accepts only a Babelute Class (or subclass) as first argument');
// const B = function(...args) {
// BaseClass.apply(this, args);
// };
// B.prototype = Object.create(BaseClass.prototype);
// B.prototype.constructor = B;
// apis.forEach((api) => {
// for (var i in api) B.prototype[i] = api[i];
// });
// // Object.assign seems to bug when used on prototype (not investigate enough : so use plain old for-in syntax)
// // investigation gives : babel make prototype not enumerable
// return B;
// }
}
/**
* Create Babelute subclass
* @param {Babelute} BaseClass the class to be extended
* @param {?Object} api an object containing methods to add to prototype
* @return {Babelute} The subclass
* @throws {AssertionError} (only in dev mode) If BaseClass is not a Babelute Subclass (or Babelute)
*/
Babelute.extends = extend;