salt
Version:
State And Logic Traversal, for today's infinite-application.
1,591 lines (1,485 loc) • 86.7 kB
JavaScript
/*!
* Salt v0.5.5
* http://github.com/bemson/salt/
*
* Dependencies:
* - Panzer v0.3.13 / Bemi Faison (c) 2012 / MIT (http://github.com/bemson/Panzer/)
*
* Copyright, Bemi Faison
* Released under the MIT License
*/
/* global define, require, module */
!function (inAMD, inCJS, Array, Math, Object, RegExp, scope, undefined) {
// dependent module initializer
function initSalt(require) {
var
Salt = ((inCJS || inAMD) ? require('panzer') : scope.Panzer).create(),
rand_string = (Math.ceil(Math.random() * 5000) + 3000).toString(18),
corePkgDef = Salt.pkg('core'),
staticUnusedArray = [],
staticUnusedObject = {},
sharedProxyStateGroupsMember = [],
protoSlice = Array.prototype.slice,
RxpProto = RegExp.prototype,
i, // loop vars
isArray = (typeof Array.isArray === 'function') ?
Array.isArray :
function (obj) {
return obj instanceof Array;
},
tokenPrefix = '@',
defaultPermissions = {world: true, owner: true, sub: true, self: true},
redirectFlag = 1,
// regexps
r_queryIsTokenized = new RegExp('\\.|\\||^' + tokenPrefix + '|\\|' + tokenPrefix),
r_validAbsolutePath = /^\/\/(?:\w+\/)+/,
r_trimSlashes = /^\/+|\/+$/g,
r_hasNonAlphanumericCharacter = /\W/,
r_hasAlphanumericCharacter = /\w/,
r_rxpIsForPath = /^\/.*\/.*\//,
traversalCallbackOrder = {
_on: 0,
_in: 1,
_out: 2,
_over: 3,
_bover: 4,
'0': 'on',
'1': 'in',
'2': 'out',
'3': 'over',
'4': 'bover'
},
activeSalts = [],
reservedQueryTokens = {
'null': {
f: 0,
i: 0
},
program: {
f: 0,
i: 1
},
root: {
f: function (node) {
return node.rootIndex;
}
},
parent: {
f: function (node) {
return node.parentIndex;
}
},
child: {
f: function (node) {
return node.firstChildIndex;
}
},
next: {
f: function (node) {
return node.nextIndex;
}
},
previous: {
f: function (node) {
return node.previousIndex;
}
},
oldest: {
f: function (node, nodes, tokenName) {
var parentNode = nodes[node.parentIndex];
if (parentNode) {
return parentNode[((tokenName.charAt(0) === 'y') ? 'first' : 'last') + 'ChildIndex'];
}
return -1;
}
},
self: {
f: function (node) {
return node.index;
}
}
},
criteriaCache = {},
// in reverse-order of complexity
criteriaKeys = [
'has',
'within',
'on',
'from',
'is'
],
criteriaKeysLength = criteriaKeys.length,
coreTags = {
// Specifies when a state is the base for rooted queries.
_root: function (tagName, exists, tags, node, parentNode, pkg, idx) {
if (idx < 2 || (exists && tags._root)) {
node.rootIndex = idx;
} else {
node.rootIndex = parentNode.rootIndex;
}
},
// Specifies when a state may not be exited with external calls.
_restrict: function (tagName, exists, tags, node, parentNode, pkg, idx) {
var prop = tagName.substr(1);
if (exists && tags[tagName]) {
node[prop] = idx;
} else if (parentNode) {
node[prop] = parentNode[prop];
} else {
node[prop] = -1;
}
},
// Specify cascading group identifiers
_group: function (tagName, exists, tags, node, parentNode) {
var
tagValue,
groups,
groupIdx,
groupName
;
if (parentNode) {
node.groups = parentNode.groups;
node.cGrps = parentNode.cGrps;
}
if (exists) {
groups = merge(node.groups);
tagValue = tags[tagName];
if (!isArray(tagValue)) {
tagValue = [tagValue];
}
groupIdx = tagValue.length;
while (groupIdx--) {
groupName = tagValue[groupIdx];
if (typeof groupName === 'string' &&
(groupName = groupName.trim().toLowerCase()) &&
!defaultPermissions.hasOwnProperty(groupName)
) {
groups[groupName] = true;
}
}
node.groups = groups;
}
node.cGrps = Object.keys(node.groups);
},
// Specify cascading permissions when a state is entered and exited
_perms: function (tagName, exists, tags, node, parentNode, pkg, idx) {
node.perms = 0;
if (exists || idx === 1) {
// when present or on first node
node.perms = node.lp = (exists) ? perms_parse(tags[tagName], parentNode.lp) : merge(defaultPermissions);
} else if (parentNode) {
// pass-thru parent permmissions data
node.lp = parentNode.lp;
} else {
// place defaults at bottom of stack
node.lp = defaultPermissions;
}
},
// Defines the path to update an owning salt - if any.
_owner: function (tagName, exists, tags, node, parentNode, pkg) {
node.oGate = 0;
if (exists) {
pkg.ownable =
node.oGate =
1;
node.ping = tags._owner;
} else if (parentNode) {
node.ping = parentNode.ping;
} else {
node.ping = -1;
}
},
// Define alias for this state, to use in queries.
_alias: function (tagName, exists, tags, node, parentNode, pkg, idx) {
if (
exists && tags._alias && typeof tags._alias === 'string' &&
!r_queryIsTokenized.test(tags._alias) && r_hasAlphanumericCharacter.test(tags._alias)
) {
pkg.tokens[tags._alias] = {
i: idx,
f: 0
};
node.alias = tags._alias;
} else {
node.alias = '';
}
},
// Define criteria for preserving instances created while traversing this branch.
_capture: function (tagName, exists, tags, node, parentNode, pkg) {
if (exists) {
node.caps = subs_sanitizeCriteria(tags._capture);
} else if (parentNode) {
node.caps = parentNode.caps;
} else {
node.caps = 0;
pkg.caps = [];
}
},
// Define data names and values for a branch.
//
// _data: 'foo'
// _data: ['foo']
// _data: {foo: 'bar'}
// _data: ['foo', {zoo:'baz'}]
_data: function (tagName, exists, tags, node) {
var
cfgs = {},
dataAry,
dataIdx = 0,
dataLn,
data,
typeofData,
key
;
// init dtos property to collect data tracking objects
node.dcfgs = [];
if (exists) {
dataAry = isArray(tags._data) ? tags._data : [tags._data];
dataLn = dataAry.length;
for (; dataIdx < dataLn; dataIdx++) {
data = dataAry[dataIdx];
typeofData = typeof data;
if (typeofData === 'string' && data) {
cfgs[data] = {
use: 0,
name: data,
value: undefined
};
}
if (typeofData === 'object' && data) {
for (key in data) {
if (data.hasOwnProperty(key)) {
cfgs[key] = {
use: 1,
name: key,
value: data[key]
};
}
}
}
}
for (key in cfgs) {
if (cfgs.hasOwnProperty(key)) {
node.dcfgs[node.dcfgs.length] = cfgs[key];
}
}
}
},
// Specifies a branch to navigate after targeting this state.
_sequence: function (tagName, exists, tags, node, parentNode) {
if (exists) {
// set walk to a new or copied array, based on the booly value
node.seq = tags[tagName] ? [] : 0;
// set last walk to this state's walk
node.lastWalk = node.seq;
} else {
// set walk property to nil
node.seq = 0;
// pass thru the last walk array defined - if any
if (parentNode) {
node.lastWalk = parentNode.lastWalk;
} else {
node.lastWalk = 0;
}
// if there is a lastWalk array...
if (node.lastWalk) {
// add this node's index to the array
node.lastWalk[node.lastWalk.length] = node.index;
}
}
},
// Specifies when a paused state will prevent parent salt's from completing their navigation.
_pins: function (tagName, exists, tags, node, parentNode) {
if (exists) {
node.pins = !!tags._pins;
} else if (parentNode) {
node.pins = parentNode.pins;
} else {
node.pins = true;
}
},
/*
Defines one of five callback methods to invoke.
*/
_on: function (tagName, exists, tags, node) {
var
tagValue = tags[tagName]
;
if (exists && typeof tagValue === 'function') {
node.fncs[traversalCallbackOrder[tagName]] = tagValue;
}
},
_tail: function (tagName, exists, tags, node, parentNode, pkg, idx) {
var tagValue;
// start with default
node.tail = -1;
// capture paired value, set default, or use parent
if (exists) {
tagValue = tags[tagName];
if (tagValue === true) {
// target self
node.tail = idx;
} else if (tagValue !== false) {
// don't tail when false
node.tail = tagValue;
}
} else if (parentNode) {
node.tail = parentNode.tail;
}
},
// Specifies when a branch should be invisible to external queries
_conceal: function (tagName, exists, tags, node, parentNode, pkg, idx) {
node.conceal = -1;
if (exists && idx > 1) {
if (tags._conceal) {
node.conceal = idx;
}
} else if (parentNode) {
node.conceal = parentNode.conceal;
}
}
},
// tags that depend on other tags or require cleanup
corePostTags = {
// Clean up lastWalk flag
_sequence: function (tagName, exists, tags, node) {
delete node.lastWalk;
},
// ensure node alias was not overridden
_alias: function (tagName, exists, tags, node, parentNode, pkg, idx) {
var alias = node.alias;
if (idx === 0) {
node.alias = 'null';
} else if (idx === 1) {
node.alias = 'program';
} else if (alias && pkg.tokens[alias].i !== idx) {
node.alias = '';
}
},
// Process callbacks that are redirects
_on: function (tagName, exists, tags, node, parentNode, pkg, idx) {
var
tgtIdx = -1,
phase,
typeofTagValue,
tagValue,
useTarget = 0
;
if (exists && (typeofTagValue = typeof (tagValue = tags[tagName])) !== 'function') {
if (typeofTagValue === 'string' && tagValue.length) {
if (tagValue.charAt(0) === '>') {
useTarget = 1;
}
tgtIdx = pkg.indexOf(tagValue, node);
} else if (typeofTagValue === 'number' && pkg.nodes[tagValue]) {
tgtIdx = tagValue;
}
if (~tgtIdx && (tagName !== '_on' || tgtIdx !== idx)) {
phase = traversalCallbackOrder[tagName];
node.reds[phase] = [useTarget, tgtIdx];
node.fncs[phase] = redirectFlag;
}
}
},
// specify delay when entering on traversal
_wait: function (tagName, exists, tags, node, parentNode, pkg) {
var
tagValue = tags[tagName],
targetIndex,
firstIndexType,
waitParamsLn,
waitParams = staticUnusedArray
;
node.delay = 0;
if (exists && tagValue !== false) {
if (Array.isArray(tagValue)) {
waitParams = tagValue;
} else if (tagValue !== true) {
waitParams = [tagValue];
}
// vet wait query, if present
waitParamsLn = waitParams.length;
firstIndexType = typeof waitParams[0];
// exit when only arg is not a number
if (waitParamsLn === 1 && firstIndexType !== 'number') {
return;
}
if (waitParamsLn > 1 && firstIndexType !== 'function') {
if (~(targetIndex = pkg.indexOf(waitParams[0]), node)) {
// resolve query
waitParams[0] = targetIndex;
} else {
// exit when first arg is not a valid query
return;
}
}
node.delay = waitParams;
}
},
// specify next node when entering on traversal
_next: function (tagName, exists, tags, node, parentNode, pkg) {
var
tagValue,
targetIndex
;
node.nxt = -1;
node.nxtc = 0;
if (exists) {
if (typeof (tagValue = tags[tagName]) === 'string' && tagValue.charAt(0) === '>') {
// flag that this will clear existing waypoints
node.nxtc = 1;
}
if (~(targetIndex = pkg.indexOf(tagValue, node))) {
// capture index when valid
node.nxt = targetIndex;
}
}
},
// resolve and keep valid tail targets
_tail: function (tagName, exists, tags, node, parentNode, pkg, idx) {
var
query = node.tail,
targetIndex
;
// resolve query
node.tail = targetIndex = pkg.indexOf(query, node);
// ensure resolved target is not pointing to self
if (targetIndex === idx) {
node.tail = -1;
}
},
_perms: function (tagName, exists, tags, node) {
delete node.lp;
}
},
// actions to take when entering and exiting a node
nodeScopeActions = {
// ping owner
0: function (node, pkg) {
// notify owner before entering and after exiting this node
if (node.oGate && ~node.ping) {
pkg.pingOwner(node.ping);
}
},
// scope data
1: function (node, pkg, add) {
var
data = pkg.proxy.data,
dataCfgs = node.dcfgs,
dataCfgLn = dataCfgs.length,
dataCfgIdx = 0,
dataCfg,
dataName,
dataTrackingObject,
scopeAction
;
// exit when there are no configurations for this node
if (!node.dcfgs.length) {
return;
}
// define scoping routine
if (add) {
// scope new value to stack - set value from config
scopeAction = function () {
// capture current value in stack (if any)
if (data.hasOwnProperty(dataName)) {
// capture current value in stack
dataTrackingObject.stack.unshift(data[dataName]);
}
if (dataCfg.use) {
// set key to value from config
data[dataName] = dataCfg.value;
} else {
// set key to last value or undefined (by default)
data[dataName] = dataTrackingObject.stack[0];
}
};
} else {
// set value form stack and remove
scopeAction = function () {
if (dataTrackingObject.stack.length) {
// use and remove value from stack
data[dataName] = dataTrackingObject.stack.shift();
} else {
// remove tracking object and data member
delete pkg.dtos[dataName];
delete data[dataName];
}
};
}
for (; dataCfgIdx < dataCfgLn; dataCfgIdx++) {
dataCfg = dataCfgs[dataCfgIdx];
dataName = dataCfg.name;
dataTrackingObject = pkg.getDTO(dataName);
scopeAction();
}
},
// permissions stack
2: function (node, pkg, add) {
shared_nodeStackHandler(pkg.perms, node.perms, add);
// copy to public state.perms
pkg.proxy.state.perms = merge(pkg.perms[0]);
},
// capture criteria stack
3: function (node, pkg, add) {
shared_nodeStackHandler(pkg.caps, node.caps, add);
// if exiting program node
if (node.index === 1 && !add) {
// clear sub-instances
pkg.bin = {};
pkg.tin = {};
}
}
},
nodeScopeActionsLength = 4,
// import resolution helpers
import_pkgCnt,
import_tagKeyTests,
// cache of core tag keys
coreTagKeys = [],
coreTagKeyCount,
// cache of core post tag keys
corePostTagKeys = [],
corePostTagKeyCount
;
Salt.version = '0.5.5';
// define remaining core tags and share tag initializers
/*
_ingress: Defines a state that must be targeted before it's descendents.
*/
coreTags._ingress = coreTags._restrict;
coreTags._in = coreTags._out = coreTags._over = coreTags._bover = coreTags._on;
corePostTags._in = corePostTags._out = corePostTags._over = corePostTags._bover = corePostTags._on;
// get core tag keys
for (i in coreTags) {
if (coreTags.hasOwnProperty(i)) {
coreTagKeys[coreTagKeys.length] = i;
}
}
coreTagKeyCount = coreTagKeys.length;
// get post core tag keys
for (i in corePostTags) {
if (corePostTags.hasOwnProperty(i)) {
corePostTagKeys[corePostTagKeys.length] = i;
}
}
corePostTagKeyCount = corePostTagKeys.length;
// reuse dynamic resolution for other tokens
reservedQueryTokens['.'] = reservedQueryTokens.self;
reservedQueryTokens['..'] = reservedQueryTokens.parent;
reservedQueryTokens.youngest = reservedQueryTokens.oldest;
function isSalt(thing) {
return thing instanceof Salt;
}
// add or remove from stack when there is an item
function shared_nodeStackHandler(stack, item, add) {
if (item) {
if (add) {
stack.unshift(item);
} else {
stack.shift();
}
}
}
function sharedNullFunction() {}
function subs_addInsts (collection, subInsts, pkg) {
var
node = pkg.nodes[pkg.tank.currentIndex],
i = 0,
subInst,
subId,
totalAdded = 0
;
for (; subInst = subInsts[i]; i++) {
subId = subInst.tank.id;
if (!collection.hasOwnProperty(subId)) {
totalAdded++;
collection[subId] = {
inst: subInst,
cap: node
};
}
}
return totalAdded;
}
function subs_removeInsts (collection, subInsts) {
var
i = subInsts.length,
subInst,
subId,
totalRemoved = 0
;
while (i--) {
subInst = subInsts[i];
subId = subInst.tank.id;
if (collection.hasOwnProperty(subId)) {
totalRemoved++;
delete collection[subId];
}
}
return totalRemoved;
}
function subs_getInsts (collection, criteria) {
var
criteriaKey,
criteriaKeyIdx,
criteriaOption,
criteriaOptions,
criteriaOptionIdx,
criteriaOptionsLength,
subSet,
subSetId,
matches = []
;
nextSubset:
for (subSetId in collection) {
if (collection.hasOwnProperty(subSetId)) {
// get subSet to test
subSet = collection[subSetId];
// reset criteria key index
criteriaKeyIdx = criteriaKeysLength;
nextCriteria:
while (criteriaKeyIdx--) {
// get the criteria key
criteriaKey = criteriaKeys[criteriaKeyIdx];
// setup criteria option loop vars
criteriaOptions = criteria[criteriaKey];
criteriaOptionsLength = criteriaOptions.length;
// if this key has options...
if (criteriaOptionsLength) {
for (criteriaOptionIdx = 0; criteriaOptionIdx < criteriaOptionsLength; criteriaOptionIdx++) {
criteriaOption = criteriaOptions[criteriaOptionIdx];
// if this criteria option has not not compiled...
if (criteriaOptionIdx >= criteriaOptions.made) {
// capture and replace option at index with compiled version
criteriaOption = criteriaOptions[criteriaOptionIdx] = subs_compileCriteriaOption(criteriaKey, criteriaOption, typeof criteriaOption);
// flag that this option is now compiled
criteriaOptions.made++;
}
// test result of compiled criteria option's function
// pass in the subset, and criteria object
if (criteriaOption.f(subSet, criteriaOption)) {
// add subset instance to matches
matches[matches.length] = subSet.inst;
// continue to the next criteria, since it's been satisfied
continue nextCriteria;
}
}
// since no options of this criteria were satisfied, skip to the next subSet in the collection
continue nextSubset;
}
}
}
}
return matches;
}
function subs_sanitizeCriteria(raw) {
var
criteria,
criteriaKey,
criteriaKeyIdx,
criteriaCacheId,
rxpOldToJSON,
typeofRaw,
isRegExp,
specifiesCriteria
;
// if raw is null or undefined...
if (raw === null || raw === undefined) {
// set to false (capture nothing)
raw = false;
}
// get type or raw value
typeofRaw = typeof raw;
// if not an object, or does not have "is" criteria...
if (typeofRaw !== 'object' || !raw.hasOwnProperty('is')) {
if (RxpProto.hasOwnProperty('toJSON')) {
// capture existing .toJSON() method
rxpOldToJSON = RxpProto.toJSON;
}
// augment .toJSON() for any array's with nested RegExp's
RxpProto.toJSON = subs_stainRxpInJSON;
// get cache id for this criteria - prefix to avoid native collisiions
criteriaCacheId = 'c' + JSON.stringify(raw);
// reset .toJSON() for RegExp's
if (rxpOldToJSON) {
RxpProto.toJSON = rxpOldToJSON;
} else {
delete RxpProto.toJSON;
}
if (criteriaCache.hasOwnProperty(criteriaCacheId)) {
// exit when this criteria has already been sanitized
return criteriaCache[criteriaCacheId];
}
}
// define criteria to return
criteria = {buffer: -1};
// handle short-forms
if (typeofRaw === 'boolean' && raw) {
// `true` means match all sub-instances at or within this index
criteria.on = ['/'];
criteria.on.made = 0;
} else if (
// calculate here - only when necessary
(isRegExp = raw instanceof RegExp) ||
(typeofRaw === 'string' && raw) ||
(typeofRaw === 'number' && raw === ~~raw)
) {
criteria.on = [raw];
criteria.on.made = 0;
} else if (typeofRaw === 'object' && !isRegExp) {
// sanitize known criteria from the raw object
criteriaKeyIdx = criteriaKeysLength;
while(criteriaKeyIdx--) {
criteriaKey = criteriaKeys[criteriaKeyIdx];
if (raw.hasOwnProperty(criteriaKey)) {
// extract object criteria, as a set of options (i.e., an array)
criteria[criteriaKey] = isArray(raw[criteriaKey]) ? protoSlice.call(raw[criteriaKey]) : [raw[criteriaKey]];
criteria[criteriaKey].made = 0;
specifiesCriteria = 1;
}
}
}
// ensure all criteria are present
criteriaKeyIdx = criteriaKeysLength;
while (criteriaKeyIdx--) {
criteriaKey = criteriaKeys[criteriaKeyIdx];
if (!criteria.hasOwnProperty(criteriaKey)) {
// use default empty option list (no need to track compiled options)
criteria[criteriaKey] = staticUnusedArray;
}
}
// sanitize buffer
if (raw.hasOwnProperty('buffer')) {
// capture all, when only option is buffer
if (!specifiesCriteria) {
criteria.on = ['/'];
criteria.on.made = 0;
}
if (raw.buffer !== -1) {
criteria.buffer = raw.buffer ? 1 : 0;
}
}
if (criteriaCacheId) {
// add sanitized criteria to cache
criteriaCache[criteriaCacheId] = criteria;
}
return criteria;
}
// pre-cache the criteria compilation of `true`
subs_sanitizeCriteria(true);
// compilation will usually involve searching a set or string with a regexp or string
function subs_compileCriteriaOption(criteriaKey, value, typeofValue) {
var
isRegExp = value instanceof RegExp,
option = {
v: value,
f: sharedNullFunction
}
;
if (criteriaKey === 'has') {
// compile "has" criteria
if (isRegExp) {
option.f = subs_filter_searchSetRxp;
option.g = subs_filter_getSubInstMember;
if (r_rxpIsForPath.test(value)) {
option.m = 'pAry';
} else {
option.m = 'sAry';
}
} else if (value && typeofValue === 'string') {
option.g = subs_filter_getSubInstMember;
if (~value.indexOf('/')) {
option.f = subs_filter_searchSet;
option.m = 'pAry';
// wrap in slashes to match whole sub-paths
if (option.v.charAt(0) !== '/') {
option.v = '/' + option.v;
}
if (option.v.substr(-1) !== '/') {
option.v += '/';
}
} else {
option.f = subs_filter_matchSet;
option.m = 'sAry';
}
} else if (
typeofValue === 'number' &&
value >= 0 &&
value === ~~value
) {
option.f = subs_filter_has_index;
}
} else if (criteriaKey === 'from') {
// compile "from" criteria
if (isRegExp) {
if (r_rxpIsForPath.test(value)) {
option.f = subs_filter_searchRxp;
option.g = subs_filter_from_path;
} else {
option.f = subs_filter_searchSetRxp;
option.g = subs_filter_from_pathParts;
}
} else if (value && typeofValue === 'string') {
if (~value.indexOf('/')) {
option.f = subs_filter_search;
option.g = subs_filter_from_path;
// wrap in slashes to match whole sub-paths
if (option.v.charAt(0) !== '/') {
option.v = '/' + option.v;
}
if (option.v.substr(-1) !== '/') {
option.v += '/';
}
} else {
option.f = subs_filter_matchSet;
option.g = subs_filter_from_pathParts;
}
} else if (
typeofValue === 'number' &&
value >= 0 &&
value === ~~value
) {
option.f = subs_filter_match;
option.g = subs_filter_from_index;
}
} else if (criteriaKey === 'is') {
// compile "is" criteria
option.f = subs_filter_is;
} else if (criteriaKey === 'on') {
// compile "on" criteria
if (isRegExp) {
option.f = subs_filter_searchRxp;
if (r_rxpIsForPath.test(value)) {
option.g = subs_filter_getStateProperty;
option.m = 'path';
} else {
option.g = subs_filter_getStateProperty;
option.m = 'name';
}
} else if (value && typeofValue === 'string') {
if (~value.indexOf('/')) {
option.f = subs_filter_search;
option.g = subs_filter_getStateProperty;
option.m = 'path';
// wrap in slashes to match whole sub-paths
if (option.v.charAt(0) !== '/') {
option.v = '/' + option.v;
}
if (option.v.substr(-1) !== '/') {
option.v += '/';
}
} else {
option.f = subs_filter_match;
option.g = subs_filter_getStateProperty;
option.m = 'name';
}
} else if (
typeofValue === 'number' &&
value >= 0 &&
value === ~~value
) {
option.f = subs_filter_match;
option.g = subs_filter_getStateProperty;
option.m = 'index';
}
} else if (criteriaKey === 'within') {
// compile "within" criteria
if (isRegExp) {
if (r_rxpIsForPath.test(value)) {
option.f = subs_filter_searchRxp;
option.g = subs_filter_within_path;
} else {
option.f = subs_filter_searchSetRxp;
option.g = subs_filter_within_pathParts;
}
} else if (value && typeofValue === 'string') {
if (~value.indexOf('/')) {
option.f = subs_filter_search;
option.g = subs_filter_within_path;
// wrap in slashes to match whole sub-paths
if (option.v.charAt(0) !== '/') {
option.v = '/' + option.v;
}
if (option.v.substr(-1) !== '/') {
option.v += '/';
}
} else {
option.f = subs_filter_matchSet;
option.g = subs_filter_within_pathParts;
}
} else if (
typeofValue === 'number' &&
value > -1 &&
value === ~~value
) {
option.f = subs_filter_within_index;
}
}
return option;
}
// sub-instance helper functions
// prepend regex with random string
function subs_stainRxpInJSON() {
return rand_string + this;
}
// generic needle search a string
// expects params.v to be the needle
// expects params.g to be a function that gets the haystack
function subs_filter_search(subSet, params) {
return ~params.g(subSet, params).indexOf(params.v);
}
// generic regexp search a string
// expects params.v to be the regexp
// expects params.g to be a function that gets the string
function subs_filter_searchRxp(subSet, params) {
return params.v.test(params.g(subSet, params));
}
// generic regexp search over set of strings
// expects params.v to be the regexp
// expects params.g to be a function that gets the set
function subs_filter_searchSetRxp(subSet, params) {
var
set = params.g(subSet, params),
setIdx = set.length,
rxp = params.v
;
while (setIdx--) {
if (rxp.test(set[setIdx])) {
return 1;
}
}
}
// generic string set search
// expects params.v to be the needle
// expects params.g to be a function that gets the set
function subs_filter_searchSet(subSet, params) {
var
set = params.g(subSet, params),
setIdx = set.length,
hay,
match = params.v
;
while (setIdx--) {
hay = set[setIdx];
if (~hay.indexOf(match)) {
return 1;
}
}
}
// generic match between values
// expects params.v to be the match
// expects params.g to be a function that gets the comparison value
function subs_filter_match(subSet, params) {
return params.v === params.g(subSet, params);
}
// generic string set match
// expects params.v to be the match
// expects params.g to be a function that gets the set
function subs_filter_matchSet(subSet, params) {
var
set = params.g(subSet, params),
setIdx = set.length,
match = params.v
;
while (setIdx--) {
if (set[setIdx] === match) {
return 1;
}
}
}
// returns property of subInst
// expects params.m to be a member of the sub-instance
function subs_filter_getSubInstMember(subSet, params) {
return subSet.inst[params.m];
}
// returns property of subInst
// expects params.m to be a member of the sub-instance
function subs_filter_getStateProperty(subSet, params) {
var subInst = subSet.inst;
return subInst.nodes[subInst.tank.currentIndex][params.m];
}
// criteria "is" filter functions
function subs_filter_is(subSet, params) {
// the sub-instance must be sourced by the parameterized value
return subSet.inst.nodes[1].value === params.v;
}
// criteria "has" filter functions
// returns true when params.v is within the given index
function subs_filter_has_index(subSet, params) {
return params.v < subSet.inst.nodes.length;
}
// criteria "from" filter functions
// returns subSet capture path
function subs_filter_from_path(subSet) {
return subSet.cap.path;
}
// returns subSet capture path as an array of states
// returns an empty array when the path is the null or program state
function subs_filter_from_pathParts(subSet) {
var captureNode = subSet.cap;
if (captureNode.index > 1) {
return captureNode.path.slice(2, -1).split('/');
}
return staticUnusedArray;
}
// returns index that sub-instance was capture
function subs_filter_from_index(subSet) {
return subSet.cap.index;
}
// returns path of current state's parent
function subs_filter_within_path(subSet) {
var
subInst = subSet.inst,
currentNode = subInst.nodes[subInst.tank.currentIndex]
;
if (~currentNode.parentIndex) {
return subInst.nodes[currentNode.parentIndex].path;
}
return '';
}
// returns path parts of current state's parent path
function subs_filter_within_pathParts(subSet) {
var
subInst = subSet.inst,
currentNode = subInst.nodes[subInst.tank.currentIndex]
;
if (currentNode.parentIndex > 1) {
return subInst.nodes[currentNode.parentIndex].path.slice(2, -1).split('/');
}
return staticUnusedArray;
}
// returns true when the sub-instance's current node is within the given state index
function subs_filter_within_index(subSet, params) {
var
subInst = subSet.inst,
nodes = subInst.nodes,
currentNode = nodes[subInst.tank.currentIndex],
targetNode = nodes[params.v]
;
if (targetNode) {
return currentNode.within(targetNode);
}
}
// shallow object merge
function mix(base) {
var
argumentIdx = 1,
source,
member
;
for (; source = arguments[argumentIdx]; argumentIdx++) {
for (member in source) {
if (source.hasOwnProperty(member)) {
base[member] = source[member];
}
}
}
return base;
}
// deep object copy
function merge() {
var
argumentIdx = 0,
obj = {},
source,
member,
value
;
for (; source = arguments[argumentIdx]; argumentIdx++) {
for (member in source) {
if (source.hasOwnProperty(member)) {
value = source[member];
if (value !== null && typeof value === 'object') {
obj[member] = merge(value);
} else {
obj[member] = value;
}
}
}
}
return obj;
}
function perms_parse(option, lastPerms) {
var
perms,
deny,
typeofOption = typeof option,
optionIdx,
optionLength,
key,
self_perm = 'self'
;
if (typeofOption === 'object' && isArray(option)) {
optionLength = option.length;
for (optionIdx = 0; optionIdx < optionLength; optionIdx++) {
lastPerms = perms_parse(option[optionIdx], lastPerms);
}
} else if (
typeofOption === 'string' ||
typeofOption === 'object' ||
typeofOption === 'boolean'
) {
perms = merge(lastPerms);
if (typeofOption === 'string' && option) {
option = option.toLowerCase();
deny = option.charAt(0) === '!';
if (deny) {
option = option.substr(1);
}
if (option !== self_perm) {
perms[option] = !deny;
}
} else if (typeofOption === 'boolean') {
for (key in perms) {
if (key !== self_perm && perms.hasOwnProperty(key)) {
perms[key] = option;
}
}
} else {
// lowercase all options
lastPerms = {};
for (key in option) {
if (option.hasOwnProperty(key) && key.charAt(0) !== '!') {
lastPerms[key.toLowerCase()] = option[key];
}
}
// ensure self is true
mix(perms, lastPerms, {self: true});
}
return perms;
}
// return new or old lastPerms
return lastPerms;
}
// gets tag key tests for parsing state tags
function import_cacheTagKeyTests () {
var pkgNames = Salt.pkg();
// only compile if the number of packages has changed
// NOTE: this approach is performant but fails if attrkeys are changed per package
if (pkgNames.length !== import_pkgCnt) {
// compile list of attribute keys from all packages
if ((import_pkgCnt = pkgNames.length) === 1) {
import_tagKeyTests = [corePkgDef.attrKey];
} else {
import_tagKeyTests = pkgNames
.map(import_cacheTagKeyTests_map)
.filter(import_cacheTagKeyTests_filter)
;
}
// cache type of attribute test
import_tagKeyTests = import_tagKeyTests
.map(import_cacheTagKeyTests_precalc);
}
}
function import_cacheTagKeyTests_map ( pkgName ) {
return Salt.pkg(pkgName).attrKey;
}
function import_cacheTagKeyTests_filter ( tagKeyTest ) {
return tagKeyTest;
}
function import_cacheTagKeyTests_precalc ( tagKeyTest ) {
var typeMap = {
// flag when the test is a function
f:0,
// flag when the test is a regular-expression
r:0
};
if (typeof tagKeyTest === 'function') {
typeMap.f = tagKeyTest;
} else {
typeMap.r = tagKeyTest;
}
return typeMap;
}
// flag when the given state member is a state (otherwise a tag)
function import_isState ( name, value ) {
var
i = 0,
tagKeyTest
;
// all key tests must fail, in order to be a state
for (; tagKeyTest = import_tagKeyTests[i]; i++) {
if (
(
tagKeyTest.r &&
tagKeyTest.r.test(name)
) ||
(
tagKeyTest.f &&
tagKeyTest.f(name, value)
)
) {
return 0;
}
}
return 1;
}
// ensure the given value is a state object
function import_mergeStates_convertToObject( state ) {
var typeofState = typeof state;
if (typeofState === 'string') {
return {
_import: state
};
} else if (typeofState === 'function') {
return {
_on: state
};
} else if (typeofState === 'object') {
return state;
}
return {};
}
function import_mergeStates ( baseState, sourceState, mergedState) {
var
merged = mergedState || {},
base = import_mergeStates_convertToObject(baseState),
source = import_mergeStates_convertToObject(sourceState),
baseKeys = Object.keys(base),
sourceKeys = Object.keys(source),
keyIsInSource,
idx = 0,
key
;
// import base keys and merge states also in source
for (; key = baseKeys[idx]; idx++) {
keyIsInSource = source.hasOwnProperty(key);
if (import_isState(key)) {
if (keyIsInSource) {
merged[key] = import_mergeStates(base[key], source[key], {});
} else {
merged[key] = base[key];
}
} else if (keyIsInSource && key !== '_import') { // override all but the _import tag
merged[key] = source[key];
} else {
merged[key] = base[key];
}
}
// append unique source keys that are not "_import"
for (idx = 0; key = sourceKeys[idx]; idx++) {
if (!base.hasOwnProperty(key) && key !== '_import') {
merged[key] = source[key];
}
}
return merged;
}
function import_getStateByAbsolutePath( path, program ) {
var
resolvedState = program,
parts = path.slice(2, -1).split('/'),
partsLn = parts.length,
partIdx = 0,
partialPath
;
for (; partIdx < partsLn; partIdx++) {
partialPath = parts[partIdx];
if (
resolvedState.hasOwnProperty(partialPath) &&
import_isState(partialPath, resolvedState[partialPath])
) {
resolvedState = resolvedState[partialPath];
} else {
return;
}
}
return resolvedState;
}
function import_resolveBase( sourceState, program, importedPaths ) {
var
resolvedState,
baseState,
sourceStateType = typeof sourceState,
importTagValue,
importTagValueType,
importPath
;
if (sourceStateType === 'string') {
importPath = sourceState;
} else if (
sourceStateType === 'object' &&
sourceState !== null &&
sourceState.hasOwnProperty('_import')
) {
importTagValue = sourceState._import;
importTagValueType = typeof importTagValue;
// determine whether we're importing a path or an (external) object
if (importTagValueType === 'object') {
if (importTagValue instanceof Salt) {
importTagValue = corePkgDef(importTagValue).nodes[1].value;
}
baseState = corePkgDef.prepNode(importTagValue, importTagValue) || importTagValue;
} else if (importTagValueType === 'function') {
// convert functions to states
baseState = {_on: importTagValue};
} else {
importPath = importTagValue;
}
}
// verify and resolve new program path
if (
importPath &&
!importedPaths.hasOwnProperty(importPath) &&
r_validAbsolutePath.test(importPath)
) {
baseState = import_getStateByAbsolutePath(importPath, program);
}
// merge resolved state
if (baseState) {
if (sourceStateType === 'string') {
// use base state directly
resolvedState = import_mergeStates_convertToObject(baseState);
} else if (sourceStateType === 'object') {
// merge base state with this state
resolvedState = import_mergeStates(baseState, sourceState);
}
}
return resolvedState;
}
// define package statics
mix(corePkgDef, {
// collection of active package-trees - exposed to support package integration
actives: [],
// configure panzer parsing and initialization
// pattern for identifying tag keys
attrKey: /^_/,
// pattern for identifying invalid state names
badKey: /^[^a-zA-Z]|\||\/|\.|^toString$/,
// tree preprocessor
prepTree: function (orig) {
import_cacheTagKeyTests();
if (isSalt(orig)) {
// when given a Salt instance, return the original instance's program
return corePkgDef(orig).nodes[1].value;
}
},
// node preprocessor
prepNode: function ( state, program ) {
var
finalState,
tmpState = state
;
// resolve all top-level imports for this state - avoid shallow-recursion
// this is similar to sub-classing
while (tmpState = import_resolveBase(tmpState, program, {})) {
finalState = tmpState;
}
return finalState;
},
// initialize the package instance with custom properties
// only argument is the object passed after the program when calling "new Salt(program, extraArg)"
init: function () {
var
pkg = this,
activeSalt = activeSalts[0],
nodes = pkg.nodes,
nodeCount = nodes.length,
i, j,
node, parentNode, tagName
;
// init sub-instances hashes
pkg.bin = {};
pkg.tin = {};
// init groups
pkg.groups = staticUnusedObject;
// sub-instance "has" criteria search helpers
pkg.nStr =
pkg.pStr =
pkg.lastPing = // use same value for lastPingId
'|';
pkg.sAry = [];
pkg.pAry = [];
// define initial vars member
pkg.vars = {};
// collection of custom query tokens
pkg.tokens = {};
// collection of custom callback queries
pkg.cq = {};
// collection of arguments for traversal functions
pkg.args = [];
// collection of node calls made while traversing
pkg.calls = [];
// collection of nodes targeted and reached while traversing
pkg.trail = [];
// permissions stack - begin with default perms
pkg.perms = [defaultPermissions];
// state index to add to trail at end of traversal/resume
pkg.tgtTrail = -1;
// collection of declared variable tracking objects
pkg.dtos = {};
// init delay timer, function, and args
pkg.waitTimer =
pkg.waitFnc =
pkg.waitArgs =
0;
// collection of cached values
pkg.cache = {
// token query cache
indexOf: {}
};
// indicates when this salt is in the stack of navigating salts
pkg.active = 0;
// flag when being invoked by a blessed function
pkg.blessed = 0;
// init index of node paths
pkg.nids = {};
// the number of child salts fired by this salt's program functions
pkg.pinned = 0;
// collection of parent salt references
pkg.pinning = {};
// count of parent salt references
pkg.pinCnt = 0;
// collection of targeted nodes
pkg.targets = [];
// identify the initial phase for this salt, 0 by default
pkg.phase = 0;
// set owner permission and assignment defaults
pkg.owner = pkg.ownable = 0;
// set name of first node
pkg.nodes[0].name = '_null';
// set name of second node
pkg.nodes[1].name = '_program';
// initialize nodes from first to last
for (i = 0; i < nodeCount; i++) {
node = nodes[i];
parentNode = nodes[node.parentIndex];
// index this node path
pkg.nids[node.path] = i;
if (i > 1) {
// add to delimited string of names (for sub-instances "has" matching)
pkg.nStr += node.name + '|';
pkg.sAry[pkg.sAry.length] = node.name;
}
// add to delimited string of paths (for sub-instances "has" matching)
pkg.pStr += node.path + '|';
pkg.pAry[pkg.pAry.length] = node.path;
// prep for tag compilation
node.pkg = pkg;
node.fncs = [0,0,0,0,0];
node.reds = [];
// run core tags
j = coreTagKeyCount;
while (j--) {
tagName = coreTagKeys[j];
coreTags[tagName](tagName, node.attrs.hasOwnProperty(tagName), node.attrs, node, parentNode, pkg, i);
}
// if there is no _on[0] function and this node's value is a function...
if (!node.fncs[0] && typeof node.value === 'function') {
// use as the _on[0] traversal function
node.fncs[0] = node.value;
}
}
// run post core tags for each node
i = nodeCount;
// order matters less on cleanup
while (i--) {
node = nodes[i];
parentNode = nodes[node.parentIndex];
j = corePostTagKeyCount;
while (j--) {
tagName = corePostTagKeys[j];
corePostTags[tagName](tagName, node.attrs.hasOwnProperty(tagName), node.attrs, node, parentNode, pkg, i);
}
}
// reference dat