/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)( function(require){ var module={} // makes module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') var actions = module.actions = require('ig-actions') /*********************************************************************/ // XXX use object.Error as base when ready... var FeatureLinearizationError module.FeatureLinearizationError = object.Constructor('FeatureLinearizationError', Error, { get name(){ return this.constructor.name }, toString: function(){ return 'Failed to linearise' }, __init__: function(data){ this.data = data }, }) //--------------------------------------------------------------------- // 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. // // .title - feature name (string | null) // .doc - feature description (string | null) // // .priority - feature priority // can be: // - 'high' (99) | 'medium' (0) | 'low' (-99) // - number // - null (0, default) // features with higher priority will be setup first, // 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) // 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. // // .actions - action object containing feature actions (ActionSet | null) // this will be mixed into the base object on .setup() // and mixed out on .remove() // .config - feature configuration, will be merged with base // object's .config // NOTE: the final .config is an empty object with // .__proto__ set to the merged configuration // data... // .handlers - feature event handlers (list | null) // // // // .handlers format: // [ // [ , ], // ... // ] // // NOTE: both and must be compatible with // Action.on(..) // // // Feature applicability: // If feature.isApplicable(..) returns false then the feature will not be // considered on setup... // // var Feature = module.Feature = object.Constructor('Feature', { //__featureset__: Features, __featureset__: null, __verbose: null, get __verbose__(){ return this.__verbose == null && (this.__featureset__ || {}).__verbose__ }, set __verbose__(value){ this.__verbose = value }, // Attributes... tag: null, //title: null, //doc: null, //priority: null, //exclusive: null, //suggested: null, //depends: null, //actions: null, //config: null, //handlers: null, isApplicable: function(actions){ return true }, // API... getPriority: function(human_readable){ var res = this.priority || 0 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 HANDLERS this could install the handlers in two locations: // - the actions object... // - mixin if available... // - base object (currently implemented) // ...the handlers should theoreticly be stored neither in the // instance nor in the mixin but rather in the action-set itself // on feature creation... (???) // ...feels like user handlers and feature handlers should be // isolated... // XXX setting handlers on the .__proto__ breaks... setup: function(actions){ var that = this // mixin actions... // NOTE: this will only mixin functions and actions... if(this.actions != null){ this.tag ? // XXX HANDLERS actions.mixin(this.actions, {source_tag: this.tag, action_handlers: true}) : actions.mixin(this.actions, {action_handlers: true}) } //actions.mixin(this.actions, {source_tag: this.tag}) //: actions.mixin(this.actions) } /*/ XXX HANDLERS this is not needed if handlers are local to actions... // install handlers... if(this.handlers != null){ this.handlers.forEach(function([a, h]){ //actions.__proto__.on(a, that.tag, h) }) } actions.on(a, that.tag, h) }) } //*/ // merge config... // NOTE: this will merge the actual config in .config.__proto__ // keeping the .config clean for the user to lay with... if(this.config != null || (this.actions != null && this.actions.config != null)){ // sanity check -- warn of config shadowing... // XXX do we need this??? if(this.__verbose__ && this.config && (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){ actions.config = Object.create({}) } Object.keys(config) .forEach(function(n){ // NOTE: this will overwrite existing values... actions.config.__proto__[n] = config[n] }) } // custom setup... // XXX is this the correct way??? 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){ this.actions != null && actions.mixout(this.tag || this.actions) /*/ XXX HANDLERS do we need this if .handlers if local to action... this.handlers != null && actions.off('*', this.tag) //*/ // XXX revise naming... this.hasOwnProperty('remove') && this.setup !== Feature.prototype.remove && this.remove(actions) return this }, // 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() // 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) } if(tag != null && obj.tag != null && obj.tag != tag){ throw new Error('tag and obj.tag mismatch, either use one or both must match.') } // actions... if(obj instanceof actions.Action){ if(tag == null){ throw new Error('need a tag to make a feature out of an action') } obj = { tag: tag, actions: obj, } // meta-feature... } else if(obj.constructor === Array){ if(tag == null){ throw new Error('need a tag to make a meta-feature') } obj = { tag: tag, suggested: obj, } } // XXX HANDLERS setup .handlers... if(obj.handlers){ obj.actions = obj.actions || {} // NOTE: obj.actions does not have to be an action so w cheat \ // a bit here, then copy the mindings... var tmp = Object.create(actions.MetaActions) obj.handlers .forEach(function([a, h]){ tmp.on(a, obj.tag, h) }) Object.assign(obj.actions, tmp) } // feature-set... if(feature_set){ feature_set[obj.tag] = obj } return obj }, }) //--------------------------------------------------------------------- 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 }) }, // 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 in load order... // // .buildFeatureList() // .buildFeatureList('*') // -> data // // .buildFeatureList(feature-tag) // -> data // // .buildFeatureList([feature-tag, .. ]) // -> data // // .buildFeatureList(.., filter) // -> data // // // 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) // // 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: // { // // input feature feature tags... // input: [ .. ], // // // output list of feature tags... // features: [ .. ], // // // disabled features... // disabled: [ .. ], // // exclusive features that got excluded... // excluded: [ .. ], // // // 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, // }, // // // 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, .. ], // .. // }, // } // // 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 instanceof Array ? lst : [lst] //isDisabled = isDisabled || function(){ return false } var that = this // 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] }) }) } // Expand feature references (recursive)... // // 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 || [] // 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 // 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 } // 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 }) .filter(function(e){ return e }) // 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 } // skip already done features... if(f in store){ return } //var feature = store[f] = that[f] var feature = that[f] if(feature){ var _lst = [] // merge lists... ;(target instanceof Array ? target : [target]) .forEach(function(t){ _lst = _lst.concat(feature[t] || []) }) store[f] = _lst // traverse down... expand(target, _lst, store, data, _seen.concat([f])) } }) return store } // 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: lst, features: list, disabled: disabled, excluded: excluded, // 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 features on existing actions object... // .setup(actions, [feature-tag, ...]) // -> actions // // 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){ // no explicit object is given... if(lst == null){ lst = obj obj = null } obj = obj || (this.__actions__ || actions.Actions)() lst = lst instanceof Array ? lst : [lst] 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 }) // if we have critical errors and set verbose... var fatal = features.error && (features.error.loops.length > 0 || features.error.sort_loop_overflow) // 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!') } features.FeatureSet = this obj.features = features // fatal error -- can't load... if(fatal){ throw new FeatureLinearizationError(features) } // mixout everything... this.remove(obj, obj.mro('tag') .filter(function(e){ return !!e })) // do the setup... var that = this var setup = Feature.prototype.setup features.features.forEach(function(n){ // setup... if(that[n] != null){ this.__verbose__ && console.log('Setting up feature:', n) setup.call(that[n], obj) } }) 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){ this.__verbose__ && console.log('Removing feature:', n) 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 }, }) /*********************************************************************/ // default feature set... var Features = module.Features = new FeatureSet() /********************************************************************** * vim:set ts=4 sw=4 : */ return module })