You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
9.2 KiB
JavaScript
349 lines
9.2 KiB
JavaScript
7 years ago
|
var assert = require("assert");
|
||
|
var Op = Object.prototype;
|
||
|
var hasOwn = Op.hasOwnProperty;
|
||
|
var types = require("./types");
|
||
|
var isArray = types.builtInTypes.array;
|
||
|
var isNumber = types.builtInTypes.number;
|
||
|
var Ap = Array.prototype;
|
||
|
var slice = Ap.slice;
|
||
|
var map = Ap.map;
|
||
|
|
||
|
function Path(value, parentPath, name) {
|
||
|
assert.ok(this instanceof Path);
|
||
|
|
||
|
if (parentPath) {
|
||
|
assert.ok(parentPath instanceof Path);
|
||
|
} else {
|
||
|
parentPath = null;
|
||
|
name = null;
|
||
|
}
|
||
|
|
||
|
// The value encapsulated by this Path, generally equal to
|
||
|
// parentPath.value[name] if we have a parentPath.
|
||
|
this.value = value;
|
||
|
|
||
|
// The immediate parent Path of this Path.
|
||
|
this.parentPath = parentPath;
|
||
|
|
||
|
// The name of the property of parentPath.value through which this
|
||
|
// Path's value was reached.
|
||
|
this.name = name;
|
||
|
|
||
|
// Calling path.get("child") multiple times always returns the same
|
||
|
// child Path object, for both performance and consistency reasons.
|
||
|
this.__childCache = null;
|
||
|
}
|
||
|
|
||
|
var Pp = Path.prototype;
|
||
|
|
||
|
function getChildCache(path) {
|
||
|
// Lazily create the child cache. This also cheapens cache
|
||
|
// invalidation, since you can just reset path.__childCache to null.
|
||
|
return path.__childCache || (path.__childCache = Object.create(null));
|
||
|
}
|
||
|
|
||
|
function getChildPath(path, name) {
|
||
|
var cache = getChildCache(path);
|
||
|
var actualChildValue = path.getValueProperty(name);
|
||
|
var childPath = cache[name];
|
||
|
if (!hasOwn.call(cache, name) ||
|
||
|
// Ensure consistency between cache and reality.
|
||
|
childPath.value !== actualChildValue) {
|
||
|
childPath = cache[name] = new path.constructor(
|
||
|
actualChildValue, path, name
|
||
|
);
|
||
|
}
|
||
|
return childPath;
|
||
|
}
|
||
|
|
||
|
// This method is designed to be overridden by subclasses that need to
|
||
|
// handle missing properties, etc.
|
||
|
Pp.getValueProperty = function getValueProperty(name) {
|
||
|
return this.value[name];
|
||
|
};
|
||
|
|
||
|
Pp.get = function get(name) {
|
||
|
var path = this;
|
||
|
var names = arguments;
|
||
|
var count = names.length;
|
||
|
|
||
|
for (var i = 0; i < count; ++i) {
|
||
|
path = getChildPath(path, names[i]);
|
||
|
}
|
||
|
|
||
|
return path;
|
||
|
};
|
||
|
|
||
|
Pp.each = function each(callback, context) {
|
||
|
var childPaths = [];
|
||
|
var len = this.value.length;
|
||
|
var i = 0;
|
||
|
|
||
|
// Collect all the original child paths before invoking the callback.
|
||
|
for (var i = 0; i < len; ++i) {
|
||
|
if (hasOwn.call(this.value, i)) {
|
||
|
childPaths[i] = this.get(i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Invoke the callback on just the original child paths, regardless of
|
||
|
// any modifications made to the array by the callback. I chose these
|
||
|
// semantics over cleverly invoking the callback on new elements because
|
||
|
// this way is much easier to reason about.
|
||
|
context = context || this;
|
||
|
for (i = 0; i < len; ++i) {
|
||
|
if (hasOwn.call(childPaths, i)) {
|
||
|
callback.call(context, childPaths[i]);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Pp.map = function map(callback, context) {
|
||
|
var result = [];
|
||
|
|
||
|
this.each(function(childPath) {
|
||
|
result.push(callback.call(this, childPath));
|
||
|
}, context);
|
||
|
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
Pp.filter = function filter(callback, context) {
|
||
|
var result = [];
|
||
|
|
||
|
this.each(function(childPath) {
|
||
|
if (callback.call(this, childPath)) {
|
||
|
result.push(childPath);
|
||
|
}
|
||
|
}, context);
|
||
|
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
function emptyMoves() {}
|
||
|
function getMoves(path, offset, start, end) {
|
||
|
isArray.assert(path.value);
|
||
|
|
||
|
if (offset === 0) {
|
||
|
return emptyMoves;
|
||
|
}
|
||
|
|
||
|
var length = path.value.length;
|
||
|
if (length < 1) {
|
||
|
return emptyMoves;
|
||
|
}
|
||
|
|
||
|
var argc = arguments.length;
|
||
|
if (argc === 2) {
|
||
|
start = 0;
|
||
|
end = length;
|
||
|
} else if (argc === 3) {
|
||
|
start = Math.max(start, 0);
|
||
|
end = length;
|
||
|
} else {
|
||
|
start = Math.max(start, 0);
|
||
|
end = Math.min(end, length);
|
||
|
}
|
||
|
|
||
|
isNumber.assert(start);
|
||
|
isNumber.assert(end);
|
||
|
|
||
|
var moves = Object.create(null);
|
||
|
var cache = getChildCache(path);
|
||
|
|
||
|
for (var i = start; i < end; ++i) {
|
||
|
if (hasOwn.call(path.value, i)) {
|
||
|
var childPath = path.get(i);
|
||
|
assert.strictEqual(childPath.name, i);
|
||
|
var newIndex = i + offset;
|
||
|
childPath.name = newIndex;
|
||
|
moves[newIndex] = childPath;
|
||
|
delete cache[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
delete cache.length;
|
||
|
|
||
|
return function() {
|
||
|
for (var newIndex in moves) {
|
||
|
var childPath = moves[newIndex];
|
||
|
assert.strictEqual(childPath.name, +newIndex);
|
||
|
cache[newIndex] = childPath;
|
||
|
path.value[newIndex] = childPath.value;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
Pp.shift = function shift() {
|
||
|
var move = getMoves(this, -1);
|
||
|
var result = this.value.shift();
|
||
|
move();
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
Pp.unshift = function unshift(node) {
|
||
|
var move = getMoves(this, arguments.length);
|
||
|
var result = this.value.unshift.apply(this.value, arguments);
|
||
|
move();
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
Pp.push = function push(node) {
|
||
|
isArray.assert(this.value);
|
||
|
delete getChildCache(this).length
|
||
|
return this.value.push.apply(this.value, arguments);
|
||
|
};
|
||
|
|
||
|
Pp.pop = function pop() {
|
||
|
isArray.assert(this.value);
|
||
|
var cache = getChildCache(this);
|
||
|
delete cache[this.value.length - 1];
|
||
|
delete cache.length;
|
||
|
return this.value.pop();
|
||
|
};
|
||
|
|
||
|
Pp.insertAt = function insertAt(index, node) {
|
||
|
var argc = arguments.length;
|
||
|
var move = getMoves(this, argc - 1, index);
|
||
|
if (move === emptyMoves) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
index = Math.max(index, 0);
|
||
|
|
||
|
for (var i = 1; i < argc; ++i) {
|
||
|
this.value[index + i - 1] = arguments[i];
|
||
|
}
|
||
|
|
||
|
move();
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
Pp.insertBefore = function insertBefore(node) {
|
||
|
var pp = this.parentPath;
|
||
|
var argc = arguments.length;
|
||
|
var insertAtArgs = [this.name];
|
||
|
for (var i = 0; i < argc; ++i) {
|
||
|
insertAtArgs.push(arguments[i]);
|
||
|
}
|
||
|
return pp.insertAt.apply(pp, insertAtArgs);
|
||
|
};
|
||
|
|
||
|
Pp.insertAfter = function insertAfter(node) {
|
||
|
var pp = this.parentPath;
|
||
|
var argc = arguments.length;
|
||
|
var insertAtArgs = [this.name + 1];
|
||
|
for (var i = 0; i < argc; ++i) {
|
||
|
insertAtArgs.push(arguments[i]);
|
||
|
}
|
||
|
return pp.insertAt.apply(pp, insertAtArgs);
|
||
|
};
|
||
|
|
||
|
function repairRelationshipWithParent(path) {
|
||
|
assert.ok(path instanceof Path);
|
||
|
|
||
|
var pp = path.parentPath;
|
||
|
if (!pp) {
|
||
|
// Orphan paths have no relationship to repair.
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
var parentValue = pp.value;
|
||
|
var parentCache = getChildCache(pp);
|
||
|
|
||
|
// Make sure parentCache[path.name] is populated.
|
||
|
if (parentValue[path.name] === path.value) {
|
||
|
parentCache[path.name] = path;
|
||
|
} else if (isArray.check(parentValue)) {
|
||
|
// Something caused path.name to become out of date, so attempt to
|
||
|
// recover by searching for path.value in parentValue.
|
||
|
var i = parentValue.indexOf(path.value);
|
||
|
if (i >= 0) {
|
||
|
parentCache[path.name = i] = path;
|
||
|
}
|
||
|
} else {
|
||
|
// If path.value disagrees with parentValue[path.name], and
|
||
|
// path.name is not an array index, let path.value become the new
|
||
|
// parentValue[path.name] and update parentCache accordingly.
|
||
|
parentValue[path.name] = path.value;
|
||
|
parentCache[path.name] = path;
|
||
|
}
|
||
|
|
||
|
assert.strictEqual(parentValue[path.name], path.value);
|
||
|
assert.strictEqual(path.parentPath.get(path.name), path);
|
||
|
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
Pp.replace = function replace(replacement) {
|
||
|
var results = [];
|
||
|
var parentValue = this.parentPath.value;
|
||
|
var parentCache = getChildCache(this.parentPath);
|
||
|
var count = arguments.length;
|
||
|
|
||
|
repairRelationshipWithParent(this);
|
||
|
|
||
|
if (isArray.check(parentValue)) {
|
||
|
var originalLength = parentValue.length;
|
||
|
var move = getMoves(this.parentPath, count - 1, this.name + 1);
|
||
|
|
||
|
var spliceArgs = [this.name, 1];
|
||
|
for (var i = 0; i < count; ++i) {
|
||
|
spliceArgs.push(arguments[i]);
|
||
|
}
|
||
|
|
||
|
var splicedOut = parentValue.splice.apply(parentValue, spliceArgs);
|
||
|
|
||
|
assert.strictEqual(splicedOut[0], this.value);
|
||
|
assert.strictEqual(
|
||
|
parentValue.length,
|
||
|
originalLength - 1 + count
|
||
|
);
|
||
|
|
||
|
move();
|
||
|
|
||
|
if (count === 0) {
|
||
|
delete this.value;
|
||
|
delete parentCache[this.name];
|
||
|
this.__childCache = null;
|
||
|
|
||
|
} else {
|
||
|
assert.strictEqual(parentValue[this.name], replacement);
|
||
|
|
||
|
if (this.value !== replacement) {
|
||
|
this.value = replacement;
|
||
|
this.__childCache = null;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < count; ++i) {
|
||
|
results.push(this.parentPath.get(this.name + i));
|
||
|
}
|
||
|
|
||
|
assert.strictEqual(results[0], this);
|
||
|
}
|
||
|
|
||
|
} else if (count === 1) {
|
||
|
if (this.value !== replacement) {
|
||
|
this.__childCache = null;
|
||
|
}
|
||
|
this.value = parentValue[this.name] = replacement;
|
||
|
results.push(this);
|
||
|
|
||
|
} else if (count === 0) {
|
||
|
delete parentValue[this.name];
|
||
|
delete this.value;
|
||
|
this.__childCache = null;
|
||
|
|
||
|
// Leave this path cached as parentCache[this.name], even though
|
||
|
// it no longer has a value defined.
|
||
|
|
||
|
} else {
|
||
|
assert.ok(false, "Could not replace path");
|
||
|
}
|
||
|
|
||
|
return results;
|
||
|
};
|
||
|
|
||
|
module.exports = Path;
|