pWiki/lib/features.js
Alex A. Naanou 0ccd3c8514 notes...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2022-04-09 11:11:47 +03:00

1139 lines
34 KiB
JavaScript
Executable File

/**********************************************************************
*
*
*
**********************************************************************/
((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:
// [
// [ <event-spec>, <handler-function> ],
// ...
// ]
//
// NOTE: both <event-spec> and <handler-function> 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(<tag>, <obj>)
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(<feature-set>, <obj>)
} else {
obj = tag
tag = null }
// Feature(<obj>)
// 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:
// [ <feature>, <index>, <priority> ]
.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 })