From 113084652f3f65de65d6d91c26214b4a3e3aa02f Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sat, 2 May 2020 00:55:14 +0300 Subject: [PATCH] updated local deps... Signed-off-by: Alex A. Naanou --- lib/actions.js | 1874 +++++++++++++++++++++++++++++++++-------------- lib/features.js | 1370 ++++++++++++++++++++++------------ lib/object.js | 625 ++++++++++++---- 3 files changed, 2726 insertions(+), 1143 deletions(-) diff --git a/lib/actions.js b/lib/actions.js index 3bbb79d..c113b2e 100755 --- a/lib/actions.js +++ b/lib/actions.js @@ -11,278 +11,237 @@ var object = require('ig-object') -/*********************************************************************/ - -var args2array = function(a){ return [].slice.call(a) } - -var UNDEFINED = -module.UNDEFINED = ['undefined placeholder'] - - - -/*********************************************************************/ -// Actions -// -// Actions are an extension to the JavaScript object model tailored for -// a set of specific tasks. -// -// Goals: -// - provide a unified mechanism to define and manage user API's for -// use in UI-hooks, keyboard mappings, scripting, ... etc. -// - a means to generate configuration UI's -// - a means to generate documentation -// -// -// The main entities: -// -// Action set -// - an object containing a number of actions, -// - optionally, directly or indirectly inherited from MetaActions -// and/or other action sets, -// - the action handlers are bound relative to it (._action_handlers) -// -// Action -// -// + pre + pre + + post + post + -// Action event handler: o-------x o-------x -// v ^ -// Actions o-------x o-------x -// v ^ -// Root Action o---|---x -// -// - a method, created by Action(..), -// - calls all the shadowed/overloaded actions in the inheritance -// chain in sequence implicitly, -// NOTE: there is no way to prevent an action in the chain from -// running, this is by design, i.e. no way to fully shadow. -// - actions that do not shadow anything are called root actions. -// - returns the action set by default (for call chaining), -// - the base/root action can return any value. -// NOTE: if undefined is returned, it will be replaced by the -// action context/action set. -// NOTE: there is no distinction between root and other actions -// other than that root action's return values are not -// ignored. -// - can consist of two parts: the first is called before the -// shadowed action (pre-callback) and the second after (post-callback). -// - post-callback has access to the return value and can modify it -// but not replace it. -// - can be bound to, a-la an event, calling the handlers when it is -// called, -// -// Action (event) handler -// - a function, -// - can be bound to run before and/or after the action itself, -// - is local to an action set it was bound via, -// - when an action is triggered from an action set, all the pre -// handlers in its inheritance chain will be called before the -// respective actions they are bound to and all the post handlers -// are called directly after. -// - pre handlers are passed the same arguments the original actions -// got when it was called. -// - post action handlers will get the root action result as first -// argument succeeded by the action arguments. -// -// -// -// The action system main protocols: -// -// 1) Documentation generation and introspection (MetaActions) -// -// .toString() -// -> code of original action function -// -// .getDoc() -// .getDoc([, ..]) -// -> dict of action-name, doc -// -// .a.getHandlerDocStr() -// -> formated string of action handlers -// -// .actions -// -> list of action names -// -// .length -// -> number of actions -// -// -// 2) Event-like callbacks for actions (MetaActions, Action) -// -// .on('action', function(){ ... }) -// .on('action.post', function(){ ... }) -// -// .on('action.pre', function(){ ... }) -// -// -// 3) A mechanism to define and extend already defined actions -// This replaces / complements the standard JavaScript overloading -// mechanisms (Action, Actions) -// -// // Actions... -// var X = Actions({ -// m: [function(){ console.log('m') }] -// }) -// var O = Actions(X, { -// m: [function(){ -// console.log('pre') -// return function(res){ -// console.log('post') -// } -// }] -// }) -// -// NOTE: what is done here is similar to calling O.__proto__.m.call(..) -// but is implicit, and not dependant on the original containing -// object name/reference ('O'), thus enabling an action to be -// referenced and called from any object and still chain correctly. -// -// -// -// Secondary action protocols: -// -// 1) A mechanism to manually call the pre/post stages of an action -// -// Pre phase... -// .pre() -// .pre(, [, ..]) -// -> -// -// Post phase... -// .post(, ) -// -> -// -// This is internally used to implement the action call as well as the -// chaining callbacks (see below). -// -// All action protocol details apply. -// -// NOTE: there is not reliable way to call the post phase without first -// calling the pre phase due to how the pre phase is defined (i.e. -// pre phase functions can return post phase functions). -// -// -// 2) A mechanism to chain/wrap actions or an action and a function. -// This enables us to call a callback or another action (inner) between -// the root action's (outer) pre and post stages. -// -// Outer action o-------x o-------x -// v ^ -// Inner action/callback o---|---x -// -// A trivial example: -// -// actionSet.someAction.chainApply(actionsSet, -// function(){ -// // this gets run between someAction's pre and post -// // stages... -// }, -// args) -// -// This is intended to implement protocols where a single action is -// intended to act as a hook point (outer) and multiple different -// implementations (inner) within a single action set can be used as -// entry points. -// -// // Protocol root action (outer) definition... -// protocolAction: [function(){}], -// -// // Implementation actions (inner)... -// implementationAction1: [function(){ -// return this.protocolAction.chainApply(this, function(){ -// // ... -// }, ..) -// }] -// -// implementationAction2: [function(){ -// return this.protocolAction.chainApply(this, function(){ -// // ... -// }, ..) -// }] -// -// Now calling any of the 'implementation' actions will execute code -// in the following order: -// 1) pre phase of protocol action (outer) -// 2) implementation action (inner) -// 3) post phase of protocol action (outer) -// -// NOTE: this will not affect to protocol/signature of the outer action -// in any way. -// NOTE: both the inner and outer actions will get passed the same -// arguments. -// NOTE: another use-case is testing/debugging actions. -// NOTE: this is effectively the inside-out of normal action overloading. -// NOTE: there is intentionally no shorthand for this feature, to avoid -// confusion and to discourage the use of this feature unless -// really necessary. -// -// -// 3) .__call__ action / handler -// This action if defined is called for every action called. It behaves -// like any other action but with a fixed signature, it always receives -// the action name as first argument and a list of action arguments as -// the second arguments, and as normal a result on the post phase. -// -// NOTE: it is not necessary to define the actual action, binding to a -// handler will also work. -// NOTE: one should not call actions directly from within a __call__ -// handler as that will result in infinite recursion. -// XXX need a way to prevent this... -// NOTE: one should use this with extreme care as this will introduce -// an overhead on all the actions if not done carefully. -// -// -// /*********************************************************************/ // helpers... -var normalizeTabs = function(str){ - str = str.split(/\n/g) - - // get min number of leading tabs... - var i = str.length == 2 && /^\t/.test(str[1]) ? - str[1].split(/^(\t+)/)[1].length - 1 - : Math.min.apply(null, str - // skip first line... - .slice(1) - // skip empty strings... - .filter(function(l){ return l.trim() != '' }) - // count leading tabs... - .map(function(l){ - return /^\t+/.test(l) ? - l.split(/^(\t+)/)[1].length - : 0})) - - return (str[0] +'\n' - + str - .slice(1) - // trim leading tabs... - .map(function(l){ return l.slice(i) }).join('\n') - // replace tabs... - .replace(/\t/g, ' ')) - // remove leading and trailing whitespace... - .trim() -} - - +// XXX doc... var doWithRootAction = module.doWithRootAction = function(func){ return function(){ - var args = args2array(arguments) + var args = [...arguments] var handlers = (this.getHandlerList || MetaActions.getHandlerList) .apply(this, args) + return func.apply(this, [handlers.pop()].concat(args)) } } - return func.apply(this, [handlers.pop()].concat(args)) - } -} + + +//--------------------------------------------------------------------- +// String action parser/runner... +// +// Examples: +// 'actionName' +// 'actionName: attr 123 "string" -- comment...' +// 'actionName: ...' +// +// +// Syntax: +// ALIAS ::= +// +// | : +// | : +// ::= +// +// | +// ::= +// Number|String|Array|Object +// IDENTIFIER +// | ... +// | '$[0-9]' +// ::= +// '--.*$' +// +// Special args: +// IDENTIFIER +// - expanded to context[IDENTIFIER] +// $N - expanded to an instance of parseStringAction.Argument +// ... - expanded to parseStringAction.ALLARGS (singleton) +// +// +// Returns: +// { +// action: action, +// arguments: args, +// doc: doc, +// no_default: no_default, +// stop_propagation: false, +// +// code: txt, +// } +// +// +// NOTE: identifiers are resolved as attributes of the context... +// NOTE: this is a stateless object... +// XXX this is the same as ImageGrid's keyboard.parseActionCall(..), reuse +// in a logical manner... + +// placeholders... +var __Atom +var __Argument + +var parseStringAction = +module.parseStringAction = +Object.assign( + // parser... + function(txt){ + // split off the doc... + var c = txt.split('--') + var doc = (c[1] || '').trim() + // the actual code... + c = c[0].split(':') + + // action and no default flag... + var action = c[0].trim() + var no_default = action.slice(-1) == '!' + action = no_default ? action.slice(0, -1) : action + + // parse arguments... + var args = ((c[1] || '') + .match(RegExp([ + // strings... + '"[^"]*"', + "'[^']*'", + '`[^`]*`', + + // objects... + // XXX hack-ish... + '\\{[^\\}]*\\}', + + // lists... + // XXX hack-ish... + '\\[[^\]]*\]', + + // numbers... + '\\d+\\.\\d+|\\d+', + + // identifiers... + '[a-zA-Z$@#_][a-zA-Z0-9$@#_]*', + + // rest args... + '\\.\\.\\.', + + // null... + 'null', + ].join('|'), 'gm')) + || []) + .map(function(e){ + // argument placeholder... + return /^\.\.\.$/.test(e) ? + parseStringAction.ALLARGS + : /^\$[a-zA-Z0-9$@#_]*$/.test(e) ? + new parseStringAction.Argument(e.slice(1)) + // idetifiers... + // NOTE: keep this last as it is the most general... + : /^[a-zA-Z$@#_][a-zA-Z0-9$@#_]*$/.test(e) ? + new parseStringAction.Identifier(e) + : JSON.parse(e) }) + + return { + action: action, + arguments: args, + doc: doc, + no_default: no_default, + stop_propagation: false, + + code: txt, + } }, + + // API and utils... + { + // atoms... + Atom: (__Atom = object.Constructor('Atom', { + __init__: function(value){ + this.value = value }, + valueOf: function(){ + return this.value }, + })), + Identifier: object.Constructor('Identifier', + Object.create(__Atom.prototype)), + Argument: (__Argument = object.Constructor('Argument', + Object.create(__Atom.prototype))), + ALLARGS: new __Argument('...'), + + // general API... + resolveArgs: function(context, action_args, call_args){ + var that = this + var rest + var args = [...action_args] + // merge args... + .map(function(arg, i){ + return arg instanceof that.Argument ? + (arg === that.ALLARGS ? + (function(){ + rest = i + return arg + })() + : call_args[parseInt(arg.value)]) + // resolve idents... + : arg instanceof that.Identifier ? + context[arg.value] + : arg }) + rest != null + && args.splice(rest, 1, ...call_args) + return args }, + + // XXX should this break if action does not exist??? + callAction: function(context, action, ...args){ + action = typeof(action) == typeof('str') ? + this(action) + : action + // XXX should this break if action does not exist??? + return context[action.action] instanceof Function ? + context[action.action] + .apply(context, this.resolveArgs(context, action.arguments, args)) + // action not found or is not callable... (XXX) + : undefined }, + applyAction: function(context, action, args){ + return this.callAction(context, action, ...args) }, + + // XXX make this stricter... + isStringAction: function(txt){ + try{ + var parsed = typeof(txt) == typeof('str') + && (this.parseStringAction || parseStringAction)(txt) + return parsed + && /[a-zA-Z_][a-zA-Z0-9_]*/.test(parsed.action) + } catch(e){ + return false } }, + }) + +// shorthand... +var isStringAction = +module.isStringAction = + parseStringAction.isStringAction /*********************************************************************/ +// Action... + +// Return value wrapper... +// +// Wrapping a value in this and returning it from an action will force +// the action to return the value as-is... +// This is mainly usefull for specially handled values. +var ASIS = +module.ASIS = +object.Constructor('ASIS', { + __init__: function(obj){ this.value = obj } }) + +// undefined wrapper... +var UNDEFINED = +module.UNDEFINED = ASIS(undefined) + // Construct an action object... // +// Action(, ) +// Action([, [, ]][, ,] ) +// Action([, [[, ]][, ,] ]) +// -> +// +// // Action function format: // // // pre event code... @@ -323,6 +282,10 @@ function(func){ // // - an action will return the deepest (root) action's return, if that // return is undefined, then the action set is returned instead. +// If the root action returns a Promise, then the post phase will be +// triggerd AFTER that promise is resolved or rejected, this can be +// disabled by setting the 'await' action attribute to false (see: +// Action.prototype.await for details) // // - action arguments are "threaded" through the action chain down and // root action return value and arguments are threaded back up the @@ -340,6 +303,36 @@ function(func){ // XXX is this correct?? // NOTE: by default an action will return 'this', i.e. the action set // object the action was called from. +// NOTE: if func.nmae is set to '' it will be reset to the +// action name by Action(..). This is a means for extending functions +// to get the specific action name. +// Example: +// var getActionName = function(func){ +// var f = function(...args){ +// return func(f.name, ...args) } +// // this will force Actions(..) to set a name on f +// Object.defineProperty(f, 'name', { value: '' }) +// return f +// } +// +// ... +// +// someAction: [ +// getActionName(function(name, ...args){ +// console.log('Action name:', name) +// })], +// someOtherAction: [ +// function(name, ...args){ +// // there is no way to know the action name from within +// // and action... +// }], +// +// But note that the .name is set in definition time and not in +// call time, so renaming the action in runtime will have no effect +// on what it will log... +// Also note that using Object.defineProperty(..) is required as +// chrome ignores changes to function's .name in other cases... +// // // XXX add more metadata/docs: // .section @@ -347,196 +340,448 @@ function(func){ // ... // XXX might be a good idea to add an option to return the full results... var Action = -module.Action = -function Action(name, doc, ldoc, func){ - // we got called without a 'new'... - if(this == null || this.constructor !== Action){ - // XXX using something like .apply(.., arguemnts) would be more - // generel but have no time to figure out how to pass it - // to new without the later complaining... - return new Action(name, doc, ldoc, func) - } +module.Action = +object.Constructor('Action', { + __proto__: Function, - // prevent action overloading... - if(this[name] != null){ - throw 'action "'+name+'" already exists.' - } + // Control how an action handles returned promises... + // + // Possible values: + // true - if an action returns a promise then trigger the post phase + // after that promise is resolved / rejected... (default) + // false - handle promises like any other returned value. + // + // + // NOTE: .await is only checked in the root action, thus it can not be + // overloaded by extending actions. + // This is done intentionally, as the action actually returning a + // value (and defining the signature) is the only one responsible + // for controlling how it's handled. + // + // For implmentation see: Action.prototype.chainApply(..) + // + // XXX should we be able to set this in the context??? + // XXX can we use 'await'??? + await: true, - // create the actual instance we will be returning... - //var meth = function(){ - // return meth.chainApply(this, null, arguments) } - var meth = function(){ - return meth.chainApply(this, null, arguments) } - meth.__proto__ = this.__proto__ - // populate the action attributes... - //meth.name = name - Object.defineProperty(meth, 'name', { - value: name, - }) - meth.doc = doc - meth.long_doc = ldoc + // pre/post stage runners... + // + // .pre(context, args) + // -> data + // + // .post(context, data) + // -> result + // + // + // Call data format: + // { + // arguments: args, + // + // wrapper: call_wrapper, + // handlers: handlers, + // + // result: res, + // } + // + // + // External methods (required): + // .getHandlers(..) resolved from: context, MetaActions + // + // + // External methods (optoinal): + // .__actioncall__(..) resolved from: context + // .preActionHandler(..) resolved from: context, MetaActions + // + // + // Special cases: + // - An action is referenced via a different name than is in its .name + // this can occur if: + // 1) an action is renamed but its .name is not + // 2) an action is created and stored with a different name + // var f = new Action('m', function(){ ... }) + // + // + // NOTE: All the defaults should be handled by the pre stage, post will + // process data assuming that it is correct. + // NOTE: .post(..) will not wait for returned promises to resolve, use + // .chainApply(..) / ,chainCall(..) instead, or handle .result + // manually... + // (see: Action.prototype.chainApply(..)) + // XXX revise the structure.... + // ...is it a better idea to define action methods in an object + // and assign that??? + pre: function(context, args){ + var that = this + args = args || [] - meth.func = func + // prepare for after calls... + // XXX this may pose problems with concurency... + // XXX do not like that this forces exception rethrowing... + // XXX EXPERIMENTAL (after calls)... + context.__action_after_running = [ + // nested call... + context.__action_after_running, + // top list... + (context.__action_after_running || [null, []])[1], + ] - // make introspection be a bit better... - meth.toString = func.toString.bind(func) + var res = context + var outer = this.name - return meth -} -// this will make action instances behave like real functions... -Action.prototype.__proto__ = Function + // get the handler list... + var getHandlers = context.getHandlers + || MetaActions.getHandlers + var handlers = getHandlers.call(context, outer) -// The pre/post stage runners... -// -// .pre(context, args) -// -> data -// -// .post(context, data) -// -> result -// -// -// NOTE: All the defaults should be handled by the pre stage, post will -// process data assuming that it is correct. -// -// XXX revise the structure.... -// ...is it a better idea to define action methods in an object -// and assign that??? -Action.prototype.pre = function(context, args){ - args = args || [] - - var res = context - var outer = this.name - - // get the handler list... - var getHandlers = context.getHandlers || MetaActions.getHandlers - var handlers = getHandlers.call(context, outer) - - // special case: see if we need to handle the call without handlers... - var preActionHandler = context.preActionHandler || MetaActions.preActionHandler - if(preActionHandler){ - // XXX signature needs work... - var res = preActionHandler.call(context, outer, handlers, args) - if(res !== undefined){ - return res + // handle cases where .func is not in handlers... + // + // NOTE: see Special cases in method doc above... + if(handlers.length == 0 + || handlers.filter(function(h){ + return h.pre === that.func }).length == 0){ + var cur = { + pre: this.func, + } + this.doc + && (cur.doc = this.doc) + this.long_doc + && (cur.long_doc = this.long_doc) + handlers.unshift(cur) } - } - var call_wrapper = outer != '__call__' ? - getHandlers.call(context, '__call__') - : [] + // special case: see if we need to handle the call without handlers... + var preActionHandler = context.preActionHandler + || MetaActions.preActionHandler + if(preActionHandler){ + var res = preActionHandler.call(context, outer, handlers, args) + if(res !== undefined){ + return res } } - // wrapper handlers: pre phase... - call_wrapper = call_wrapper - .map(function(a){ - if(a.pre){ - res = a.pre.call(context, outer, args) + var call_wrapper = outer != '__actioncall__' ? + getHandlers.call(context, '__actioncall__') + : [] - // if a handler returns a function register is as a post - // handler... - if(res - && res !== context - && res instanceof Function){ - a.post = res - } + try { + // wrapper handlers: pre phase... + call_wrapper = call_wrapper + .map(function(a){ + if(a.pre){ + res = a.pre.call(context, outer, args) + + // if a handler returns a function register is as a post + // handler... + if(res + && res !== context + && res instanceof Function){ + a.post = res + } + } + return a }) + + // handlers: pre phase... + handlers + // NOTE: current action will get included and called by the code + // above and below, so no need to explicitly call func... + // NOTE: pre handlers are called FIFO, i.e. the last defined first... + .map(function(a){ + if(a.pre){ + res = a.pre.apply(context, args) + + // if a handler returns a function register is as a post + // handler... + if(res + && res !== context + && res instanceof Function){ + a.post = res + + // reset the result... + // NOTE: this is the only difference between this + // and wrapper stages... + res = context + } + } + return a }) + + // XXX EXPERIMENTAL (after calls)... + } catch(error){ + // XXX should we unwind this??? + delete context.__action_after_running + throw error + } + + // return context if nothing specific is returned... + res = res === undefined ? context + : res instanceof ASIS ? res.value + // XXX returning an explicit [undefined]... + //: res instanceof Array + // && res.length == 1 + // && res.indexOf(undefined) == 0 ? + // undefined + : res + + return { + arguments: args, + + wrapper: call_wrapper, + handlers: handlers, + + result: res, + } }, + post: function(context, data){ + var res = data.result + + var args = data.arguments || [] + // the post handlers get the result as the first argument... + args.splice(0, 0, res) + + var outer = this.name + + try { + // handlers: post phase... + data.handlers && data.handlers + // NOTE: post handlers are called LIFO -- last defined last... + .reverse() + .forEach(function(a){ + a.post + && a.post.apply(context, args) + }) + + // wrapper handlers: post phase... + data.wrapper && data.wrapper + // NOTE: post handlers are called LIFO -- last defined last... + .reverse() + .forEach(function(a){ + a.post + && a.post.call(context, res, outer, args.slice(1)) + }) + + // XXX EXPERIMENTAL (after calls)... + } catch(error){ + // should we unwind this??? + delete context.__action_after_running + throw error + } + + // handle after calls... + // XXX EXPERIMENTAL (after calls)... + ;(context.__action_after_running || []) + .slice(2) + .forEach(function(func){ + func.call(context) }) + // top calls... + if(context.__action_after_running){ + if(context.__action_after_running[0] == null){ + ;(context.__action_after_running[1] || []) + .forEach(function(func){ + func.call(context) }) + delete context.__action_after_running + // back to prev level... + } else { + context.__action_after_running = context.__action_after_running[0] } - return a - }) + } - // handlers: pre phase... - handlers - // NOTE: current action will get included and called by the code - // above and below, so no need to explicitly call func... - // NOTE: pre handlers are called FIFO, i.e. the last defined first... - .map(function(a){ - if(a.pre){ - res = a.pre.apply(context, args) + return res }, - // if a handler returns a function register is as a post - // handler... - if(res - && res !== context - && res instanceof Function){ - a.post = res - // reset the result... - // NOTE: this is the only difference between this - // and wrapper stages... - res = context - } + // chaining... + // + // For docs see: MetaActions.chainApply(..) and the base module doc. + chainApply: function(context, inner, args){ + args = [...(args || [])] + var outer = this.name + + var data = this.pre(context, args) + + // call the inner action/function if preset.... + // NOTE: this is slightly different (see docs) to what happens in + // .pre(..)/.post(..), thus we are doing this separately and + // not reusing existing code... + if(inner){ + var res = inner instanceof Function ? + inner.apply(context, args) + : inner instanceof Array && inner.length > 0 ? + context[inner.pop()].chainApply(context, inner, args) + : typeof(inner) == typeof('str') ? + context[inner].chainApply(context, null, args) + : undefined + + // call the resulting function... + if(res instanceof Function){ + res.apply(context, [context].concat(args)) + data.result = context + + // push the inner result into the chain... + } else if(res !== undefined){ + data.result = res } - return a - }) + } - // return context if nothing specific is returned... - res = res === undefined ? context - : res === UNDEFINED ? undefined - : res + // returned promise -> await for resolve/error... + // XXX should we be able to set this in the context??? + if(data.result instanceof Promise + && (context.getRootActionAttr || MetaActions.getRootActionAttr) + .call(context, this.name, 'await') ){ + var that = this + return data.result + .then(function(){ + return that.post(context, data) }) + .catch(function(){ + return that.post(context, data) }) + } - return { - arguments: args, + return this.post(context, data) }, + chainCall: function(context, inner){ + return this.chainApply(context, inner, [...arguments].slice(2)) }, - wrapper: call_wrapper, - handlers: handlers, - result: res, - } -} -Action.prototype.post = function(context, data){ - var res = data.result + // constructor... + // + // Action(, ) + // Action([, [, ]][, ,] ) + // Action(, [ [[, ]][, ,] ]) + // -> + // + __new__: function(context, name, doc, ldoc, attrs, func){ + // prevent action overloading... + // XXX do we need this??? + //if(context != null && context[name] != null){ + // throw 'action "'+name+'" already exists.' } - var args = data.arguments || [] - // the post handlers get the result as the first argument... - args.splice(0, 0, res) + // create the actual instance we will be returning... + var meth = function(){ + return meth.chainApply(this, null, arguments) } + meth.__proto__ = this.__proto__ - var outer = this.name - - // handlers: post phase... - data.handlers && data.handlers - // NOTE: post handlers are called LIFO -- last defined last... - .reverse() - .forEach(function(a){ - a.post - && a.post.apply(context, args) - }) - - // wrapper handlers: post phase... - data.wrapper && data.wrapper - // NOTE: post handlers are called LIFO -- last defined last... - .reverse() - .forEach(function(a){ - a.post - && a.post.call(context, res, outer, args.slice(1)) - }) - - return res -} - -// Chaining... -Action.prototype.chainApply = function(context, inner, args){ - args = [].slice.call(args || []) - var res = context - var outer = this.name - - var data = this.pre(context, args) - - // call the inner action/function if preset.... - if(inner){ - //res = inner instanceof Function ? - inner instanceof Function ? - inner.call(context, args) - : inner instanceof Array && inner.length > 0 ? - context[inner.pop()].chainCall(context, inner, args) - : typeof(inner) == typeof('str') ? - context[inner].chainCall(context, null, args) + // precess args... + var args = doc instanceof Array ? + doc + : [...arguments] + .slice(2) + .filter(function(e){ return e !== undefined }) + func = args.pop() + last = args[args.length-1] + attrs = (last != null && typeof(last) != typeof('str')) ? + args.pop() + : {} + doc = typeof(args[0]) == typeof('str') ? + args.shift() + : func.doc ? + func.doc + : null + ldoc = typeof(args[0]) == typeof('str') ? + args.shift() + : func.long_doc ? + func.long_doc : null - } - return this.post(context, data) -} -Action.prototype.chainCall = function(context, inner){ - return this.chainApply(context, inner, args2array(arguments).slice(2)) -} + // populate the action attributes... + //meth.name = name + Object.defineProperty(meth, 'name', { + value: name, + }) + func.doc = meth.doc = doc + func.long_doc = meth.long_doc = ldoc + + meth.func = func + + if(func.name == ''){ + Object.defineProperty(func, 'name', { + value: name, + }) + } + + // make introspection be a bit better... + meth.toString = function(){ + return object.normalizeIndent(func.toString()) } + + // setup attrs... + Object.assign(meth, attrs) + Object.assign(func, attrs) + + return meth }, +}) + + + +//--------------------------------------------------------------------- + +// Action alias constructor... +// +// This is signature compatible with Action(..) with one difference being +// that this expects the target to be a string compatible with +// .parseStringAction(..)... +// +// This will resolve special alias args: +// name -> parseStringAction.Identifier(name) -> this[name] +// $N -> parseStringAction.Argument(N) -> arguments[n] +// ... -> parseStringAction.ALLARGS -> arguments +// +// +// XXX alias parsing is dependant on the action set, move this functionality +// to the ActionSet.alias(..) method/action... +// XXX handle alias args and pass them to the target... +// XXX should an alias return a value??? +var Alias = +module.Alias = +object.Constructor('Alias', { + __proto__: Action.prototype, + + __new__: function(context, alias, doc, ldoc, attrs, target){ + // precess args... + var args = doc instanceof Array ? + doc + : [...arguments] + .slice(2) + .filter(function(e){ return e !== undefined }) + target = args.pop() + last = args[args.length-1] + attrs = (last != null && typeof(last) != typeof('str')) ? + args.pop() + : {} + doc = typeof(args[0]) == typeof('str') ? + args.shift() + : null + ldoc = typeof(args[0]) == typeof('str') ? + args.shift() + : null + + attrs.alias = target + + // NOTE: we are not parsing this directly here because the context + // may define a different .parseStringAction(..) + var parsed = typeof(target) == typeof('str') ? + null + : target + + doc = (!doc && parsed) ? + parsed.doc + : doc + + var func = function(){ + // empty alias... + if(target == ''){ + return } + + var p = parsed + || (this.parseStringAction || parseStringAction)(target) + + return p.action in this ? + (this.parseStringAction || parseStringAction).callAction(this, p, ...arguments) + // error... + : console.error(`${alias}: Unknown alias target action: ${p.action}`) } + func.toString = function(){ + return meth.alias.code || meth.alias } + + // make the action... + var meth = object.parentCall(Alias.prototype.__new__, this, context, alias, doc, ldoc, attrs, func) + //meth.__proto__ = this.__proto__ + + meth.func.alias = target + + return meth }, +}) @@ -573,22 +818,182 @@ module.MetaActions = { res.push(k) } } - return res + return res }, + + + // List aliases... + // + get aliases(){ + var that = this + return this.actions + .filter(function(n){ + return that[n] instanceof Alias }) }, + get localAliases(){ + var that = this + return this.aliases + .filter(function(n){ + return that.hasOwnProperty(n) })}, + + // XXX move this to the right spot... + parseStringAction: parseStringAction, + isStringAction: isStringAction, + + // XXX EXPERIMENTAL... + call: function(action, ...args){ + return action instanceof Function ? + action.apply(this, args) + : this[action] ? + this[action].apply(this, args) + : this.parseStringAction.applyAction(this, action, args) }, + apply: function(action, args){ + return this.call(action, ...args)}, + + + // Set/remove action alias... + // + // Set alias... + // .alias(alias, code) + // .alias(alias[, doc[, long-doc]][, attrs,] code) + // .alias(alias, [ [doc[, long-doc]][, attrs,] code ]) + // -> action-set + // + // Remove alias... + // .alias(alias, null) + // .alias(alias, false) + // -> action-set + // + // code should be compatible with .parseStringAction(..) + // + // NOTE: this does not check if it will override anything, so it is + // possible to override/delete an action/method/attribute with + // this... + // + // XXX should this prevent overriding stuff??? + // XXX move to a better spot... + alias: Action('alias', function(alias, target){ + // remove alias... + if(arguments.length == 2 + && (target === false || target === null)){ + // delete only aliases... + this[alias] instanceof Alias + && (delete this[alias]) + + // set alias... + } else { + this[alias] = Alias(...arguments) + } }), + + + // Get action attribute... + // + // Attribute search order (return first matching): + // - Local action + // - Local action function (.func) + // - if an alias look in the target... + // - repeat for .__proto__ (until top of MRO) + // - repeat for '__actioncall__' special action (XXX EXPERIMENTAL) + // + // + // NOTE: this will get attribute set both on the action object and + // the action function, this covers two usecases: + // 1) action constructor attributes... + // someAction: ['...', + // // action attribute... + // {attr: 'value'}, + // function(){ ... }], + // 2) action modifiers... + // var modifyAction = function(func){ + // // function attribute... + // func.attr = 'value' + // return func + // } + // ... + // someAction: ['...', + // modifyAction(function(){ ... })], + // + // XXX document... + // XXX add option to to enable/disable look in .__actioncall__... + getActionAttr: function(action, attr){ + var cur = this + + // go up the proto chain... + while(cur.__proto__ != null){ + var c = cur[action] + if(c != null){ + // attribute of action... + if(c[attr] !== undefined){ + return c[attr] + + // attribute of action function... + } else if(c.func && c.func[attr] !== undefined){ + return c.func[attr] + + // alias -> look in the target action... + } else if(c instanceof Alias){ + var res = this.getActionAttr( + this.parseStringAction(cur[action].alias).action, + attr) + if(res !== undefined){ + return res + } + } + } + cur = cur.__proto__ + } + + // search .__actioncall__ action... + if(cur[action] != null && action != '__actioncall__'){ + return this.getActionAttr('__actioncall__', attr) + } }, - // Number of defined actions... + // Get root action attribute value... // - get length(){ - return this.actions.length + // This is similar to .getActionAttr(..) but will only chenck the + // root action for the attribute... + // + // NOTE: if an attr is not explicitly defined in the root action, the + // base Action object is checked (Action.prototype.await)... + getRootActionAttr: function(action, attr){ + var cur = this + + // go up the proto chain... + while(cur.__proto__ != null){ + if(cur[action] != null){ + var target = cur + } + cur = cur.__proto__ + } + + // attribute of action... + if(target[action][attr] !== undefined){ + return target[action][attr] + + // attribute of action function... + } else if(target[action].func + && target[action].func[attr] !== undefined){ + return target[action].func[attr] + } }, // Get action documentation... // + // Format: + // { + // action-name: [ + // doc, + // long_doc, + // name, + // ], + // ... + // } + // + // NOTE: oveloading actions will shadow parents doc if they define .doc. getDoc: function(actions){ var res = {} var that = this actions = actions == null ? this.actions - : arguments.length > 1 ? args2array(arguments) + : arguments.length > 1 ? [...arguments] : typeof(actions) == typeof('str') ? [actions] : actions @@ -605,14 +1010,13 @@ module.MetaActions = { cur = cur.__proto__ } }) - return res - }, + return res }, getPath: function(actions){ var res = {} var that = this actions = actions == null ? this.actions - : arguments.length > 1 ? args2array(arguments) + : arguments.length > 1 ? [...arguments] : typeof(actions) == typeof('str') ? [actions] : actions @@ -631,19 +1035,135 @@ module.MetaActions = { res[(doc && doc.replace(/[\\\/]$/, '/'+n)) || n] = [n, doc, long_doc] }) - return res - }, + return res }, + // Toggle handler cache... + // + // Toggle cache... + // .toggleHandlerCache() + // + // Set caching on... + // .toggleHandlerCache('on') + // .toggleHandlerCache(true) + // + // Set caching off... + // .toggleHandlerCache('off') + // .toggleHandlerCache(false) + // + // Reset caching... + // .toggleHandlerCache('!') + // + // Get current caching state... + // .toggleHandlerCache('?') + // + // Get supported states... + // .toggleHandlerCache('??') + // -> ['on', 'off'] + // + // + // NOTE: setting the cache on may prevent calling of actions event + // handlers of parent action-sets if they are added (via .on(..) + // or .one(..), ...) AFTER the current object cloned it's cache. + // to avoid this, care must be taken to reset the cache of + // children objects, or not use caching for cases where action + // event handlers can be added on the tree in runtime. + // + // + // XXX should we use the toggler object here??? + // XXX EXPERIMENTAL (handler cache)... + toggleHandlerCache: function(to){ + if(to == '?'){ + return this.__handler_cache ? 'on' : 'off' + + } else if(to == '??'){ + return ['on', 'off'] + } + + to = (to === true || to == 'on') ? true + : (to === false || to == 'off') ? false + : to == '!' ? !!this.__handler_cache + : !this.__handler_cache + + if(to){ + // no local cache -> copy from parent... + if(this.__handler_cache + && !Object.hasOwnProperty(this, '__handler_cache')){ + var parent = this.__handler_cache + var cache = this.__handler_cache = {} + for(var a in parent){ + cache[a] = parent[a] + } + + // local cache only... + } else { + this.__handler_cache = this.__handler_cache || {} + } + + } else { + // NOTE: we do not delete here so as to shadow the parent's + // cache... + this.__handler_cache = false + } + + // XXX this is not the handler protocol... + return this }, + + // Rest handler cache... + // + // Reset the full cache... + // .resetHandlerCache() + // -> this + // + // Reset handler cache for action... + // .resetHandlerCache(action) + // -> this + // + // NOTE: when .toggleHandlerCache('?') is 'off' this has no effect. + // + // XXX EXPERIMENTAL (handler cache)... + resetHandlerCache: function(name){ + var cache = this.__handler_cache + if(cache){ + // full reset... + if(name == null){ + this.__handler_cache = {} + + // reset action... + } else { + // no local cache -> copy from parent... + if(!Object.hasOwnProperty(this, '__handler_cache')){ + var parent = this.__handler_cache + var cache = this.__handler_cache = {} + for(var a in parent){ + cache[a] = parent[a] + } + } + + delete cache[name] + } + } + return this }, + // Get action handlers from the inheritance chain... // // NOTE: this collects both the event handlers (in order of hierarchy, // then order of definition) and actions (in order of hierarchy) // NOTE: this is the correct order for 'pre' calling, but is the // reverse of how the 'post' handlers must be called. + // NOTE: if .toggleHandlerCache('?') is on, this will serch once and + // return the cached results on every subsequent call. // // For more docs on handler sequencing and definition see: .on(..) getHandlerList: function(name){ + // handler cache... + // XXX EXPERIMENTAL (handler cache)... + var cache = this.__handler_cache + if(cache && cache[name]){ + return cache[name].slice() + } + + // get the handlers... var handlers = [] var cur = this while(cur.__proto__ != null){ @@ -657,15 +1177,30 @@ module.MetaActions = { // get the overloading action... // NOTE: this will get all the handlers including the root // and the current handlers... - // NOTE: this will ignore "shadows" that are not actions... - if(cur.hasOwnProperty(name) && cur[name] instanceof Action){ - handlers.push(cur[name].func) + // NOTE: if this encounters a matching mormal method/function + // this will not search beyond it. + if(cur.hasOwnProperty(name)){ + // action -> collect... + if(cur[name] instanceof Action){ + handlers.push(cur[name].func) + + // function -> terminate chain... + } else if(cur[name] instanceof Function){ + handlers.push(cur[name]) + break + } } cur = cur.__proto__ } - return handlers - }, + + // handler cache... + // XXX EXPERIMENTAL (handler cache)... + if(cache){ + cache[name] = handlers + } + + return handlers }, // Get structured action handler definitions... // @@ -676,6 +1211,9 @@ module.MetaActions = { // // .pre or .post, this does not mean that one can // // not define both, just that they are stored separately // pre|post: , + // + // // XXX + // alias: , // }, // ... // ] @@ -698,9 +1236,12 @@ module.MetaActions = { res.post = a.post_handler } - return res - }) - }, + a.doc + && (res.doc = a.doc) + a.long_doc + && (res.long_doc = a.long_doc) + + return res }) }, // Handler for cases when we need to avoid the pre/post handlers... // @@ -711,6 +1252,7 @@ module.MetaActions = { // NOTE: the object result must be compatible with Action.pre(..) // return value... // NOTE: this is mostly a stub, here for documentation reasons... + // XXX doc / revise... //preActionHandler: doWithRootAction( // function(action, name, handlers, args){ return null }), @@ -760,6 +1302,8 @@ module.MetaActions = { action = mode[0] mode = mode[1] + that.resetHandlerCache(action) + // keep the original handler for future use... var a_handler = handler @@ -772,13 +1316,15 @@ module.MetaActions = { // for removal via. .off(..) a_handler.orig_handler = old_handler.orig_handler || old_handler + a_handler.orig_handler.event_tag = tag + // not pre mode... } else if(mode != 'pre') { // XXX throw 'Unknown action mode: '+action+'.'+mode } - a_handler.tag = tag + a_handler.event_tag = tag // register handlers locally only... if(!that.hasOwnProperty('_action_handlers')){ @@ -794,8 +1340,7 @@ module.MetaActions = { } }) - return this - }, + return this }, // Remove an action callback... // @@ -828,6 +1373,8 @@ module.MetaActions = { action = mode[0] mode = mode[1] + that.resetHandlerCache(action) + // get the handlers... var h = that._action_handlers[action] || [] @@ -862,13 +1409,12 @@ module.MetaActions = { h.splice.apply(h, [0, h.length] .concat(h.filter(function(e){ - return e.tag != handler }))) + return e.event_tag != handler }))) } }) } - return this - }, + return this }, // Register an action callback that will only fire once per event... // @@ -899,8 +1445,38 @@ module.MetaActions = { that.on(action, tag, handler) }) - return this - }, + return this }, + + // XXX EXPERIMENTAL (after calls)... + isActionRunning: function(){ + return !!this.__action_after_running }, + // Queue a function after the action is done... + // + // .afterAction(func) + // .afterAction('top', func) + // -> this + // + // .afterAction('local', func) + // -> this + // + // XXX EXPERIMENTAL (after calls)... + afterAction: function(mode, func){ + func = mode instanceof Function ? mode : func + mode = mode instanceof Function ? null : mode + mode = mode || 'top' + + if(!this.__action_after_running){ + throw new Error('afterAction: no action is running.') + } + + ;(mode == 'top' ? + this.__action_after_running[1] + : mode == 'local' ? + this.__action_after_running + : this.__action_after_running) + .push(func) + + return this }, // Apply/call a function/action "inside" an action... // @@ -922,38 +1498,131 @@ module.MetaActions = { // // The given arguments are passed as-is to both the outer and inner // actions. - // The base inner action return value is passed to the outer action + // The inner action return value is passed to the outer action // .post handlers. // + // inner return value is handling slightly differs from the base + // action protocol in two respects: + // 1) to keep the outer return value, inner must return undefined. + // 2) to guarantee returning the context regardless of outer's return + // value, the inner must return the context (this) explicilty. + // + // NOTE: as a restriction of the action protocol the inner return will + // override the return value of outer, but there is no way to + // see that value. // NOTE: these call the action's .chainApply(..) and .chainCall(..) // methods, thus is not compatible with non-action methods... - // NOTE: .chianCall('action', ..) is equivalent to .action.chianCall(..) + // NOTE: .chainCall('action', ..) is equivalent to .action.chainCall(..) chainApply: function(outer, inner, args){ return this[outer].chainApply(this, inner, args) }, chainCall: function(outer, inner){ - return this[outer].chainApply(this, inner, args2array(arguments).slice(2)) }, + return this[outer].chainApply(this, inner, [...arguments].slice(2)) }, + + // Call action handlers serted by .sortedActionPriority... + // + // NOTE: this by design ignores the action call results to avoid + // actions competing on who will return a value... + // NOTE: if action name does not exist this will do nothing and + // return normally (without error)... + // NOTE: this essentially re-implements parts of the .pre(..)/.post(..) + // action protocol... + // NOTE: this may not support some legacy action protocol features... + callSortedAction: function(name, ...args){ + var that = this + this.getHandlers(name) + .map(function(h, i){ + var p = (h.pre || {}).sortedActionPriority + // normalize priority... + p = p == 'high' ? + 50 + : p == 'normal' ? + 0 + : p == 'low' ? + -50 + : p + return [i, p, h] }) + // sort by .sortedActionPriority ascending... + .sort(function([ia, pa, a], [ib, pb, b]){ + return (pa != null && pb != null) ? + pa - pb + : (pa > 0 || pb < 0) ? + 1 + : (pb > 0 || pa < 0) ? + -1 + : ia - ib }) + // the list should be ordered descending form highest + // priority or closeness to root action... + .reverse() + // call the actions (pre)... + .map(function([i, p, a]){ + return a.pre ? + a.pre.call(that, ...args) + : a.post }) + .reverse() + // call the actions (post)... + // NOTE: we do not care about call results here... + .forEach(function(func){ + func instanceof Function + && func.call(that, ...args) }) + return this }, + + + + // Get action/method resolution order... + // + // List mixin tags if present, else objects... + // .mro() + // .mro('tag-object') + // -> tags + // + // List mixin tags... + // .mro('tag') + // -> tags + // + // List mixin objects... + // .mro('object') + // -> objects + // + // List mixin tag-object pairs... + // .mro('item') + // -> items + // + // NOTE: this will return the full MRO including Object.prototype + mro: function(target){ + target = target || 'tag-object' + var res = [] + var cur = this + while(cur != null){ + res.push(target == 'tag-object' ? + cur.__mixin_tag || cur + : target == 'tag' ? + cur.__mixin_tag + : target == 'object' ? + cur + : [cur.__mixin_tag, cur]) + // go to next item in chain... + cur = cur.__proto__ + } + return res }, + // Get mixin object in inheritance chain... // + // NOTE: from can be either an explicit action object or a tag... // NOTE: if pre is true this will return the chain item before the // mixin, this is useful, for example, to remove mixins, see // .mixout(..) for an example... getMixin: function(from, pre){ - var cur = this - var proto = this.__proto__ - while(proto != null){ - // we have a hit... - if(proto.hasOwnProperty('__mixin_source') - && proto.__mixin_source === from){ - return pre ? cur : proto - } - // go to next item in chain... - cur = proto - proto = cur.__proto__ - } - return null - }, - + var mro = this.mro('object') + var res = (pre ? mro.slice(1) : mro) + .filter(function(e){ + return e.__mixin_tag == from + || e.__mixin_source === from }) + .shift() + return pre ? + mro[mro.indexOf(res)-1] + : res }, + // Mixin a set of actions into this... // // NOTE: if 'all' is set then mixin all the actions available, @@ -961,12 +1630,18 @@ module.MetaActions = { // NOTE: this will override existing own attributes. // // XXX should we include functions by default???? - inlineMixin: function(from, all, descriptors, all_attr_types){ + // XXX should .source_tag be set here or in Actions(..)??? + inlineMixin: function(from, options){ // defaults... - descriptors = descriptors || true - all_attr_types = all_attr_types || false + options = options || {} + var descriptors = options.descriptors || true + var all_attr_types = options.all_attr_types || false + var source_tag = options.source_tag - if(all){ + resetHandlerCache = (this.resetHandlerCache || MetaActions.resetHandlerCache) + resetHandlerCache.call(this) + + if(options.all){ var keys = [] for(var k in from){ keys.push(k) @@ -993,45 +1668,84 @@ module.MetaActions = { prop.configurable = true Object.defineProperty(that, k, prop) - // actions and other attributes... } else { var attr = from[k] if(all_attr_types - //|| attr instanceof Function + || attr instanceof Function || attr instanceof Action){ that[k] = attr } + + // source tag actions... + // XXX should this set action and method .source_tag or only action??? + //if(source_tag && attr instanceof Action){ + if(source_tag && (attr instanceof Action || attr instanceof Function)){ + // existing tag... + if(that[k].source_tag == source_tag + || (that[k].func || {}).source_tag == source_tag){ + return + + // new tag... + // XXX not sure if this is the right way to go... + } else if(that[k].source_tag + || (that[k].func || {}).source_tag){ + console.warn('Aactions: about to overwrite source tag...\n' + +' from: "' + +(that[k].source_tag + || (that[k].func || {}).source_tag)+'"\n' + +' to: "'+source_tag+'"\n' + +' on:', that[k]) + } + + if(that[k].func){ + that[k].func.source_tag = source_tag + } + that[k].source_tag = source_tag + } } }) - return this - }, + return this }, // Same as .inlineMixin(..) but isolates a mixin in a seporate object // in the inheritance chain... // - mixin: function(from, all, descriptors, all_attr_types){ + mixin: function(from, options){ + options = options || {} + options.source_tag = options.source_tag || from.__mixin_tag + var proto = Object.create(this.__proto__) // mixinto an empty object - proto.inlineMixin(from, all, descriptors, all_attr_types) + proto.inlineMixin(from, options) // mark the mixin for simpler removal... proto.__mixin_source = from + // add source tag to proto... + if(options && options.source_tag){ + proto.__mixin_tag = options.source_tag + } + this.__proto__ = proto - return this - }, + return this }, + + // Mixin from after target in the mro... + // + // NOTE: target must be .getMixin(..) compatible... + mixinAfter: function(target, from, options){ + this + .getMixin(target) + .mixin(from, options) + return this }, // Mixin a set of local actions into an object... // // XXX this will not work on non-actions... - mixinTo: function(to, all, descriptors, all_attr_types){ - return this.mixin.call(to, this, all, descriptors, all_attr_types) - }, - + mixinTo: function(to, options){ + return this.mixin.call(to, this, options) }, // Remove mixed in actions from this... // @@ -1039,12 +1753,15 @@ module.MetaActions = { // not be affected... // NOTE: this will not affect event handlers, they should be removed // manually if needed... - inlineMixout: function(from, all, descriptors, all_attr_types){ + inlineMixout: function(from, options){ // defaults... - descriptors = descriptors || true - all_attr_types = all_attr_types || false + options = options || {} + var descriptors = options.descriptors || true + var all_attr_types = options.all_attr_types || false - if(all){ + (this.resetHandlerCache || MetaActions.resetHandlerCache).call(this) + + if(options.all){ var keys = [] for(var k in from){ keys.push(k) @@ -1075,30 +1792,28 @@ module.MetaActions = { } }) - return this - }, + return this }, - // This is similare in effect but different in mechanics to .inlineMixout(..) + // This is similar in effect but different in mechanics to .inlineMixout(..) // - // This will find and remove a mixin object from the inheritance chian. + // This will find and remove a mixin object from the inheritance chain. // // NOTE: this will remove only the first occurance of a mixin. mixout: function(from){ var o = this.getMixin(from, true) - + var target = null // pop the mixin off the chain... if(o != null){ + target = o.__proto__ o.__proto__ = o.__proto__.__proto__ + this.resetHandlerCache() } - - return this - }, + return target }, // Remove a set of local mixed in actions from object... // - mixoutFrom: function(to, all, descriptors, all_attr_types){ - return this.mixout.call(to, this, all, descriptors, all_attr_types) - }, + mixoutFrom: function(to, options){ + return this.mixout.call(to, this, options) }, // Create a child object... // @@ -1117,18 +1832,61 @@ module.MetaActions = { o.config = Object.create(this.config) } } - return o - }, + return o }, + + getHandlerSourceTags: function(name){ + return this.getHandlers(name) + .map(function(a){ + return a.pre ? (a.pre.source_tag || a.pre.event_tag) + : a.post ? (a.post.source_tag || a.post.event_tag) + : null + }) + .unique() }, + + + // Run a function in the context of the action set... + // + // This will return 'this' if func returns undefined, otherwise func + // return value is returned. + // + // This is here simply as a utility function, to enable running code + // in a concatinative manner without interruption... + run: function(func){ + var res = func ? + func.call(this) + : undefined + return res === undefined ? + this + : res }, + // doc generators... // // XXX would be nice to make these prop of the action itself but I // do not see a way to do this properly yet -- we can't get to // the action context from the action dynamically... + // XXX add doc per action... getHandlerDocStr: function(name){ var lst = this.getHandlers(name) var str = '' + var getTags = function(handler, p){ + return (handler.event_tag ? + object.normalizeIndent('// Event tag: ' + handler.event_tag) + p + : '') + + (handler.source_tag ? + object.normalizeIndent('// Source tag: ' + handler.source_tag) + p + : '') } + var getDoc = function(cur, p){ + return (cur.doc ? + '// --- .doc ---'+p + +'// '+ object.normalizeIndent(cur.doc).replace(/\n/g, p+'// ') +p + : '') + + (cur.long_doc ? + '// --- .long_doc ---'+p + +'// '+ object.normalizeIndent(cur.long_doc).replace(/\n/g, p+'// ') + p + : '') } + var handler = function(p){ if(lst.length == 0){ //str += p + '---' @@ -1142,7 +1900,10 @@ module.MetaActions = { if(cur.pre){ str += p - + normalizeTabs(cur.pre.toString()).replace(/\n/g, p) + + getTags(cur.pre, p) + + getDoc(cur, p) + // code... + + object.normalizeIndent(cur.pre.toString()).replace(/\n/g, p) + p } @@ -1152,14 +1913,16 @@ module.MetaActions = { if(cur.post){ str += p + p - + normalizeTabs(cur.post.toString()).replace(/\n/g, p) + + getTags(cur.post, p) + + getDoc(cur, p) + // code... + + object.normalizeIndent(cur.post.toString()).replace(/\n/g, p) } } handler('\n|') - return str - }, + return str }, getHandlerDocHTML: function(name){ var lst = this.getHandlers(name) var res = $('
') @@ -1175,7 +1938,13 @@ module.MetaActions = { if(cur.pre){ p.append($('
').html(
-					normalizeTabs(cur.pre.toString())
+					// meta...
+					(cur.pre.event_tag ? 
+						object.normalizeIndent('// Event tag: ' + cur.pre.event_tag) + p : '')
+					+ (cur.pre.source_tag ? 
+						object.normalizeIndent('// Source tag: ' + cur.pre.source_tag) + p : '')
+					// code...
+					+ object.normalizeIndent(cur.pre.toString())
 						.replace(/return/g, 'return')))
 			}
 
@@ -1183,33 +1952,37 @@ module.MetaActions = {
 
 			if(cur.post){
 				p.append($('
').html(
-					normalizeTabs(cur.post.toString())))
+					// meta...
+					(cur.post.event_tag ? 
+						object.normalizeIndent('// Event source tag: ' + cur.post.event_tag) + p : '')
+					+ (cur.post.source_tag ? 
+						object.normalizeIndent('// Source tag: ' + cur.post.source_tag) + p : '')
+					// code...
+					+ object.normalizeIndent(cur.post.toString())))
 			}
 		}
 
 		handler(res)
 
-		return res
-	},
+		return res },
 
 
 	// This will create a .config in instances...
 	// NOTE: this makes Actions compatible with lib/object.js...
 	__init__: function(){
-		if(this.__proto__.config && !Object.hasOwnProperty(this, 'config')){
-			this.config = Object.create(this.__proto__.config)
-		}
-	},
+		this.__proto__.config 
+			&& !Object.hasOwnProperty(this, 'config')
+			&& (this.config = Object.create(this.__proto__.config)) },
 }
 
 
 var ActionSet =
 module.ActionSet =
-object.makeConstructor('ActionSet', MetaActions)
+object.Constructor('ActionSet', MetaActions)
 
 
 
-// An action set...
+// An action set constructor...
 //
 //	Actions()
 //	Actions(, )
@@ -1227,30 +2000,35 @@ object.makeConstructor('ActionSet', MetaActions)
 // 		 : [
 // 			,
 // 			,
-// 			
+// 			,
+// 			 | 
 // 		],
 //
 // 		// short doc only...
 // 		 : [
 // 			,
-// 			
+// 			 | 
 // 		],
 //
 // 		// only the code...
 // 		 : [
-// 			
+// 			 | 
 // 		],
 // 		...
 // 	}
 //
 //
 // NOTE: the action function is always last.
+// NOTE:  if given must be right before the function and must not
+// 		be a string...
 // NOTE: if  is not given, MetaActions will be used as default.
 //
 // For more documentation see: Action(..).
 //
 // XXX add doc, ldoc, tags and save them to each action...
 // XXX is .config processing correct here???
+// XXX do we need to handle methods in a special way???
+// XXX should this set the .source_tag???
 var Actions =
 module.Actions =
 function Actions(a, b){
@@ -1258,33 +2036,6 @@ function Actions(a, b){
 	var proto = b == null ? b : a
 	obj = obj || new ActionSet()
 
-	// NOTE: this is intentionally done only for own attributes...
-	Object.keys(obj).forEach(function(k){
-		// NOTE: we are not getting the attrs directly (vars = obj[k])
-		// 		as that will trigger the getters on an object that is
-		// 		not in a consistent state...
-		// NOTE: this will skip all the getters and setters, they will 
-		// 		be included as-is...
-		var arg = Object.getOwnPropertyDescriptor(obj, k).value
-
-		// skip non-arrays...
-		if(arg == null 
-				// XXX node?: for some magical reason when running this 
-				// 		from node console instanceof tests fail...
-				//|| !(arg instanceof Array)
-				|| arg.constructor.name != 'Array'
-				// and arrays the last element of which is not a function...
-				|| typeof(arg[arg.length-1]) != 'function'){
-				//|| !(arg[arg.length-1] instanceof Function)){
-			return
-		}
-
-		var func = arg.pop()
-
-		// create a new action...
-		obj[k] = new Action(k, arg[0], arg[1], func)
-	})
-
 	if(proto != null){
 		obj.__proto__ = proto
 
@@ -1294,17 +2045,48 @@ function Actions(a, b){
 		}
 	}
 
-	return obj
-}
+	// NOTE: this is intentionally done only for own attributes...
+	Object.keys(obj).forEach(function(k){
+		// NOTE: we are not getting the attrs directly (vars = obj[k])
+		// 		as that will trigger the getters on an object that is
+		// 		not in a consistent state...
+		// NOTE: this will skip all the getters and setters, they will 
+		// 		be included as-is...
+		var arg = Object.getOwnPropertyDescriptor(obj, k).value
+
+		// action/alias...
+		if(arg instanceof Array 
+				&& (arg[arg.length-1] instanceof Function
+					|| (typeof(arg[arg.length-1]) == typeof('str')
+						&& (arg[arg.length-1] == ''
+							// XXX should this be stricter???
+							|| (obj.isStringAction || isStringAction)(arg[arg.length-1])))) ){
+			obj[k] = arg[arg.length-1] instanceof Function ?
+				(new Action(k, arg))
+				: (new Alias(k, arg))
+		}
+	})
+
+	return obj }
+
 
 
 
 /*********************************************************************/
 
+// NOTE: this can only mix actions sets and MetaActions, i.e. only the 
+// 		actions, properties and .config will get handled...
+// NOTE: MetaActions is a special case, if given it will be used as the
+// 		prototype for the root object in the created chain...
+// 		...MetaActions order in the list has no effect.
+//
+// XXX what the mix order should be?
+// 		base, extending, surface		- order of application (current)
+// 		surface, extending, base		- python-like
 var mix =
 module.mix = 
 function(){
-	var args = [].slice.call(arguments)
+	var args = [...arguments]
 	var res = {}
 
 	// special case: if MetaActions is in the args then inherit the root
@@ -1329,8 +2111,8 @@ function(){
 		}
 	})
 
-	return res
-}
+	return res }
+
 
 
 
diff --git a/lib/features.js b/lib/features.js
index a4e8147..bd41924 100755
--- a/lib/features.js
+++ b/lib/features.js
@@ -14,16 +14,47 @@ var actions = module.actions = require('ig-actions')
 
 /*********************************************************************/
 
-var args2array = function(a){ return [].slice.call(a) } 
+var FeatureLinearizationError =
+module.FeatureLinearizationError = 
+function(data){
+	this.data = data
+	this.message = 'Failed to linearise.'
+	this.toString = function(){
+		return this.message
+	}
+}
+FeatureLinearizationError.prototype = Object.create(new Error)
+FeatureLinearizationError.prototype.constructor = FeatureLinearizationError
 
 
 
-/*********************************************************************/
+
+//---------------------------------------------------------------------
+// Base feature...
+//
+// 	Feature(obj)
+// 		-> feature
+//
+// 	Feature(feature-set, obj)
+// 		-> feature
+//
+// 	Feature(tag, obj)
+// 		-> feature
+//
+// 	Feature(tag, suggested)
+// 		-> feature
+//
+// 	Feature(tag, actions)
+// 		-> feature
+//
+// 	Feature(feature-set, tag, actions)
+// 		-> feature
+//
 //
 // Feature attributes:
 // 	.tag			- feature tag (string)
-// 					  this is used to identify the feature, its event handlers
-// 					  and DOM elements.
+// 					  this is used to identify the feature, its event 
+// 					  handlers and DOM elements.
 //
 // 	.title			- feature name (string | null)
 // 	.doc			- feature description (string | null)
@@ -34,17 +65,20 @@ var args2array = function(a){ return [].slice.call(a) }
 // 					  	- number
 // 					  	- null (0, default)
 // 					  features with higher priority will be setup first,
-// 					  features with the same priority will be run in order of
-// 					  occurrence.
+// 					  features with the same priority will be run in 
+// 					  order of occurrence.
 // 	.suggested		- list of optional suggested features, these are not 
 // 					  required but setup if available.
-// 					  This is useful for defining meta features but without
-// 					  making each sub-feature a strict dependency.
-// 	.depends		- feature dependencies -- tags of features that must setup
-// 					  before the feature (list | null)
+// 					  This is useful for defining meta features but 
+// 					  without making each sub-feature a strict dependency.
+// 	.depends		- feature dependencies -- tags of features that must 
+// 					  setup before the feature (list | null)
+// 					  NOTE: a feature can depend on an exclusive tag, 
+// 					  		this will remove the need to track which 
+// 					  		specific exclusive tagged feature is loaded...
 // 	.exclusive		- feature exclusivity tags (list | null)
-// 					  an exclusivity group enforces that only one feature in
-// 					  it will be run, i.e. the first / highest priority.
+// 					  an exclusivity group enforces that only one feature
+// 					  in it will be run, i.e. the first / highest priority.
 //
 // 	.actions		- action object containing feature actions (ActionSet | null)
 // 					  this will be mixed into the base object on .setup()
@@ -73,26 +107,48 @@ var args2array = function(a){ return [].slice.call(a) }
 // 	considered on setup...
 //
 //
-// XXX this could install the handlers in two locations:
-// 		- mixin if available...
-// 		- base object (currently implemented)
-// 		should the first be done?
-var FeatureProto =
-module.FeatureProto = {
+var Feature =
+module.Feature =
+object.Constructor('Feature', {
+	//__featureset__: Features,
+	__featureset__: null,
+
+	// Attributes...
 	tag: null,
 
-	isApplicable: function(actions){
-		return true
-	},
+	//title: null,
+	//doc: null,
+	//priority: null,
+	//exclusive: null,
+	//suggested: null,
+	//depends: null,
+	//actions: null,
+	//config: null,
+	//handlers: null,
 
-	getPriority: function(){
+
+	isApplicable: function(actions){ return true },
+
+
+	// API...
+	getPriority: function(human_readable){
 		var res = this.priority || 0
-		return res == 'high' ? 99
+		res = res == 'high' ? 99
 			: res == 'low' ? -99
 			: res == 'medium' ? 0
+			: res == 'normal' ? 0
 			: res
-	},
+		return human_readable ?
+				(res == 99 ? 'high'
+					: res == 0 ? 'normal'
+					: res == -99 ? 'low'
+					: res)
+			: res },
 
+	// XXX this could install the handlers in two locations:
+	// 		- mixin if available...
+	// 		- base object (currently implemented)
+	// 		should the first be done?
 	setup: function(actions){
 		var that = this
 
@@ -116,6 +172,14 @@ module.FeatureProto = {
 		if(this.config != null 
 				|| (this.actions != null 
 					&& this.actions.config != null)){
+			// sanity check -- warn of config shadowing...
+			if(this.config && this.actions && this.actions.config){
+				console.warn('Feature config shadowed: '
+					+'both .config (used) and .actions.config (ignored) are defined for:', 
+					this.tag, 
+					this)
+			}
+
 			var config = this.config = this.config || this.actions.config
 
 			if(actions.config == null){
@@ -129,545 +193,910 @@ module.FeatureProto = {
 
 		// custom setup...
 		// XXX is this the correct way???
-		if(this.hasOwnProperty('setup') && this.setup !== FeatureProto.setup){
+		if(this.hasOwnProperty('setup') && this.setup !== Feature.prototype.setup){
 			this.setup(actions)
 		}
 
 		return this
 	},
+
+	// XXX need to revise this...
+	// 		- .mixout(..) is available directly from the object while 
+	// 			.remove(..) is not...
+	// 		- might be a good idea to add a specific lifecycle actions to
+	// 			enable feautures to handle their removal correctly... 
 	remove: function(actions){
 		if(this.actions != null){
-			actions.mixout(this.actions)
+			actions.mixout(this.tag || this.actions)
 		}
 
 		if(this.handlers != null){
 			actions.off('*', this.tag)
 		}
 
-		if(this.hasOwnProperty('remove') && this.setup !== FeatureProto.remove){
+		// XXX
+		if(this.hasOwnProperty('remove') && this.setup !== Feature.prototype.remove){
 			this.remove(actions)
 		}
 
 		// remove feature DOM elements...
+		// XXX
 		actions.ribbons.viewer.find('.' + this.tag).remove()
 
 		return this
 	},
-}
 
 
-// XXX is hard-coded default feature-set a good way to go???
-//
-// 	Feature(obj)
-// 		-> feature
-//
-// 	Feature(feature-set, obj)
-// 		-> feature
-//
-// 	Feature(tag, obj)
-// 		-> feature
-//
-//
-// 	Feature(tag, actions)
-// 		-> feature
-//
-// 	Feature(feature-set, tag, actions)
-// 		-> feature
-//
-var Feature =
-module.Feature =
-function Feature(feature_set, tag, obj){
-	if(arguments.length == 2){
-		// Feature(, )
-		if(typeof(feature_set) == typeof('str')){
-			obj = tag
-			tag = feature_set
-			feature_set = Features
+	// XXX EXPERIMENTAL: if called from a feature-set this will add self
+	// 		to that feature-set...
+	// XXX do we need this to be .__new__(..) and not .__init__(..)
+	__new__: function(context, feature_set, tag, obj){
+		// NOTE: we need to account for context here -- inc length...
+		if(arguments.length == 3){
+			// Feature(, )
+			if(typeof(feature_set) == typeof('str')){
+				obj = tag
+				tag = feature_set
+				//feature_set = Features
+				// XXX EXPERIMENTAL...
+				feature_set = context instanceof FeatureSet ?
+					context
+					: (this.__featureset__ || Features)
 
-		// Feature(, )
-		} else {
-			obj = tag
-			tag = null
+			// Feature(, )
+			} else {
+				obj = tag
+				tag = null
+			}
+
+		// Feature()
+		// NOTE: we need to account for context here -- inc length...
+		} else if(arguments.length == 2){
+			obj = feature_set
+			//feature_set = Features
+			// XXX EXPERIMENTAL...
+			feature_set = context instanceof FeatureSet ?
+				context
+				: (this.__featureset__ || Features)
 		}
 
-	// Feature()
-	} else if(arguments.length == 1){
-		obj = feature_set
-		feature_set = Features
-	}
+		if(tag != null && obj.tag != null && obj.tag != tag){
+			throw 'Error: tag and obj.tag mismatch, either use one or both must match.' }
 
-	if(tag != null && obj.tag != null && obj.tag != tag){
-		throw 'Error: tag and obj.tag mismatch, either use one or both must match.'
-	}
+		// action...
+		if(obj instanceof actions.Action){
+			if(tag == null){
+				throw 'Error: need a tag to make a feature out of an action' }
+			var f = {
+				tag: tag,
+				actions: obj,
+			}
+			obj = f
 
-	// action...
-	if(obj instanceof actions.Action){
-		if(tag == null){
-			throw 'Error: need a tag to make a feature out of an action'
+		// meta-feature...
+		} else if(obj.constructor === Array){
+			if(tag == null){
+				throw 'Error: need a tag to make a meta-feature'
+			}
+			var f = {
+				tag: tag,
+				suggested: obj,
+			}
+			obj = f
 		}
-		var f = {
-			tag: tag,
-			actions: obj,
+
+		// feature-set...
+		if(feature_set){
+			feature_set[obj.tag] = obj
 		}
-		obj = f
 
-	// meta-feature...
-	} else if(obj.constructor === Array){
-		if(tag == null){
-			throw 'Error: need a tag to make a meta-feature'
-		}
-		var f = {
-			tag: tag,
-			suggested: obj,
-		}
-		obj = f
-
-	// feature...
-	} else {
-		obj.__proto__ = FeatureProto
-	}
-
-	if(feature_set){
-		feature_set[obj.tag] = obj
-	}
-
-	return obj
-}
-Feature.prototype = FeatureProto
-Feature.prototype.constructor = Feature
+		return obj
+	},
+})
 
 
-var FeatureSetProto = {
-	__feature__: Feature,
-	__actions__: actions.Actions,
 
+//---------------------------------------------------------------------
+
+var FeatureSet =
+module.FeatureSet = 
+object.Constructor('FeatureSet', {
 	// if true, .setup(..) will report things it's doing... 
 	__verbose__: null,
 
+
+	__actions__: actions.Actions,
+
+	// NOTE: a feature is expected to write a reference to itself to the 
+	// 		feature-set (context)...
+	Feature: Feature,
+
+
 	// List of registered features...
 	get features(){
 		var that = this
 		return Object.keys(this)
 			.filter(function(e){ 
 				return e != 'features' 
-					&& that[e] instanceof Feature }) 
+					&& that[e] instanceof Feature }) },
+
+	// build exclusive groups...
+	//
+	// 	Get all exclusive tags...
+	// 	.getExclusive()
+	// 	.getExclusive('*')
+	// 		-> exclusive
+	//
+	// 	Get specific exclusive tags...
+	// 	.getExclusive(tag)
+	// 	.getExclusive([tag, ..])
+	// 		-> exclusive
+	//
+	// If features is given, only consider the features in list.
+	// If rev_exclusive is given, also build a reverse exclusive feature
+	// list.
+	//
+	// output format:
+	// 	{
+	// 		exclusive-tag: [
+	// 			feature-tag,
+	// 			...
+	// 		],
+	// 		...
+	// 	}
+	//
+	getExclusive: function(tag, features, rev_exclusive, isDisabled){
+		tag = tag == null || tag == '*' ? '*'
+			: tag instanceof Array ? tag
+			: [tag]
+
+		features = features || this.features
+		rev_exclusive = rev_exclusive || {}
+
+		var that = this
+		var exclusive = {}
+		features
+			.filter(function(f){ 
+				return !!that[f].exclusive 
+					&& (!isDisabled || !isDisabled(f)) })
+			.forEach(function(k){
+				var e = that[k].exclusive
+				;((e instanceof Array ? e : [e]) || [])
+					.forEach(function(e){
+						// skip tags not explicitly requested...
+						if(tag != '*' && tag.indexOf(e) < 0){
+							return
+						}
+						exclusive[e] = (exclusive[e] || []).concat([k]) 
+						rev_exclusive[k] = (rev_exclusive[k] || []).concat([e]) }) })
+		return exclusive
 	},
 
-	// Build list of features...
+	// Build list of features in load order...
 	//
-	//	Build list of all features for an empty object...
-	//	.buildFeatureList()
-	//	.buildFeatureList({})
-	//	.buildFeatureList({}, '*')
-	//		-> data
+	// 	.buildFeatureList()
+	// 	.buildFeatureList('*')
+	// 		-> data
 	//
-	//	Build a list of features for a specific root feature and object...
-	//	.buildFeatureList(object, feature)
-	//		-> data
+	// 	.buildFeatureList(feature-tag)
+	// 		-> data
 	//
-	//	Build a list of features for a specific set of root features and object...
-	//	.buildFeatureList(object, [feature, ..])
-	//		-> data
-	//		NOTE: to disable a feature and all of it's dependants prefix
-	//			it's tag with '-' in the list.
-	//			e.g. including 'some-feature' will include the feature
-	//			and its dependants while '-some-feature' will remove
-	//			it and it's dependants.
+	// 	.buildFeatureList([feature-tag, .. ])
+	// 		-> data
+	//
+	// 	.buildFeatureList(.., filter)
+	// 		-> data
 	//
 	//
-	// This will build from user input a loadable list of features taking 
-	// into account feature dependencies, priorities and suggestions.
+	// Requirements:
+	//	- features are pre-sorted by priority, original order is kept 
+	//		where possible
+	//	- a feature is loaded strictly after it's dependencies
+	//	- features depending on inapplicable feature(s) are also 
+	//		inapplicable (recursive up)
+	//	- inapplicable features are not loaded
+	//	- missing dependency -> missing dependency error
+	//	- suggested features (and their dependencies) do not produce 
+	//		dependency errors, unless explicitly included in dependency 
+	//		graph (i.e. explicitly depended on by some other feature)
+	//	- features with the same exclusive tag are grouped into an 
+	//		exclusive set
+	//	- only the first feature in an exclusive set is loaded, the rest
+	//		are *excluded*
+	//	- exclusive tag can be used to reference (alias) the loaded 
+	//		feature in exclusive set (i.e. exclusive tag can be used as 
+	//		a dependency)
 	//
-	// Roughly this is done in this order starting with the given features:
-	// 	- include all dependencies (recursively)
-	// 	- include all suggested features (recursively)
-	// 	- sort features by priority
-	// 	- sort features by dependency
-	// 	- check for feature applicability
-	// 	- remove non-applicable features and all dependants (recursively)
-	// 	- remove disabled features and all dependants (recursively)
-	// 	- check and resolve exclusivity conflicts (XXX needs revision)
-	// 	- check for missing features and dependencies
+	// NOTE: an exclusive group name can be used as an alias.
+	// NOTE: if an alias is used and no feature from that exclusive group
+	// 		is explicitly included then the actual loaded feature will 
+	// 		depend on the load order, which in an async world is not 
+	// 		deterministic...
+	//
+	//
+	// Algorithm:
+	// 	- expand features:
+	// 		- handle dependencies (detect loops)
+	// 		- handle suggestions
+	// 		- handle explicitly disabled features (detect loops)
+	// 		- handle exclusive feature groups/aliases (handle conflicts)
+	// 	- sort list of features:
+	// 		- by priority
+	// 		- by dependency (detect loops/errors)
 	//
 	//
 	// Return format:
 	// 	{
-	// 		// list of input features...
-	// 		input: [ .. ],
+	//		// input feature feature tags...
+	//		input: [ .. ],
 	//
-	//		// features in correct load order...
+	//		// output list of feature tags...
 	//		features: [ .. ],
 	//
-	//		// features disabled explicitly and their dependants...
+	//		// disabled features...
 	//		disabled: [ .. ],
-	//		// unapplicable features and their dependants...
-	//		unapplicable: [ .. ],
-	//
-	//		// features removed due to exclusivity conflict...
+	//		// exclusive features that got excluded... 
 	//		excluded: [ .. ],
 	//
-	//		missing: {
-	//			// features explicitly given by user but missing...
-	//			USER: [ .. ],
-	//			// missing  dependencies...
-	//			: [ .. ],
-	//			...
+	//		// Errors...
+	//		error: null | {
+	//			// fatal/recoverable error indicator...
+	//			fatal: bool,
+	//
+	//			// missing dependencies...
+	//			// NOTE: this includes tags only included by .depends and 
+	//			//		ignores tags from .suggested...
+	//			missing: [ .. ],
+	//
+	//			// exclusive feature conflicts...
+	//			// This will include the explicitly required conflicting
+	//			// exclusive features.
+	//			// NOTE: this is not an error, but indicates that the 
+	//			//		system tried to fix the state by disabling all
+	//			//		but the first feature.
+	//			conflicts: {
+	//				exclusive-tag: [ feature-tag, .. ],
+	//				..
+	//			},
+	//
+	//			// detected dependency loops (if .length > 0 sets fatal)...
+	//			loops: [ .. ],
+	//
+	//			// sorting loop overflow error (if true sets fatal)...
+	//			sort_loop_overflow: bool,
 	//		},
-	//		conflicts: {
-	//			XXX
+	//
+	//		// Introspection...
+	//		// index of features and their list of dependencies...
+	//		depends: {
+	//			feature-tag: [ feature-tag, .. ],
+	//			..
+	//		},
+	//		// index of features and list of features depending on them...
+	//		// XXX should this include suggestions or should we do them 
+	//		//		in a separate list...
+	//		depended: { 
+	//			feature-tag: [ feature-tag, .. ],
+	//			..
 	//		},
 	// 	}
 	//
-	//
-	// NOTE: obj (action set) here is used only for applicability testing...
-	// NOTE: some feature applicability checks (.isApplicable(..)) may 
-	// 		require a real action set, thus for correct operation one 
-	// 		should be provided.
-	// NOTE: all feature sorting is done maintaining relative feature order
-	// 		when possible...
-	// NOTE: meta-features are not included in the list as they do not 
-	// 		need to be setup.
-	// 		...this is because they are not Feature objects.
-	//
-	// XXX should meta-features be MetaFeature objects???
-	// XXX not sure about handling excluded features (see inside)...
-	// XXX add dependency loops to .conflicts...
-	// XXX might be a good idea to check dependency loops on feature 
-	// 		construction, too... (???)
-	buildFeatureList: function(obj, lst){
-		var that = this
-		obj = obj || {}
-
-		lst = (lst == null || lst == '*') ? this.features : lst
+	// XXX should exclusive conflicts resolve to first (current state)
+	//		feature or last in an exclusive group???
+	// XXX PROBLEM: exclusive feature trees should be resolved accounting 
+	// 		feature applicablility...
+	// 		...in this approach it is impossible...
+	// 		...one way to fix this is to make this interactively check 
+	// 		applicability, i.e. pass a context and check applicablility 
+	// 		when needed...
+	buildFeatureList: function(lst, isDisabled){
+		var all = this.features
+		lst = (lst == null || lst == '*') ? all : lst
 		lst = lst.constructor !== Array ? [lst] : lst
 
-		var input = lst.slice()
-		var disabled = [] 
-		var excluded = []
-		var unapplicable = []
-		var missing = {}
-		var conflicts = {}
+		//isDisabled = isDisabled || function(){ return false }
 
-		var exclusive = {}
+		var that = this
 
-
-		// reverse dependency cache... 
-		var dependants = {}
-
-		// build dependency list...
-		var _buildDepList = function(n, seen){
-			seen = seen || []
-			return seen.indexOf(n) >= 0 ? []
-				: seen.push(n) && dependants[n] ? []
-					.concat.apply(
-						dependants[n], 
-						dependants[n]
-							.map(function(n){ return _buildDepList(n, seen) }))
-				: []
+		// Pre-sort exclusive feature by their occurrence in dependency
+		// tree...
+		//
+		// NOTE: there can be a case when an exclusive alias is used but
+		//		no feature in a group is loaded, in this case which 
+		//		feature is actually loaded depends on the load order...
+		var sortExclusive = function(features){
+			var loaded = Object.keys(features)
+			Object.keys(exclusive)
+				.forEach(function(k){
+					exclusive[k] = exclusive[k]
+						.map(function(e, i){ return [e, i] })
+						.sort(function(a, b){
+							var i = loaded.indexOf(a[0])
+							var j = loaded.indexOf(b[0]) 
+							// keep the missing at the end...
+							i = i < 0 ? Infinity : i
+							j = j < 0 ? Infinity : j
+							// NOTE: Infinity - Infinity is NaN, so we need 
+							// 		to guard against it...
+							return i - j || 0 })
+						.map(function(e){ return e[0] }) })
 		}
 
-
-		// missing stage 1: check if all user included features exist...
-		// NOTE: we'll ignore missing disabled features too...
-		lst.forEach(function(n){
-			if(!that[n] && n[0] != '-'){
-				var m = missing['USER'] = missing['USER'] || []
-				m.push(n)
-			}
-		})
-
-		// include all dependencies...
+		// Expand feature references (recursive)...
 		//
-		// NOTE: this should never fall into an infinite loop as we do 
-		// 		not include feature already seen...
-		// 		...unless there is an infinite number of features, but 
-		// 		I'll try to avoid that big a feature creep.
-		// XXX should we check for dependency loops here???
-		// 		...this would have been simple if this was a recursion
-		// 		(just check if cur is in path), but here it is not 
-		// 		trivial...
-		for(var i=0; i < lst.length; i++){
-			var k = lst[i]
+		// NOTE: closures are not used here as we need to pass different
+		// 		stuff into data in different situations...
+		var expand = function(target, lst, store, data, _seen){
+			data = data || {}
+			_seen = _seen || []
 
-			// skip disabled or missing features....
-			if(k[0] == '-' || !that[k]){
-				continue
-			}
+			// clear disabled...
+			// NOTE: we do as a separate stage to avoid loading a 
+			// 		feature before it is disabled in the same list...
+			lst = data.disabled ?
+				lst
+					.filter(function(n){
+						// feature disabled -> record and skip...
+						if(n[0] == '-'){
+							n = n.slice(1)
+							if(_seen.indexOf(n) >= 0){
+								// NOTE: a disable loop is when a feature tries to disable
+								// 		a feature up in the same chain...
+								// XXX should this break or accumulate???
+								console.warn(`Disable loop detected at "${n}" in chain: ${_seen}`)
+								var loop = _seen.slice(_seen.indexOf(n)).concat([n])
+								data.disable_loops = (data.disable_loops || []).push(loop)
+								return false
+							}
+							// XXX STUB -- need to resolve actual loops and 
+							// 		make the disable global...
+							if(n in store){
+								console.warn('Disabling a feature after it is loaded:', n, _seen)
+							}
+							data.disabled.push(n)
+							return false
+						}
+						// skip already disabled features...
+						if(data.disabled.indexOf(n) >= 0){
+							return false
+						}
+						return true
+					})
+				: lst
 
-			var deps = that[k].depends || []
-			var refs = that[k].suggested || []
-			var excl = that[k].exclusive || []
-
-			deps.forEach(function(n){
-				// expand lst with dependencies....
-				lst.indexOf(n) < 0 && lst.push(n)
-
-				// build reverse dependency index...
-				var d = dependants[n] = dependants[n] || []
-				d.indexOf(k) < 0 && d.push(k)
-			})
-
-			// expand lst with suggenstions....
-			refs.forEach(function(n){
-				lst.indexOf(n) < 0 && lst.push(n)
-			})
-
-			// build exclusive table...
-			excl.forEach(function(n){
-				var l = exclusive[n] = exclusive[n] || []
-				l.indexOf(k) < 0 && l.push(k)
-			})
-		}
-
-		console.log('EXCL:', exclusive)
-
-		// sort features by priority or position...
-		lst = lst
-			// remove undefined and non-features...
-			.filter(function(n){ 
-				// feature disabled -> record and skip...
-				if(n[0] == '-'){
-					disabled.push(n.slice(1))
-					return false
-				}
-				var f = that[n]
-				// feature not defined or is not a feature...
-				if(f == null || !(f instanceof Feature)){
-					return false
-				}
-				// check applicability...
-				if(f.isApplicable && !f.isApplicable.call(that, obj)){
-					unapplicable.push(n)
-					return false
-				}
-				return true
-			})
-			// remove disabled...
-			.filter(function(e){ return disabled.indexOf(e) < 0 })
-			// build the sort table: [ , ,  ]
-			.map(function(e, i){ return [ that[e].getPriority(), i, e ] })
-			// sort by priority then index...
-			// NOTE: JS compares lists as strings so we have to compare 
-			// 		the list manually...
-			.sort(function(a, b){ return a[0] - b[0] || a[1] - b[1] })
-			// cleanup -- drop the sort table...
-			.map(function(e){ return e[2] })
-
-		// remove dependants on not applicable and on disabled...
-		var _unapplicable = unapplicable.slice()
-		var _disabled = disabled.slice()
-		// build the full lists of features to remove...
-		_unapplicable
-			.forEach(function(n){ _unapplicable = _unapplicable.concat(_buildDepList(n)) })
-		_disabled
-			.forEach(function(n){ _disabled = _disabled.concat(_buildDepList(n)) })
-		// clear...
-		// NOTE: in case of intersection disabled has priority...
-		lst = lst
-			.filter(function(n){
-				return _disabled.indexOf(n) >= 0 ?
-						disabled.push(n) && false
-					: _unapplicable.indexOf(n) >= 0 ?
-						unapplicable.push(n) && false
-					: true })
-
-		// missing stage 2: dependencies...
-		lst.forEach(function(k){
-			(that[k].depends || []).forEach(function(d){
-				// NOTE: we do not need to check disabled or unapplicable
-				// 		here as if the feature depended on dropped feature
-				// 		it would have been already dropped too...
-				if(!that[k]){
-					var m = missing[k] = missing[k] || []
-					m.push(d)
-				}
-			})
-		})
-
-		// check exclusive -> excluded...
-		//
-		// NOTE: this is the right spot for this, just after priority 
-		// 		sorting and clearing but before dependency sorting.
-		//
-		// XXX do we need to clear dependencies pulled by excluded features???
-		// 		ways to go:
-		// 			- drop excluded and continue (current state)
-		// 			- disable excluded, add to original input and rebuild
-		// 			- err and let the user decide
-		var _exclusive = []
-		lst = lst.filter(function(n){
-			var e = that[n]
-
-			// keep non-exclusive stuff...
-			if(!e || e.exclusive == null){
-				return true
-			}
-
-			// count the number of exclusive features already present...
-			var res = e.exclusive
-				.filter(function(n){
-					if(_exclusive.indexOf(n) < 0){
-						_exclusive.push(n)
+			// traverse the tree...
+			lst
+				// normalize the list -- remove non-features and resolve aliases...
+				.map(function(n){ 
+					var f = that[n]
+					// exclusive tags...
+					if(f == null && data.exclusive && n in data.exclusive){
+						store[n] = null
 						return false
 					}
-					return true
+					// feature not defined or is not a feature...
+					if(f == null){
+						data.missing 
+							&& data.missing.indexOf(n) < 0
+							&& data.missing.push(n)
+						return false
+					}
+					return n
 				})
-				.length == 0
+				.filter(function(e){ return e })
 
-			!res 
-				&& excluded.push(n)
-				// warn the user...
-				// XXX not sure if this is the right place for this...
-				&& console.warn(
-					'Excluding unaplicable:', n, '(reccomended to exclude manually)')
+				// traverse down...
+				.forEach(function(f){
+					// dependency loop detection...
+					if(_seen.indexOf(f) >= 0){
+						var loop = _seen.slice(_seen.indexOf(f)).concat([f])
+						data.loops 
+							&& data.loops.push(loop)
+						return
+					}
 
-			return res
-		})
+					// skip already done features...
+					if(f in store){
+						return
+					}
 
-		// sort by dependency...
-		var l = lst.length
-		// get maximum possible length...
-		// ...the worst case length appears to be (for full reversal):
-		// 		S(2*(n-1) + 1)
-		// 			S = n => n > 0 ? 2*(n-1)+1 + S(n-1) : 0
-		// 			S = n => n > 0 ? 2*n-1 + S(n-1) : 0
-		//
-		// 		2 * S(n) - n
-		// 			S = n => n > 0 ? n + S(n-1) : 0
-		// 			f = n => 2 * S(n) - n
-		//
-		//		N^2 + C
-		//			S = n => n * n
-		//
-		// NOTE: this is the brute force way to check if we have a 
-		// 		dependency loop, need something faster...
-		//
-		// XXX is O(n^2) good enough worst case here?
-		// 		...at this point I think it is acceptable as we'll not 
-		// 		expect dependency graphs too saturated, and the average 
-		// 		complexity is far better...
-		var max = l * l
+					//var feature = store[f] = that[f]
+					var feature = that[f]
+					if(feature){
+						var _lst = []
 
-		for(var i=0; i < lst.length; i++){
-			var k = lst[i]
-			var depends = that[k].depends || []
+						// merge lists...
+						;(target instanceof Array ? target : [target])
+							.forEach(function(t){
+								_lst = _lst.concat(feature[t] || [])
+							})
+						store[f] = _lst 
 
-			// list of dependencies to move...
-			var move = []
-
-			lst
-				.slice(0, i)
-				.forEach(function(n, j){
-					// if n is a dependency of k, prepare to move...
-					if(depends.indexOf(n) >= 0){
-						delete lst[j] 
-						move.push(n)
+						// traverse down...
+						expand(target, _lst, store, data, _seen.concat([f]))
 					}
 				})
 
-			// move the dependencies after k...
-			// NOTE: this will keep the order within the dependencies...
-			move.length > 0
-				&& lst.splice.apply(lst, [i+1, 0].concat(move))
-
-			// check for cyclic dependencies...
-			// XXX loop signs:
-			// 		- the tail length stops changing -- we stop progressing to list end
-			// 		- the loop is packed
-			// 			- each element includes a set of dependencies
-			// 			- this set is of the same length when at a specific element
-			// 		- we only shift the same set of N elements over N iterations
-			// 		- ...
-			if(lst.length >= max){
-				// XXX get the actual cycle...
-				console.error('Feature cyclic dependency...')
-				break
-			}
+			return store
 		}
 
-		// cleanup after sort...
-		lst = lst
-			// remove undefined and non-features...
-			.filter(function(e){ 
-				return that[e] != null && that[e] instanceof Feature })
+		// Expand feature dependencies and suggestions recursively...
+		//
+		// NOTE: this relies on the following values being in the closure:
+		// 		loops			- list of loop chains found
+		// 		disable_loops	- disable loops
+		// 							when a feature containing a disable 
+		// 							directive gets disabled as a result
+		// 		disabled		- list of disabled features
+		// 		missing			- list of missing features
+		// 		missing_suggested
+		// 						- list of missing suggested features and
+		// 							suggested feature dependencies
+		// 		exclusive		- exclusive feature index
+		// 		suggests		- index of feature suggestions (full)
+		// 		suggested		- suggested feature dependency index
+		// NOTE: the above containers will get updated as a side-effect.
+		// NOTE: all of the above values are defined near the location 
+		// 		they are first used/initiated...
+		// NOTE: closures are used here purely for simplicity and conciseness
+		// 		as threading data would not add any flexibility but make 
+		// 		the code more complex...
+		var expandFeatures = function(lst, features){
+			features = features || {}
+
+			// feature tree...
+			var expand_data = {
+				loops: loops, 
+				disabled: disabled, 
+				disable_loops: disable_loops, 
+				missing: missing,
+				exclusive: exclusive,
+			}
+
+			features = expand('depends', lst, features, expand_data)
+
+			// suggestion list...
+			//	...this will be used to check if we need to break on missing 
+			//	features, e.g. if a feature is suggested we can silently skip 
+			//	it otherwise err...
+			//
+			// NOTE: this stage does not track suggested feature dependencies...
+			// NOTE: we do not need loop detection active here...
+			var s = expand('suggested', Object.keys(features), {}, 
+				{ 
+					disabled: disabled,
+					missing: missing_suggested,
+				})
+			s = Object.keys(s)
+				.filter(function(f){ 
+					// populate the tree of feature suggestions...
+					suggests[f] = s[f]
+					// filter out what's in features already...
+					return !(f in features) })
+			// get suggestion dependencies...
+			// NOTE: we do not care bout missing here...
+			s = expand('depends', s, {}, 
+				{ 
+					loops: loops, 
+					disabled: disabled, 
+					disable_loops: disable_loops, 
+					exclusive: exclusive,
+					missing: missing_suggested,
+				})
+			Object.keys(s)
+				.forEach(function(f){ 
+					// keep only suggested features -- diff with features...
+					if(f in features){
+						delete s[f]
+
+					// mix suggested into features...
+					} else {
+						features[f] = s[f]
+						suggested[f] = (s[f] || []).slice()
+					}
+				})
+
+			sortExclusive(features)
+
+			return features
+		}
+
+
+		//--------------------- Globals: filtering / exclusive tags ---
+
+		var loops = []
+		var disable_loops = []
+		var disabled = []
+		var missing = []
+		var missing_suggested = []
+		var suggests = {}
+		var suggested = {}
+
+		// user filter...
+		// NOTE: we build this out of the full feature list...
+		disabled = disabled
+			.concat(isDisabled ? all.filter(isDisabled) : [])
+
+		// build exclusive groups...
+		// XXX need to sort the values to the same order as given features...
+		var rev_exclusive = {}
+		var exclusive = this.getExclusive('*', all, rev_exclusive, isDisabled)
+
+
+		//-------------------------------- Stage 1: expand features ---
+		var features = expandFeatures(lst)
+
+
+		//-------------------------------- Exclusive groups/aliases ---
+		// Handle exclusive feature groups and aliases...
+		var conflicts = {}
+		var done = []
+		var to_remove = []
+		Object.keys(features)
+			.forEach(function(f){
+				// alias...
+				while(f in exclusive && done.indexOf(f) < 0){
+					var candidates = (exclusive[f] || [])
+						.filter(function(c){ return c in features })
+
+					// resolve alias to non-included feature...
+					if(candidates.length == 0){
+						var target = exclusive[f][0]
+
+						// expand target to features...
+						expandFeatures([target], features)
+
+					// link alias to existing feature...
+					} else {
+						var target = candidates[0]
+					}
+
+					// remove the alias...
+					// NOTE: exclusive tag can match a feature tag, thus
+					// 		we do not want to delete such tags...
+					// NOTE: we are not removing to_remove here as they may
+					// 		get added/expanded back in by other features...
+					!(f in that)
+						&& to_remove.push(f)
+					// replace dependencies...
+					Object.keys(features)
+						.forEach(function(e){
+							var i = features[e] ? features[e].indexOf(f) : -1
+							i >= 0
+								&& features[e].splice(i, 1, target)
+						})
+					f = target
+					done.push(f)
+				}
+				
+				// exclusive feature...
+				if(f in rev_exclusive){
+					// XXX handle multiple groups... (???)
+					var group = rev_exclusive[f]
+					var candidates = (exclusive[group] || [])
+						.filter(function(c){ return c in features })
+
+					if(!(group in conflicts) && candidates.length > 1){
+						conflicts[group] = candidates
+					}
+				}
+			})
+		// cleanup...
+		to_remove.forEach(function(f){ delete features[f] })
+		// resolve any exclusivity conflicts found...
+		var excluded = []
+		Object.keys(conflicts)
+			.forEach(function(group){
+				// XXX should this resolve to the last of the first feature???
+				excluded = excluded.concat(conflicts[group].slice(1))})
+		disabled = disabled.concat(excluded)
+
+
+		//--------------------------------------- Disabled features ---
+		// Handle disabled features and cleanup...
+
+		// reverse dependency index...
+		// 	...this is used to clear out orphaned features later and for
+		// 	introspection...
+		var rev_features = {}
+		Object.keys(features)
+			.forEach(function(f){
+				(features[f] || [])
+					.forEach(function(d){ 
+						rev_features[d] = (rev_features[d] || []).concat([f]) }) })
+
+		// clear dependency trees containing disabled features...
+		var suggested_clear = []
+		do {
+			var expanded_disabled = false
+			disabled
+				.forEach(function(d){ 
+					// disable all features that depend on a disabled feature...
+					Object.keys(features)
+						.forEach(function(f){ 
+							if(features[f]
+									&& features[f].indexOf(d) >= 0
+									&& disabled.indexOf(f) < 0){
+								expanded_disabled = true
+								disabled.push(f)
+							}
+						})
+
+					// delete the feature itself...
+					var s = suggests[d] || []
+					delete suggests[d]
+					delete features[d] 
+
+					// clear suggested...
+					s
+						.forEach(function(f){
+							if(disabled.indexOf(f) < 0 
+									// not depended/suggested by any of 
+									// the non-disabled features...
+									&& Object.values(features)
+										.concat(Object.values(suggests))
+											.filter(n => n.indexOf(f) >= 0)
+											.length == 0){
+								expanded_disabled = true
+								disabled.push(f)
+							}
+						})
+				})
+		} while(expanded_disabled)
+
+		// remove orphaned features...
+		// ...an orphan is a feature included by a disabled feature...
+		// NOTE: this should take care of missing features too...
+		Object.keys(rev_features)
+			.filter(function(f){
+				return rev_features[f]
+					// keep non-disabled and existing sources only...
+					.filter(function(e){ 
+						return !(e in features) || disabled.indexOf(e) < 0 })
+					// keep features that have no sources left, i.e. orphans...
+					.length == 0 })
+			.forEach(function(f){
+				console.log('ORPHANED:', f)
+				disabled.push(f)
+				delete features[f]
+			})
+
+
+		//---------------------------------- Stage 2: sort features ---
+
+		// Prepare for sort: expand dependency list in features... 
+		//
+		// NOTE: this will expand lst in-place...
+		// NOTE: we are not checking for loops here -- mainly because
+		// 		the input is expected to be loop-free...
+		var expanddeps = function(lst, cur, seen){
+			seen = seen || []
+			if(features[cur] == null){
+				return
+			}
+			// expand the dep list recursively...
+			// NOTE: this will expand features[cur] in-place while 
+			// 		iterating over it...
+			for(var i=0; i < features[cur].length; i++){
+				var f = features[cur][i]
+				if(seen.indexOf(f) < 0){
+					seen.push(f)
+
+					expanddeps(features[cur], f, seen)
+
+					features[cur].forEach(function(e){
+						lst.indexOf(e) < 0
+							&& lst.push(e)
+					})
+				}
+			}
+		}
+		// do the actual expansion...
+		var list = Object.keys(features)
+		list.forEach(function(f){ expanddeps(list, f) })
+
+		// sort by priority...
+		//
+		// NOTE: this will attempt to only move features with explicitly 
+		// 		defined priorities and keep the rest in the same order 
+		// 		when possible...
+		list = list
+			// format: 
+			// 	[ , ,  ]
+			.map(function(e, i){ 
+				return [e, i, (that[e] && that[e].getPriority) ? that[e].getPriority() : 0 ] })
+			.sort(function(a, b){ 
+				return a[2] - b[2] || a[1] - b[1] })
+			// cleanup...
+			.map(function(e){ return e[0] })
+			// sort by the order features should be loaded...
 			.reverse()
 
+		// sort by dependency...
+		//
+		// NOTE: this requires the list to be ordered from high to low 
+		// 		priority, i.e. the same order they should be loaded in...
+		// NOTE: dependency loops will throw this into and "infinite" loop...
+		var loop_limit = list.length + 1
+		do {
+			var moves = 0
+			if(list.length == 0){
+				break
+			}
+			list
+				.slice()
+				.forEach(function(e){
+					var deps = features[e]
+					if(!deps){
+						return
+					}
+					var from = list.indexOf(e)
+					var to = list
+						.map(function(f, i){ return [f, i] })
+						.slice(from+1)
+						// keep only dependencies...
+						.filter(function(f){ return deps.indexOf(f[0]) >= 0 })
+						.pop()
+					if(to){
+						// place after last dependency...
+						list.splice(to[1]+1, 0, e)
+						list.splice(from, 1)
+						moves++
+					}
+				})
+			loop_limit--
+		} while(moves > 0 && loop_limit > 0)
 
+
+		//-------------------------------------------------------------
+
+		// remove exclusivity tags that were resolved...
+		var isMissing = function(f){
+			return !(
+				// feature is a resolvable exclusive tag...
+				(exclusive[f] || []).length > 0 
+				// feature was resolved...
+				&& exclusive[f]
+					.filter(function(f){ return list.indexOf(f) >= 0 })
+					.length > 0) }
+		
 		return {
-			input: input,
+			input: lst,
 
-			features: lst,
+			features: list,
 
 			disabled: disabled,
-			unapplicable: unapplicable,
 			excluded: excluded,
 
-			missing: missing,
-			conflicts: conflicts,
+			// errors and conflicts...
+			error: (loops.length > 0 
+					|| Object.keys(conflicts).length > 0 
+					|| loop_limit <= 0 
+					|| missing.length > 0
+					|| missing_suggested.length > 0) ?
+				{
+					missing: missing.filter(isMissing),
+					missing_suggested: missing_suggested.filter(isMissing),
+					conflicts: conflicts,
+
+					// fatal stuff...
+					fatal: loops.length > 0 || loop_limit <= 0,
+					loops: loops,
+					sort_loop_overflow: loop_limit <= 0,
+				}
+				: null,
+
+			// introspection...
+			depends: features,
+			depended: rev_features,
+			suggests: suggests,
+			suggested: suggested,
+			//exclusive: exclusive,
 		}
 	},
 
-
+	// Setup features...
 	//
-	//	.setup(, [, ...])
-	//		-> 
+	//	Setup features on existing actions object...
+	//	.setup(actions, [feature-tag, ...])
+	//		-> actions
 	//
-	//	.setup([, ...])
-	//		-> 
+	//	Setup features on a new actions object...
+	//	.setup(feature-tag)
+	//	.setup([feature-tag, ...])
+	//		-> actions
 	//
+	//
+	// This will set .features on the object.
+	//
+	// .features format:
+	// 	{
+	// 		// the current feature set object...
+	// 		// XXX not sure about this -- revise...
+	// 		FeatureSet: feature-set,
+	//
+	// 		// list of features not applicable in current context...
+	// 		//
+	// 		// i.e. the features that defined .isApplicable(..) and it 
+	// 		// returned false when called.
+	// 		unapplicable: [ feature-tag, .. ],
+	//
+	// 		// output of .buildFeatureList(..)...
+	// 		...
+	// 	}
+	//
+	// NOTE: this will store the build result in .features of the output 
+	// 		actions object.
+	// NOTE: .features is reset even if a FeatureLinearizationError error
+	// 		is thrown.
 	setup: function(obj, lst){
-		// if no explicit object is given, just the list...
+		// no explicit object is given...
 		if(lst == null){
 			lst = obj
 			obj = null
 		}
-
 		obj = obj || (this.__actions__ || actions.Actions)()
+		lst = lst instanceof Array ? lst : [lst]
 
-		lst = lst.constructor !== Array ? [lst] : lst
-		var features = this.buildFeatureList(obj, lst)
-		lst = features.features
+		var unapplicable = []
+		var features = this.buildFeatureList(lst, 
+			(function(n){
+				// if we already tested unapplicable, no need to test again...
+				if(unapplicable.indexOf(n) >= 0){
+					return true
+				}
+				var f = this[n]
+				// check applicability if possible...
+				if(f && f.isApplicable && !f.isApplicable.call(this, obj)){
+					unapplicable.push(n)
+					return true
+				}
+				return false
+			}).bind(this)) 
+		features.unapplicable = unapplicable 
+		// cleanup disabled -- filter out unapplicable and excluded features...
+		// NOTE: this is done mainly for cleaner and simpler reporting 
+		// 		later on...
+		features.disabled = features.disabled
+			.filter(function(n){ 
+				return unapplicable.indexOf(n) < 0 
+					&& features.excluded.indexOf(n) < 0 })
 
-		// check for conflicts...
-		if(Object.keys(features.conflicts).length != 0
-				|| Object.keys(features.missing).length != 0){
-			var m = features.missing
-			var c = features.conflicts
+		// if we have critical errors and set verbose...
+		var fatal = features.error 
+			&& (features.error.loops.length > 0 || features.error.sort_loop_overflow)
 
-			// build a report...
-			var report = []
-
-			// missing deps...
-			Object.keys(m).forEach(function(k){
-				report.push(k + ': missing but required by:\n          ' + m[k].join(', '))
-			})
-			report.push('\n')
-
-			// conflicts...
-			Object.keys(c).forEach(function(k){
-				report.push(k + ': must setup after:\n          ' + c[k].join(', '))
-			})
-
-			// break...
-			throw 'Feature dependency error:\n    ' + report.join('\n    ') 
+		// report stuff...
+		if(this.__verbose__){
+			var error = features.error
+			// report dependency loops...
+			error.loops.length > 0
+				&& error.loops
+					.forEach(function(loop){
+						console.warn('Feature dependency loops detected:\n\t' 
+							+ loop.join('\n\t\t-> ')) })
+			// report conflicts...
+			Object.keys(error.conflicts)
+				.forEach(function(group){
+					console.error('Exclusive "'+ group +'" conflict at:', error.conflicts[group]) })
+			// report loop limit...
+			error.sort_loop_overflow
+				&& console.error('Hit loop limit while sorting dependencies!')
 		}
 
-		// report excluded features...
-		if(this.__verbose__ && features.excluded.length > 0){
-			console.warn('Excluded features due to exclusivity conflict:', 
-					features.excluded.join(', '))
-		}
+		features.FeatureSet = this
 
-		// report unapplicable features...
-		if(this.__verbose__ && features.unapplicable.length > 0){
-			console.log('Features not applicable in current context:', 
-					features.unapplicable.join(', '))
+		obj.features = features
+
+		// fatal error -- can't load...
+		if(fatal){
+			throw new FeatureLinearizationError(features)
 		}
 
 		// do the setup...
 		var that = this
-		var setup = FeatureProto.setup
-		lst.forEach(function(n){
+		var setup = Feature.prototype.setup
+		features.features.forEach(function(n){
 			// setup...
 			if(that[n] != null){
 				this.__verbose__ && console.log('Setting up feature:', n)
@@ -675,35 +1104,54 @@ var FeatureSetProto = {
 			}
 		})
 
-		// XXX should we extend this if it already was in the object???
-		obj.features = features
-
 		return obj
 	},
+
+	// XXX revise...
+	// 		...the main problem here is that .mixout(..) is accesible 
+	// 		directly from actions while the feature .remove(..) method
+	// 		is not...
+	// 		...would be nice to expose the API to actions directly or 
+	// 		keep it only in features...
 	remove: function(obj, lst){
 		lst = lst.constructor !== Array ? [lst] : lst
 		var that = this
 		lst.forEach(function(n){
 			if(that[n] != null){
 				console.log('Removing feature:', n)
-				that[n].remove(obj)
-			}
-		})
+				that[n].remove(obj) } }) },
+
+
+	// Generate a Graphviz graph from features...
+	//
+	// XXX experimental...
+	gvGraph: function(lst, dep){
+		lst = lst || this.features
+		dep = dep || this
+
+		var graph = ''
+		graph += 'digraph ImageGrid {\n'
+		lst
+			.filter(function(f){ return f in dep })
+			.forEach(function(f){
+				var deps = dep[f].depends || []
+
+				deps.length > 0 ?
+					deps.forEach(function(d){
+						graph += `\t"${f}" -> "${d}";\n` })
+					: (graph += `\t"${f}";\n`)
+			})
+		graph += '}'
+
+		return graph
 	},
-
-	// shorthand for: Feature(, ...)
-	// XXX should this return this?
-	Feature: function(){
-		return this.__feature__.apply(null, [this].concat(args2array(arguments)))
-	},
-}
+})
 
 
-var FeatureSet =
-module.FeatureSet = object.makeConstructor('FeatureSet', FeatureSetProto)
 
 
-//---------------------------------------------------------------------
+/*********************************************************************/
+// default feature set...
 
 var Features =
 module.Features = new FeatureSet()
diff --git a/lib/object.js b/lib/object.js
index 5caa4f7..f73b2d5 100755
--- a/lib/object.js
+++ b/lib/object.js
@@ -1,50 +1,403 @@
 /**********************************************************************
 * 
+* object.js
 *
+* Repo and docs:
+* 	https://github.com/flynx/object.js
+*
+*
+* XXX should this extend Object???
+* 		...if yes then it would also be logical to move Object.run(..) 
+* 		here...
 *
 **********************************************************************/
-((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)(
-function(require){ var module={} // makes module AMD/node compatible...
+((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
+(function(require){ var module={} // make module AMD/node compatible...
 /*********************************************************************/
+// Helpers...
+
+var TAB_SIZE =
+module.TAB_SIZE = 4
 
 
-
-/*********************************************************************/
-
-
-// Make a JavaScrip object constructor...	
+// Normalize indent...
 //
+// 	normalizeIndent(text)
+// 		-> text
+//
+//
+// This will remove common indent from each like of text, this is useful 
+// for printing function code of functions that were defined at deep 
+// levels of indent.
+//
+// NOTE: this will trim out both leading and trailing white-space.
+//
+// XXX is this the right place for this???
+// 		...when moving take care that ImageGrid's core.doc uses this...
+var normalizeIndent =
+module.normalizeIndent =
+function(text, tab_size){
+	tab_size = tab_size || TAB_SIZE
+	text = tab_size > 0 ?
+		text.replace(/\t/g, ' '.repeat(tab_size))
+		: text
+	var lines = text.split(/\n/)
+	var l = lines 
+		.reduce(function(l, e, i){
+			var indent = e.length - e.trimLeft().length
+			return e.trim().length == 0 
+					// ignore 0 indent of first line...
+					|| (i == 0 && indent == 0) ? l 
+				: l < 0 ? 
+					indent 
+				: Math.min(l, indent)
+		}, -1)
+	return lines
+		.map(function(line, i){ 
+			return i == 0 ? 
+				line 
+				: line.slice(l) })
+		.join('\n')
+		.trim() }
+
+
+
+//---------------------------------------------------------------------
+// Prototype chain content access...
+
+// Get a list of source objects for a prop/attr name...
+//
+// 	sources(obj, name)
+// 	sources(obj, name, callback)
+// 		-> list
+// 		-> []
+// 		
+// 	callback(obj)
+// 		-> true | 'stop'
+// 		-> ..
+// 		
+// 		
+// The callback(..) is called with each matching object.
+// 
+// The callback(..) can be used to break/stop the search, returning 
+// a partial list og matcges up untill and including the object 
+// triggering the stop.
+//
+//
+// NOTE: this go up the prototype chain, not caring about any role (
+// 		instance/class or instance/prototype) bounderies and depends 
+// 		only on the object given as the starting point.
+// 		It is possible to start the search from this, thus checking
+// 		for any overloading in the instance, though this approach is 
+// 		not very reusable....
+// NOTE: this will not trigger any props...
+var sources =
+module.sources =
+function(obj, name, callback){
+	var stop
+	var res = []
+	do {
+		if(obj.hasOwnProperty(name)){
+			res.push(obj) 
+			// handle callback...
+			stop = callback
+				&& callback(obj)
+			// stop requested by callback...
+			if(stop === true || stop == 'stop'){
+				return res } 
+		}
+		obj = obj.__proto__
+	} while(obj !== null)
+	return res }
+
+
+// Find the next parent attribute in the prototype chain.
+//
+// 	Get parent attribute value...
+// 	parent(proto, name)
+// 		-> value
+// 		-> undefined
+//
+// 	Get parent method...
+// 	parent(method, this)
+// 		-> meth
+// 		-> undefined
+//
+// 
+// The two forms differ in:
+// 	- in parent(method, ..) a method's .name attr is used for name.
+// 	- in parent(method, ..) the containing prototype is inferred.
+//
+// NOTE: there are cases where method.name is not set (e.g. anonymous 
+// 		function), so there a name should be passed explicitly...
+// NOTE: when passing a method it is recommended to pass an explicit 
+// 		reference to it relative to the constructor, i.e.:
+// 			Constructor.prototype.method
+// 		this will avoid relative resolution loops, for example: 
+// 			this.method 
+// 		deep in a chain will resolve to the first .method value visible 
+// 		from 'this', i.e. the top most value and not the value visible
+// 		from that particular level...
+//
+//
+// Example:
+// 		var X = object.Constructor('X', {
+//			__proto__: Y.prototype,
+//
+//			attr: 123,
+//
+// 			method: function(){
+// 				// get attribute...
+// 				var a = object.parent(X.prototype, 'attr')
+//
+// 				// get method...
+// 				var ret = object.parent(X.prototype.method, this)
+// 					.call(this, ...arguments)
+//
+// 				// ...
+// 			}
+// 		})
+//
+//
+// NOTE: in the general case this will get the value of the returned 
+// 		property/attribute, the rest of the way passive to props.
+// 		The method case will get the value of every method from 'this' 
+// 		and to the method after the match.
+// NOTE: this is super(..) replacement, usable in any context without 
+// 		restriction -- super(..) is restricted to class methods only...
+var parent =
+module.parent =
+function(proto, name){
+	// special case: method...
+	if(typeof(name) != typeof('str')){
+		that = name
+		name = proto.name
+		// get first matching source...
+		proto = sources(that, name, 
+				function(obj){ return obj[name] === proto })
+			.pop() }
+	// get first source...
+	var res = sources(proto, name, 
+			function(obj){ return 'stop' })
+		.pop() 
+	return res ?
+		// get next value...
+		res.__proto__[name] 
+		: undefined }
+
+
+// Find the next parent property descriptor in the prototype chain...
+//
+// 	parentProperty(proto, name)
+// 		-> prop-descriptor
+//
+//
+// This is like parent(..) but will get a property descriptor...
+//
+var parentProperty =
+module.parentProperty =
+function(proto, name){
+	// get second source...
+	var c = 0
+	var res = sources(proto, name, 
+			function(obj){ return c++ == 1 })
+		.pop() 
+	return res ?
+		// get next value...
+		Object.getOwnPropertyDescriptor(res, name)
+		: undefined }
+
+
+// Find the next parent method and call it...
+//
+// 	parentCall(proto, name, this, ...)
+// 	parentCall(meth, this, ...)
+// 		-> res
+// 		-> undefined
+//
+//
+// This also gracefully handles the case when no higher level definition 
+// is found, i.e. the corresponding parent(..) call will return undefined
+// or a non-callable.
+//
+// NOTE: this is just like parent(..) but will call the retrieved method,
+// 		essentially this is a shorthand to:
+// 			parent(proto, name).call(this, ...)
+// 		or:
+// 			parent(method, this).call(this, ...)
+// NOTE: for more docs see parent(..)
+var parentCall =
+module.parentCall =
+function(proto, name, that, ...args){
+	var meth = parent(proto, name)
+	return meth instanceof Function ?
+		meth.call(...( typeof(name) == typeof('str') ?
+			[...arguments].slice(2)
+			: [...arguments].slice(1) ))
+		: undefined }
+
+
+
+//---------------------------------------------------------------------
+// Mixin utils...
+// XXX should we add mixout(..) and friends ???
+
+// Mix a set of methods/props/attrs into an object...
+// 
+//	mixinFlat(root, object, ...)
+//		-> root
+//
+//
+// NOTE: essentially this is just like Object.assign(..) but copies 
+// 		properties directly rather than copying property values...
+var mixinFlat = 
+module.mixinFlat = 
+function(root, ...objects){
+	return objects
+		.reduce(function(root, cur){
+			Object.keys(cur)
+				.map(function(k){
+					Object.defineProperty(root, k,
+						Object.getOwnPropertyDescriptor(cur, k)) })
+			return root }, root) }
+
+
+// Mix sets of methods/props/attrs into an object as prototypes...
+//
+// 	mixin(root, object, ...)
+// 		-> root
+//
+//
+// This will create a new object per set of methods given and 
+// mixinFlat(..) the method set into this object leaving the 
+// original objects intact.
+// 
+// 		root <-- object1_copy <-- .. <-- objectN_copy
+// 				
+var mixin = 
+module.mixin = 
+function(root, ...objects){
+	return objects
+		.reduce(function(res, cur){
+			return module.mixinFlat(Object.create(res), cur) }, root) }
+
+
+
+//---------------------------------------------------------------------
+// Constructor...
+
+// Make an uninitialized instance object...
+//
+// 	makeRawInstance(context, constructor, ...)
+// 		-> instance
+//
+//
+// This will:
+// 	- construct an object
+// 		- if .__new__(..) is defined
+// 			-> call and use its return value
+//		- if prototype is a function or if .__call__(..) is defined
+//			-> use a wrapper function
+//		- else
+//			-> use {}
+// 	- link the object into the prototype chain
+//
+//
+// This will not call .__init__(..)
+//
+//
+// NOTE: context is only used when passeding to .__new__(..) if defined, 
+// 		and is ignored otherwise...
+// NOTE: as this is simply an extension to the base JavaScript protocol this
+// 		can be used to construct any object...
+// 		Example:
+// 			var O = function(){}
+// 			// new is optional...
+// 			var o = new makeRawInstance(null, O)
+// NOTE: .__new__(..) is intentionaly an instance method (contary to 
+// 		Python) this is done because there are no classes in JS and 
+// 		adding and instance constructor as a class method would create 
+// 		unneccessary restrictions both on the "class" object and on the 
+// 		instance...
+// 
+// XXX Q: should the context (this) in .__new__(..) be _constructor or 
+// 		.prototype???
+// 		... .prototype seems to be needed more often but through it we 
+// 		can't reach the actual constructor... but on the other hand we 
+// 		can (should?) always explicitly use it -- .__new__(..) is usually 
+// 		in the same scope + this makes it more reusable for chaining
+// 		.__new__(..) calls...
+// 		...currently it's .prototype...
+var makeRawInstance = 
+module.makeRawInstance =
+function(context, constructor, ...args){
+	var _mirror_doc = function(func, target){
+		Object.defineProperty(func, 'toString', {
+			value: function(...args){
+				return target.toString(...args) },
+			enumerable: false,
+		})
+		return func }
+
+	var obj =
+		// prototype defines .__new__(..)...
+		constructor.prototype.__new__ instanceof Function ?
+			constructor.prototype.__new__(context, ...args)
+		// callable instance -- prototype is a function...
+		// NOTE: we need to isolate the .prototype from instances...
+		: constructor.prototype instanceof Function ?
+			_mirror_doc(
+				function(){
+					return constructor.prototype
+						.call(obj, this, ...arguments) },
+				constructor.prototype)
+		// callable instance -- prototype defines .__call__(..)...
+		// NOTE: we need to isolate the .__call__ from instances...
+		: constructor.prototype.__call__ instanceof Function ?
+			_mirror_doc(
+				function(){
+					return constructor.prototype.__call__
+						.call(obj, this, ...arguments) },
+				constructor.prototype.__call__)
+		// default object base...
+		: {} 
+
+	// link to prototype chain...
+	obj.__proto__ = constructor.prototype
+	Object.defineProperty(obj, 'constructor', {
+		value: constructor,
+		enumerable: false,
+	})
+
+	return obj }
+
+
+// Make an object constructor function...
 //
 // 	Make a constructor with an object prototype...
-// 		makeConstructor(, )
-// 			-> constructor
-//
-// 	Make a constructor with an init function prototype...
-// 		makeConstructor(, )
+// 		Constructor(name, proto)
 // 			-> constructor
 //
 // 	Make a constructor with a prototype (object/function) and a class
 // 	prototype...
-// 		makeConstructor(, , )
-// 		makeConstructor(, , )
+// 		Constructor(name, class-proto, proto)
 // 			-> constructor
 // 			NOTE: the  defines a set of class methods and 
 // 					attributes.
 //
 //
-//
 // The resulting constructor can produce objects in one of these ways:
 //
-// 	Basic constructor use...
-// 		constructor()
+// 	Create instance...
+// 		constructor(..)
 // 		new constructor
-// 		new constructor()
+// 		new constructor(..)
 // 			-> instance
 //
-// 	Pass arguments to the constructor...
-// 		constructor([, ...])
-// 		new constructor([, ...])
-// 			-> instance
+//	Create raw/uninitialized instance...
+//		constructor.__rawinstance__(..)
+//		makeRawInstance(null, constructor, ..)
+//			-> raw-instance
 //
 //
 // All produced objects are instances of the constructor
@@ -53,28 +406,65 @@ function(require){ var module={} // makes module AMD/node compatible...
 //
 //
 //
-// Init protocol:
-// 	1) the base instance object is prepared (.__proto__ is set)
-// 	2) if  is present, then it is called with instance as 
-// 		context and passed the constructor arguments
-// 	3) if .__init__(..) is present, it is called with the instance
-// 		as context and passed the constructor arguments.
+// Create and initialization protocol:
+// 	1) raw instance is created:
+// 		a) constructor.__rawinstance__(..) / makeRawInstance(..) called:
+// 			- call .__new__(..) if defined and get return value as 
+// 				instance, or
+// 			- if .__call__(..) defined or prototype is a function, wrap 
+// 				it and use the wrapper function as instance, or
+// 			- create an empty object
+// 		b) instance linked to prototype chain
+// 			set .__proto__ to constructor.prototype
+// 	2) instance is initialized: 
+// 		call .__init__(..) if defined
+// 
+//
+// 
+// Special methods (constructor):
+//
+//  Handle uninitialized instance construction
+// 	.__rawinstance__(context, ...)
+// 		-> instance
+// 		NOTE: This is a shorthand to makeRawInstance(..) see it for 
+// 			details.
+// 
+// 
+// Special methods (.prototype):
+//
+// 	Create new instance object...
+// 	.__new__(context, ..)
+// 		-> object
+//
+// 	Handle instance call...
+// 	.__call__(context, ..)
+// 		-> ..
+//
+// 	Initialize instance object...
+// 	.__init__(..)
+// 		-> ..
+//
+//
+// NOTE: raw instance creation is defined by makeRawInstance(..) so see 
+// 		it for more info.
+// NOTE: raw instance creation can be completely overloaded by defining
+// 		.__rawinstance__(..) on the constructor.
 //
 //
 //
 // Inheritance:
 // 	A simple way to build C -> B -> A chain would be:
 //
-// 		var A = makeConstructor('A', {})
+// 		// NOTE: new is optional...
+// 		var A = new Constructor('A')
 //
-// 		// NOTE: the prototype is an instance and not a constructor,
-// 		//		this is obvious if one considers that in JS there are
-// 		//		no classes and inheritance is done via object prototypes
-// 		//		but this might be a gotcha to people coming from the 
-// 		//		class-object world.
-// 		var B = makeConstructor('B', A())
+// 		// NOTE: in a prototype chain the prototypes are "inherited"
+// 		// NOTE: JS has no classes and the prototype is just another 
+// 		//		object, the only difference is that it's used by the 
+// 		//		constructor to link other objects i.e. "instances" to...
+// 		var B = Constructor('B', {__proto__: A.prototype})
 //
-// 		var C = makeConstructor('C', B())
+// 		var C = Constructor('C', Objec.create(B.prototype))
 //
 // 		var c = C()
 //
@@ -89,12 +479,12 @@ function(require){ var module={} // makes module AMD/node compatible...
 //
 //
 // Motivation:
-// 	The general motivation here is to standardise the constructor protocol
-// 	and make a single simple way to go with minimal variation. This is due
-// 	to the JavaScript base protocol though quite simple, being too flexible
-// 	making it very involved to produce objects in a consistent manner by 
-// 	hand, especially in long running projects, in turn spreading all the 
-// 	refactoring over multiple sites and styles.
+// 	The general motivation here is to standardise the constructor 
+// 	protocol and make a single simple way to go with minimal variation. 
+// 	This is due to the JavaScript base protocol though quite simple, 
+// 	being too flexible making it very involved to produce objects in a 
+// 	consistent manner by hand, especially in long running projects, 
+// 	in turn spreading all the refactoring over multiple sites and styles.
 //
 // 	This removes part of the flexibility and in return gives us:
 // 		- single, well defined protocol
@@ -103,111 +493,74 @@ function(require){ var module={} // makes module AMD/node compatible...
 // 		- easy refactoring without touching the client code
 //
 //
-// NOTE: this sets the proto's .constructor attribute, this rendering it
-// 		not reusable, to use the same prototype for multiple objects clone
-// 		it via. Object.create(..) or copy it...
-//
-// XXX might be a good idea to be able to make an instance without 
-// 		initializing it...
-// 		...mainly for inheritance.
-// 		...would also be helpful in this case to call all the 
-// 		constructors in the chain
-var makeConstructor =
-module.makeConstructor =
-function makeConstructor(name, a, b){
+// NOTE: this sets the proto's .constructor attribute, thus rendering it
+// 		not reusable, to use the same prototype for multiple objects 
+// 		clone it via. Object.create(..) or copy it...
+// NOTE: to disable .__rawinstance__(..) handling set it to false in the 
+// 		class prototype...
+var Constructor = 
+module.Constructor =
+// shorthand...
+module.C =
+function Constructor(name, a, b){
 	var proto = b == null ? a : b
 	var cls_proto = b == null ? b : a
+	proto = proto || {}
 
+	// the actual constructor...
 	var _constructor = function Constructor(){
-		/*
-		// XXX BUG: if the constructor is called from it's instance this will 
-		// 		return the instance and not a new object...
-		// in case this is called as a function (without new)...
-		if(this.constructor !== _constructor){
-			// NOTE: the following does the job of the 'new' operator but
-			// 		with one advantage, we can now pass arbitrary args 
-			// 		in...
-			// 		This is equivalent to:
-			//			return new _constructor(json)
-			var obj = {}
-			obj.__proto__ = _constructor.prototype
-			// XXX for some reason this does not resolve from .__proto__
-			obj.constructor = _constructor
-			//obj.__proto__.constructor = _constructor
+		// create raw instance...
+		var obj = _constructor.__rawinstance__ ? 
+				_constructor.__rawinstance__(this, ...arguments)
+			: makeRawInstance(this, _constructor, ...arguments)
+		// initialize...
+		obj.__init__ instanceof Function
+			&& obj.__init__(...arguments)
+		return obj }
 
-		} else {
-			var obj = this
-		}
-		*/
-
-		// NOTE: the following does the job of the 'new' operator but
-		// 		with one advantage, we can now pass arbitrary args 
-		// 		in...
-		// 		This is equivalent to:
-		//			return new _constructor(json)
-		var obj = {}
-		obj.__proto__ = _constructor.prototype
-		// XXX for some reason this does not resolve from .__proto__
-		// XXX this also is a regular attr and not a prop...
-		//obj.constructor = _constructor
-		Object.defineProperty(obj, 'constructor', {
-			value: _constructor,
-			enumerable: false,
-		})
-		//obj.__proto__.constructor = _constructor
-
-		// explicit init...
-		if(proto instanceof Function){
-			proto.apply(obj, arguments)
-		}
-
-		// load initial state...
-		if(obj.__init__ != null){
-			obj.__init__.apply(obj, arguments)
-		}
-
-		return obj
-	}
-
-	/* XXX for some reason this works for the _constructor but all 
-	 * 		instances get the wrong name resolved...
-	Object.defineProperty(_constructor, 'name', {
-		value: name,
-	})
-	*/
-
-	// just in case the browser refuses to change the name, we'll make it
-	// a different offer ;)
-	if(_constructor.name == 'Constructor'){
-			// skip for chrome app...
-			//&& !(window.chrome && chrome.runtime && chrome.runtime.id)){
-		eval('_constructor = '+ _constructor
+	_constructor.name = name
+	// just in case the browser refuses to change the name, we'll make
+	// it a different offer ;)
+	_constructor.name == 'Constructor'
+		// NOTE: this eval(..) should not be a risk as its inputs are
+		// 		static and never infuenced by external inputs...
+		&& eval('_constructor = '+ _constructor
 				.toString()
 				.replace(/Constructor/g, name))
-	}
-
+	// set .toString(..)...
+	// NOTE: do this only if .toString(..) is not defined by user...
+	;((cls_proto || {}).toString() == ({}).toString())
+		&& Object.defineProperty(_constructor, 'toString', {
+			value: function(){ 
+				var args = proto.__init__ ?
+					proto.__init__
+						.toString()
+						.split(/\n/)[0]
+							.replace(/function\(([^)]*)\){.*/, '$1')
+					: ''
+				var code = proto.__init__ ?
+					proto.__init__
+						.toString()
+						.replace(/[^{]*{/, '{')
+					: '{ .. }'
+				return `${this.name}(${args})${normalizeIndent(code)}` },
+			enumerable: false,
+		})
 	_constructor.__proto__ = cls_proto
 	_constructor.prototype = proto
-	_constructor.prototype.constructor = _constructor
+	// generic raw instance constructor...
+	_constructor.__rawinstance__ instanceof Function
+		|| (_constructor.__rawinstance__ = 
+			function(context, ...args){
+				return makeRawInstance(context, this, ...args) })
 
-	return _constructor
-}
+	// set .prototype.constructor
+	Object.defineProperty(_constructor.prototype, 'constructor', {
+		value: _constructor,
+		enumerable: false,
+	})
 
-
-// super equivalent...
-//
-// 	superMethod(, ).call(this, ...)
-// 		-> 
-//
-// This will return a next method in inheritance chain after  by
-// its name ().
-// In the normal use-case  is the current class and 
-// is the name of the current method.
-var superMethod =
-module.superMethod =
-function superMethod(cls, meth){
-	return cls.prototype.__proto__[meth]
-}
+	return _constructor }