babelute
Version:
Internal Domain Specific (Multi)Modeling javascript framework
424 lines (357 loc) • 15.6 kB
JavaScript
/**
* Babelute Lexicon class and helpers.
* @author Gilles Coomans
* @licence MIT
* @copyright 2016-2017 Gilles Coomans
*/
import assert from 'assert'; // removed in production
import Lexem from '../lexem';
import Babelute from '../babelute';
import FirstLevel from './first-level';
import { addToInitializer, createInitializer } from './initializer';
/**
* Lexicons dico : where to store public (registered) lexicon
* @type {Object}
* @private
*/
const lexicons = {};
/**
* Lexicon class : helpers to store and manage DSL's API.
*
* A __Lexicon__ is just an object aimed to handle, store and construct easily a DSL (its lexicon - i.e. the bunch of words that compose it)
* and its related Atomic/FirstLevel/SecondLevel Babelute subclasses, and their initializers.
*
* One DSL = One lexicon.
*
* A lexicon could extend another lexicon to manage dialects.
*
* You should never use frontaly the constructor (aka never use new Lexicon in your app). Use createLexicon or createDialect in place.
*
* @public
*/
class Lexicon {
/**
* @param {string} name the lexicon name
* @param {?Lexicon} parent an optional parent lexicon to be extended here
*/
constructor(name, parent) {
assert(typeof name === 'string' && name.length, 'Lexicon constructor need a valid name as first argument'); // all assertions will be removed in production
assert(!parent || parent instanceof Lexicon, 'Lexicon constructor second (optional) argument should be another Lexicon that will be used as parent');
/**
* the parent lexicon (if any)
* @type {Lexicon}
* @public
*/
this.parent = parent;
parent = parent || {};
/**
* the lexicon's name
* @type {String}
*/
this.name = name;
// the three APIs :
/**
* interpretable sentences API (finally always made from syntactic/semantic atoms (aka last level))
* @type {Babelute}
* @protected
*/
this.Atomic = initClass(parent.Atomic || Babelute);
/**
* "document" sentences API (first level : aka all methods has been replaced by fake atomic methods)
* @type {Babelute}
* @protected
*/
this.FirstLevel = initClass(parent.FirstLevel || FirstLevel);
/**
* AST-provider API aka the whole tree between first level and last level. Never use it directly : its used under the hood by {@link developOneLevel} method.
* @type {Babelute}
* @protected
*/
this.SecondLevel = Babelute.extends(parent.SecondLevel || Babelute);
/**
* the secondLevel instance
* @type {Babelute}
* @protected
*/
this.secondLevel = new this.SecondLevel();
if (parent.Atomic)
Object.keys(parent.Atomic.initializer)
.forEach((key) => {
addToInitializer(this.Atomic.Initializer, key);
addToInitializer(this.FirstLevel.Initializer, key);
});
}
/**
* add atomic lexem (atoms) to lexicon
* @param {string[]} atomsArray array of atoms name (as string)
* @return {Lexicon} the lexicon itself
*/
addAtoms(atomsArray) {
assert(Array.isArray(atomsArray), 'lexicon.addAtoms(...) need an array as first argument');
atomsArray.forEach((name) => addAtom(this, name));
return this;
}
/**
* add compounds lexems to lexicon
* @param {Function} producer a function that take a babelute initializer as argument and that return an object containing methods (lexems) to add to lexicon
* @return {Lexicon} the lexicon itself
*/
addCompounds(producer) {
assert(typeof producer === 'function', 'lexicon.addCompounds(...) need a function (that return an object containing dsl methods) as first argument');
// Atomic API is produced with Atomic initializer
const atomicMethods = producer(this.Atomic.initializer, false);
assert(atomicMethods && typeof atomicMethods === 'object', 'lexicon.addCompounds(function(){...}) need a function that return an object containing dsl methods to add');
for (let i in atomicMethods)
this.Atomic.prototype[i] = atomicMethods[i];
// SecondLevel API is simply produced with the related FirstLevel initializer.
// (so same producer method, same api, but different handler for inner composition)
// is the only thing to do to gain capability to handle full AST. (see docs)
const secondLevelCompounds = producer(this.FirstLevel.initializer, true);
for (let j in secondLevelCompounds)
this.SecondLevel.prototype[j] = secondLevelCompounds[j];
Object.keys(atomicMethods)
.forEach((key) => {
this.FirstLevel.prototype[key] = FirstLevel.getFirstLevelMethod(this.name, key);
addToInitializer(this.Atomic.Initializer, key);
addToInitializer(this.FirstLevel.Initializer, key);
});
return this;
}
/**
* add aliases lexems to lexicon (aliases are like shortcuts : they are added as this to Atomic, FirstLevel and SecondLevel API)
* @param {Object} methods an object containing methods (lexems) to add to lexicon
* @return {Lexicon} the lexicon itself
*/
addAliases(producer) {
const producerType = typeof producer;
assert(producerType === 'function' || producerType === 'object', 'lexicon.addAliases(...) need a function (that return an object containing aliases methods) as first argument');
const methods = producerType === 'function' ? producer() : producer;
assert(methods && typeof methods === 'object', 'lexicon.addAliases(function(){...}) need a function that return an object containing aliases methods to add');
Object.keys(methods)
.forEach((key) => {
this.Atomic.prototype[key] = this.FirstLevel.prototype[key] = this.SecondLevel.prototype[key] = methods[key];
addToInitializer(this.Atomic.Initializer, key);
addToInitializer(this.FirstLevel.Initializer, key);
});
return this;
}
/**
* @protected
*/
use(babelute, name, args, firstLevel) {
assert(babelute && babelute.__babelute__, 'lexicon.use(...) need a babelute intance as first argument');
assert(typeof name === 'string', 'lexicon.use(...) need a string (a method name) as second argument');
const instance = firstLevel ? this.FirstLevel.instance : this.Atomic.instance;
if (!instance[name])
throw new Error(`Babelute (${ this.name }) : method not found : ${ name }`);
instance[name].apply(babelute, args);
}
/**
* return lexicon's initializer instance. (atomic or firstlevel depending on argument)
* @public
* @param {Boolean} firstLevel true if you want firstLevel initializer, false overwise.
* @return {Initializer} the needed initializer instance
*/
initializer(firstLevel) {
return firstLevel ? this.FirstLevel.initializer : this.Atomic.initializer;
}
/**
* Create a dialect from this lexicon. a dialect is also a Lexicon.
* @param {String} name the new lexicon name
* @return {Lexicon} the new Lexicon that inherit from this one
*/
createDialect(name) {
assert(typeof name === 'string', 'lexicon.createDialect(...) need a string (the new lexicon name) as first argument');
return new Lexicon(name, this);
}
}
/**
* Add syntactical atom lexem to lexicon (actually to inner classes that reflect API). A syntactical Atom method is a function that only add one lexem.
* @private
*/
function addAtom(lexicon, name) {
assert(lexicon instanceof Lexicon, 'Lexicon addAtom(...) first argument should be a Lexicon where add syntactical atom');
assert(typeof name === 'string', 'Lexicon addAtom(...) need a string (a method name) as second argument');
lexicon.Atomic.prototype[name] = lexicon.FirstLevel.prototype[name] = lexicon.SecondLevel.prototype[name] = FirstLevel.getFirstLevelMethod(lexicon.name, name);
addToInitializer(lexicon.Atomic.Initializer, name);
addToInitializer(lexicon.FirstLevel.Initializer, name);
}
/**
* babelute lexicon's Classes initialisation
* @private
*/
function initClass(BaseClass) {
const Class = Babelute.extends(BaseClass);
createInitializer(Class, BaseClass.Initializer);
Class.instance = new Class();
return Class;
}
/**
* Way to create lexicon instances
* @public
* @param {string} name the name of the lexicon
* @param {Lexicon} parent a lexicon instance as parent for this one (optional)
* @return {Lexicon} a lexicon instance
*/
function createLexicon(name, parent = null) {
return new Lexicon(name, parent);
}
/**
* getLexicon registred lexicon by name
*
* @param {string} lexiconName the lexicon's name
* @return {Lexicon} the lexicon
* @throws {Error} If lexicon not found with lexiconName
*/
function getLexicon(lexiconName) {
assert(typeof lexiconName === 'string', 'Lexicon.getLexicon(...) need a string (a lexicon name) as first argument');
const lexicon = lexicons[lexiconName];
if (!lexicon)
throw new Error('lexicon not found : ' + lexiconName);
return lexicon;
}
/**
* registerLexicon lexicon by name
* @param {Lexicon} lexicon the lexicon instance to registerLexicon
* @param {?string} name lexicon name (optional : if not provided : use the one from lexicon itself)
*/
function registerLexicon(lexicon, name = null) {
assert(lexicon instanceof Lexicon, 'Lexicon.registerLexicon(...) first argument should be a Lexicon');
assert(!name || typeof name === 'string', 'Lexicon.registerLexicon(...) need a string (a lexicon name) as second argument');
lexicons[name || lexicon.name] = lexicon;
}
/*
* _lexicon handeling
*/
// implementation of already declared method in Babelute's proto
Babelute.prototype._lexicon = function(lexiconName) {
assert(typeof lexiconName === 'string', '._lexicon(...) accept only a string (a Lexicon id) as argument');
return new(getLexicon(lexiconName).Atomic)(this._lexems);
};
FirstLevel.prototype._lexicon = function(lexiconName) {
assert(typeof lexiconName === 'string', '._lexicon(...) accept only a string (a Lexicon id) as argument');
return new(getLexicon(lexiconName).FirstLevel)(this._lexems);
};
/*
* translation through lexicon (already delcared in Babelute proto)
* @TODO: translation and each and if
* each :
* .each(collec, handler)
* translated to
* .each(collec, wrap(handler, translationInfos))
*
* ==> should translate automatically output from handler
* if ==> same things : wrap handlers
* ==> while translating : when lexem.name === "each" (or "if") (should always be present in target lexicon)
* ==> apply wrapping
*/
Babelute.prototype._translateLexemsThrough = function(lexicon, firstLevel = false) {
const map = (lexicon instanceof Lexicon) ? null : lexicon;
return this._translateLexems((lexem) => {
if (map)
lexicon = map[lexem.lexicon];
if (!lexicon)
return null;
const args = translateArgs(lexem.args, lexicon, firstLevel);
const b = new (firstLevel ? lexicon.FirstLevel : lexicon.Atomic)();
return b[lexem.name] && b[lexem.name](...args);
});
};
function translateArgs(args, lexicon, firstLevel){
const result = [];
for(let i = 0, len = args.length; i < len; ++i)
if(args[i] && args[i].__babelute__)
result.push(args[i]._translateLexemsThrough(lexicon, firstLevel));
else
result.push(args[i]);
return result;
}
/**
* _use handeling
*/
// implementation of already declared method in Babelute's proto
Babelute.prototype._use = function(babelute /* could be a string in "lexiconName:methodName" format */ , ...args) {
assert(!babelute || typeof babelute === 'string' || babelute.__babelute__);
return babelute ? use(this, babelute, args, false) : this;
};
// implementation of already declared method in Babelute's proto
FirstLevel.prototype._use = function(babelute /* could be a string in "lexiconName:methodName" format */ /*, ...args */ ) {
assert(!babelute || typeof babelute === 'string' || babelute.__babelute__);
return babelute ? use(this, babelute, [].slice.call(arguments, 1), true) : this;
};
function use(self, babelute, args, firstLevel) {
if (typeof babelute === 'string') {
const splitted = babelute.split(':');
getLexicon(splitted[0]).use(self, splitted[1], args, firstLevel);
} else
self._lexems = self._lexems.concat(babelute._lexems);
return self;
}
/**
* return a new babelute from needed lexicon
* @param {string} lexiconName the lexicon from where to take api
* @param {Boolean} asFirstLevel True if it needs to return a FirstLevel instance. False or ommitted : returns an Atomic instance.
* @return {[type]} the babelute instance (either an Atomic or a FirstLevel)
* @throws {Error} If lexicon not found with lexiconName
*/
function init(lexiconName, asFirstLevel) {
if (lexiconName)
return new(getLexicon(lexiconName)[asFirstLevel ? 'FirstLevel' : 'Atomic'])();
else if (asFirstLevel)
return new FirstLevel();
return new Babelute();
}
/**
* develop a FirstLevel compounds-words-lexem through SecondLevel API. It returns the FirstLevel sentence corresponding to lexem's semantic developement.
* @param {Lexem} lexem the lexem to develop
* @param {?Lexicon} lexicon the optional lexicon to use
* @return {FirstLevel} the developed sentence
* @throws {Error} If lexicon not found with lexem.lexicon
* @throws {Error} If method not found in lexicon
*/
function developOneLevel(lexem, lexicon = null) {
assert(lexem && lexem instanceof Lexem, 'lexicon.developOneLevel(...) need a lexem intance as first argument');
assert(lexicon === null || lexicon instanceof Lexicon, 'lexicon.developOneLevel(...) second argument should be null or an instance of Lexicon');
lexicon = lexicon || getLexicon(lexem.lexicon);
assert(lexicon.secondLevel[lexem.name], `lexicon.developOneLevel(...) : lexem\'s name (${lexem.name}) not found in its own referenced lexicon (${ lexem.lexicon })`);
return lexicon.secondLevel[lexem.name].apply(new lexicon.FirstLevel(), lexem.args);
}
/**
* develop a FirstLevel lexem through Atomic API. Return the atomic representation of the lexem (in its own language).
* @param {Lexem} lexem the lexem to develop
* @param {?Lexicon} lexicon the optional lexicon to use
* @return {Babelute} the developed sentence
* @throws {Error} If lexicon not found with lexem.lexicon
* @throws {Error} If method not found in lexicon
*/
function developToAtoms(lexem, lexicon = null) {
assert(lexem && lexem instanceof Lexem, 'lexicon.developToAtoms(...) need a lexem intance as first argument');
assert(lexicon === null || lexicon instanceof Lexicon, 'lexicon.developToAtoms(...) second argument should be null or an instance of Lexicon');
lexicon = lexicon || getLexicon(lexem.lexicon);
assert(lexicon.Atomic.instance[lexem.name], 'lexicon.developToAtoms(...) : lexem\'s name (${lexem.name}) not found in its own referenced lexicon (${ lexem.lexicon })');
return lexicon.Atomic.instance[lexem.name].apply(new lexicon.Atomic(), lexem.args);
}
/**
* Provide Babelute Subclass "initializer" object (the one with all the flattened shortcut api for starting sentences easily)
* @param {string} lexiconName The lexiconName where catch the Babelute Class from where getLexicon or create the initializer object.
* @param {boolean} asFirstLevel true if should return a first-level instance. false to return an atomic instance.
* @return {Object} An initializer object with shortcuted API from lexicon's Atomic prototype
* @throws {Error} If lexicon not found with lexiconName
*/
function initializer(lexiconName, asFirstLevel) {
assert(typeof lexiconName === 'string', 'Babelute.initializer(...) accept only a string (a Lexicon id) as argument');
if (!asFirstLevel)
return getLexicon(lexiconName).Atomic.initializer;
return getLexicon(lexiconName).FirstLevel.initializer;
}
export {
Lexicon,
createLexicon,
getLexicon,
registerLexicon,
init,
initializer,
developOneLevel,
developToAtoms,
lexicons
};