libcore
Version:
Kicks-start helpers for cross-browser libraries and different versions of nodejs
820 lines (653 loc) • 19.5 kB
JavaScript
;
import {
string,
method,
array,
object,
signature as typeSignature,
NUMBER,
STRING,
BOOLEAN,
REGEX,
DATE,
ARRAY,
OBJECT,
METHOD
} from "./type.js";
import {
assign as objectAssign,
clone as objectClone,
compare as objectCompare,
maxObjectIndex,
contains
} from "./object.js";
var NUMERIC_RE = /^([1-9][0-9]*|0)$/,
ARRAY_INDEX_RE = /^([1-9][0-9]*|0|)$/,
ERROR_NATIVE_OBJECT = "Root [subject] requires native Object to accept " +
"non-numeric property name.",
ERROR_PATH_INVALID = 'Invalid [path] parameter.',
START = "start",
START_ESCAPED = "start_escaped",
QUEUE = "queue",
END = "end",
END_EMPTY = "end_empty",
STATE = {
"start": {
"[": "bracket_start",
"'": "any_sq_start",
'"': "any_dq_start",
"default": "any",
"\\": "any_escape"
},
"bracket_start": {
"]": "property_end",
"'": "sq_start",
'"': "dq_start",
"default": "bracket_any"
},
"any_sq_start": {
"'": "property_end",
"\\": "any_sq_escape",
"default": "any_sq"
},
"any_sq": {
"'": "property_end",
"\\": "any_sq_escape",
"default": "any_sq"
},
"any_sq_escape": {
"default": "any_sq"
},
"any_dq_start": {
'"': "property_end",
"\\": "any_dq_escape",
"default": "any_dq"
},
"any_dq": {
'"': "property_end",
"\\": "any_dq_escape",
"default": "any_dq"
},
"any_dq_escape": {
"default": "any_dq"
},
"sq_start": {
"'": "bracket_end",
"\\": "sq_escape",
"default": "sq"
},
"sq": {
"'": "bracket_end",
"\\": "sq_escape",
"default": "sq"
},
"sq_escape": {
"default": "sq"
},
"dq_start": {
'"': "bracket_end",
"\\": "dq_escape",
"default": "dq"
},
"dq": {
'"': "bracket_end",
"\\": "dq_escape",
"default": "dq"
},
"dq_escape": {
"default": "dq"
},
"bracket_any": {
"]": "property_end",
"\\": "bracket_any_escape",
"default": "bracket_any"
},
"bracket_any_escape": {
"default": "bracket_any"
},
"bracket_end": {
"]": "property_end"
},
"any": {
".": "start",
"\\": "any_escape",
"[": "bracket_start",
"default": "any"
},
"any_escape": {
"default": "any"
},
"property_end": {
"[": "bracket_start",
".": "start"
}
},
STATE_ACTION = {
"start": {
"any": START,
"any_escape": START_ESCAPED
},
"any_sq_start": {
"any_sq": START,
"property_end": END_EMPTY
},
"any_sq": {
"any_sq": QUEUE,
"property_end": END
},
"any_sq_escape": {
"any_sq": QUEUE
},
"any_dq_start": {
"any_dq": START,
"property_end": END_EMPTY
},
"any_dq": {
"any_dq": QUEUE,
"property_end": END
},
"any_dq_escape": {
"any_dq": QUEUE
},
"any": {
"any": QUEUE,
"start": END,
"bracket_start": END
},
"any_escape": {
"any": QUEUE,
"bracket_start": END,
"start": START
},
"bracket_start": {
"bracket_any": START,
"property_end": END_EMPTY
},
"bracket_any": {
"bracket_any": QUEUE,
"property_end": END
},
"bracket_any_escape": {
"bracket_any": QUEUE
},
"sq_start": {
"sq": START,
"bracket_end": END_EMPTY
},
"sq": {
"sq": QUEUE,
"bracket_end": END
},
"sq_escape": {
"sq": QUEUE
},
"dq_start": {
"dq": START,
"bracket_end": END_EMPTY
},
"dq": {
"dq": QUEUE,
"bracket_end": END
},
"dq_escape": {
"dq": QUEUE
}
};
function onParsePath(property, last, context) {
context[context.length] = property;
}
function isAccessible(subject, item) {
var signature = typeSignature(subject);
switch (signature) {
case NUMBER:
return isFinite(subject) && item in Number.prototype && signature;
case STRING:
return item in String.prototype && signature;
case BOOLEAN:
return item in Boolean.prototype && signature;
case REGEX:
case DATE:
case ARRAY:
case OBJECT:
case METHOD:
if (item in subject) {
return signature;
}
}
return false;
}
function isWritable(subject) {
var signature = typeSignature(subject);
switch (signature) {
case REGEX:
case DATE:
case ARRAY:
case OBJECT:
case METHOD:
return signature;
}
return false;
}
function isJSONWritable(subject) {
var signature = typeSignature(subject);
switch (signature) {
case ARRAY:
case OBJECT:
return signature;
}
return false;
}
function findCallback(item, last, operation) {
var subject = operation[1];
if (!isAccessible(subject, item)) {
operation[0] = void(0);
return false;
}
operation[last ? 0 : 1] = subject[item];
return true;
}
function onPopulatePath(item, last, context) {
var subject = context[1],
iswritable = isWritable,
writable = iswritable(subject),
U = void(0),
success = false;
// populate
if (!last) {
// populate
if (writable) {
// set object
if (!(item in subject)) {
subject[item] = {};
success = true;
}
// allow only when writable
else if (iswritable(subject[item])) {
success = true;
}
}
context[1] = success ? subject[item] : U;
}
// end it with writable state?
else {
success = writable;
context[2] = success && item;
}
return success;
}
function onRemovePath(item, last, context) {
var subject = context[1],
iswritable = isWritable,
writable = iswritable(subject),
U = void(0),
success = false;
// populate
if (!last) {
if (writable && item in subject) {
// go to next
if (iswritable(subject[item])) {
success = true;
}
// still a success, nothing to remove
else {
context[3] = true;
}
}
context[1] = success ? subject[item] : U;
}
// end it with writable state?
else {
success = writable;
context[2] = success && item;
context[3] = true;
}
return success;
}
function existsCallback(item, last, context) {
var subject = context[0],
exists = isAccessible(subject, item);
if (exists) {
context[0] = subject[item];
}
if (last) {
context[1] = !!exists;
}
return exists;
}
export function jsonParsePath(path) {
var items = [];
return jsonEach(path, onParsePath, items) && items.length ?
items : null;
}
export function jsonFind(path, object) {
var operation = [void(0), object];
jsonEach(path, findCallback, operation);
operation[1] = null;
return operation[0];
}
export function jsonCompare(path, object1, object2) {
return objectCompare(jsonFind(path, object1), object2);
}
export function jsonClone(path, object, deep) {
return objectClone(jsonFind(path, object), deep === true);
}
export function jsonEach(path, callback, arg1, arg2, arg3, arg4, arg5) {
var map = STATE,
action = STATE_ACTION,
start = START,
start_escaped = START_ESCAPED,
queue = QUEUE,
end = END,
end_empty = END_EMPTY,
DEFAULT = "default";
var c, l, chr, state, stateObject, items, len, last,
next, actionObject, buffer, bl, buffered, pending,
start_queue, restart;
if (!string(path)) {
throw new Error(ERROR_PATH_INVALID);
}
if (!method(callback)) {
throw new Error("Invalid [callback] parameter");
}
buffer = bl = false;
state = "start";
stateObject = map.start;
items = [];
len = pending = 0;
for (c = -1, l = path.length; l--;) {
buffered = false;
chr = path.charAt(++c);
last = !l;
// find next state
if (chr in stateObject) {
next = stateObject[chr];
}
else if (DEFAULT in stateObject) {
next = stateObject[DEFAULT];
}
else {
return null;
}
// check for actions
if (state in action) {
actionObject = action[state];
if (next in actionObject) {
start_queue = restart = false;
switch (actionObject[next]) {
case start:
start_queue = true;
/* falls through */
case start_escaped:
if (buffer !== false) {
return false;
}
if (start_queue && !last) {
buffer = [chr];
bl = 1;
}
else {
buffer = [];
bl = 0;
}
// exit if not last
if (!last) {
break;
}
/* falls through */
case queue:
if (buffer === false) {
return false;
}
buffer[bl++] = chr;
// exit if not last
if (!last) {
break;
}
/* falls through */
case end:
if (buffer === false) {
return false;
}
items[len++] = buffer.join('');
buffer = bl = false;
break;
case end_empty:
if (buffer !== false) {
return false;
}
items[len++] = '';
break;
}
}
}
state = next;
stateObject = map[state];
if (pending < len - 1) {
if (callback(items[pending++],
false,
arg1,
arg2,
arg3,
arg4,
arg5) === false) {
return true;
}
}
// last
if (last) {
l = len - pending;
for (; l--;) {
if (callback(items[pending++],
!l,
arg1,
arg2,
arg3,
arg4,
arg5) === false) {
return true;
}
}
break;
}
}
return true;
}
export function jsonSet(path, subject, value, overwrite) {
var typeArray = ARRAY,
apply = objectAssign,
writable = isWritable;
var context, name, current, valueSignature, currentSignature,
arrayOperation, arrayPush, canApply;
if (!string(path)) {
throw new Error(ERROR_PATH_INVALID);
}
// main subject should be accessible and native object
context = [void(0), subject, false];
jsonEach(path, onPopulatePath, context);
name = context[2];
if (name !== false) {
subject = context[1];
valueSignature = writable(value);
arrayOperation = array(subject) && NUMERIC_RE.test(name);
if (name in subject) {
current = subject[name];
currentSignature = writable(current);
}
else {
current = undefined;
currentSignature = null;
}
canApply = valueSignature && !!currentSignature;
arrayPush = canApply &&
valueSignature === typeArray &&
currentSignature === typeArray;
// finalize overwrite type
switch (overwrite) {
// only available if subject is array and name is numeric index
case 'insert':
overwrite = !arrayOperation;
if (arrayOperation) {
subject.splice(name * 1, 0, value);
}
break;
// only available if subject canApply
case 'apply':
overwrite = !canApply;
if (canApply) {
apply(current, value);
}
break;
// only available if current is array and value is array
case 'push':
overwrite = !arrayPush;
if (arrayPush) {
current.push.apply(current, value);
}
break;
// only available if current is array and value is array
case 'unshift':
overwrite = !arrayPush;
if (arrayPush) {
current.splice.apply(current, [0, 0].concat(value));
}
break;
// default is no overwrite if possible
case false:
/* falls through */
default:
// can apply or push only if non-scalar current and value
overwrite = !canApply;
if (canApply) {
if (arrayPush) {
current.push.apply(current, value);
}
else {
apply(current, value);
}
}
}
// plain overwrite!
if (overwrite === true) {
subject[name] = value;
}
return true;
}
return false;
}
export function jsonUnset(path, subject) {
var context, name, returnValue;
if (!string(path)) {
throw new Error(ERROR_PATH_INVALID);
}
// main subject should be accessible and native object
context = [void(0), subject, false, false];
jsonEach(path, onRemovePath, context);
name = context[2];
returnValue = context[3];
// found! and must be removed
if (returnValue && name !== false) {
subject = context[1];
if (!(name in subject)) {
returnValue = false;
}
else {
// remove item
if (array(subject) && NUMERIC_RE.test(name)) {
subject.splice(name * 1, 1);
}
else {
delete subject[name];
// check if removable
returnValue = !(name in subject);
}
}
}
return returnValue;
}
export function jsonFill(path, subject, value) { //, overwrite) {
var typeArray = ARRAY,
getMax = maxObjectIndex,
apply = objectAssign,
has = contains,
arrayIndexRe = ARRAY_INDEX_RE,
iswritable = isJSONWritable,
isSubjectArray = array(subject);
var parent, c, l, item, parentIndex,
property, arrayIndex, writable;
if (!string(path)) {
throw new Error(ERROR_PATH_INVALID);
}
// root subject should be an object
if (!object(subject) && !isSubjectArray) {
return false;
}
// unable to create items from path
path = jsonParsePath(path);
if (!path || !path.length) {
return false;
}
parent = subject;
parentIndex = path[0];
// finalize parent index
if (!parentIndex) {
parentIndex = getMax(parent) + 1;
}
l = path.length -1;
for (c = 0; l--;) {
item = path[++c];
// only determine if arrayIndex or not,
// resolve this later if it will turn into parentIndex
arrayIndex = arrayIndexRe.test(item);
// finalize property
if (has(parent, parentIndex)) {
property = parent[parentIndex];
writable = iswritable(property);
// recreate array into object to support "named" property
if (writable === typeArray && !arrayIndex) {
property = apply({}, property);
delete property.length;
}
// contain current property
else if (!writable) {
property = arrayIndex ?
[property] : {"": property};
}
}
// error! unable to replace root object
else if (isSubjectArray && parent === subject && !arrayIndex) {
throw new Error(ERROR_NATIVE_OBJECT);
}
// populate
else {
property = arrayIndex ? [] : {};
}
parent = parent[parentIndex] = property;
parentIndex = item;
// resolve empty parentIndex
if (!item) {
parentIndex = getMax(parent) + 1;
}
}
// if not overwrite, then fill-in value in array or object
//if (overwrite !== true && has(parent, parentIndex)) {
// property = parent[parentIndex];
//
// // append
// if (T.array(property)) {
// parent = property;
// parentIndex = parent.length;
// }
// else {
// parent = parent[parentIndex] = [property];
// parentIndex = 1;
// }
//}
parent[parentIndex] = value;
return true;
}
export function jsonExists(path, subject) {
var operation = [subject, false];
jsonEach(path, existsCallback, operation);
operation[0] = null;
return operation[1];
}