mirror of
https://github.com/liabru/matter-js.git
synced 2024-12-25 13:39:06 -05:00
439 lines
13 KiB
JavaScript
439 lines
13 KiB
JavaScript
/**
|
|
* # ResurrectJS
|
|
* @version 1.0.0
|
|
*
|
|
* ResurrectJS preserves object behavior (prototypes) and reference
|
|
* circularity with a special JSON encoding. Unlike regular JSON,
|
|
* Date, RegExp, DOM objects, and `undefined` are also properly
|
|
* preserved.
|
|
*
|
|
* ## Examples
|
|
*
|
|
* function Foo() {}
|
|
* Foo.prototype.greet = function() { return "hello"; };
|
|
*
|
|
* // Behavior is preserved:
|
|
* var necromancer = new Resurrect();
|
|
* var json = necromancer.stringify(new Foo());
|
|
* var foo = necromancer.resurrect(json);
|
|
* foo.greet(); // => "hello"
|
|
*
|
|
* // References to the same object are preserved:
|
|
* json = necromancer.stringify([foo, foo]);
|
|
* var array = necromancer.resurrect(json);
|
|
* array[0] === array[1]; // => true
|
|
* array[1].greet(); // => "hello"
|
|
*
|
|
* // Dates are restored properly
|
|
* json = necromancer.stringify(new Date());
|
|
* var date = necromancer.resurrect(json);
|
|
* Object.prototype.toString.call(date); // => "[object Date]"
|
|
*
|
|
* ## Options
|
|
*
|
|
* Options are provided to the constructor as an object with these
|
|
* properties:
|
|
*
|
|
* prefix ('#'): A prefix string used for temporary properties added
|
|
* to objects during serialization and deserialization. It is
|
|
* important that you don't use any properties beginning with this
|
|
* string. This option must be consistent between both
|
|
* serialization and deserialization.
|
|
*
|
|
* cleanup (false): Perform full property cleanup after both
|
|
* serialization and deserialization using the `delete`
|
|
* operator. This may cause performance penalties (breaking hidden
|
|
* classes in V8) on objects that ResurrectJS touches, so enable
|
|
* with care.
|
|
*
|
|
* revive (true): Restore behavior (__proto__) to objects that have
|
|
* been resurrected. If this is set to false during serialization,
|
|
* resurrection information will not be encoded. You still get
|
|
* circularity and Date support.
|
|
*
|
|
* resolver (Resurrect.NamespaceResolver(window)): Converts between
|
|
* a name and a prototype. Create a custom resolver if your
|
|
* constructors are not stored in global variables. The resolver
|
|
* has two methods: getName(object) and getPrototype(string).
|
|
*
|
|
* For example,
|
|
*
|
|
* var necromancer = new Resurrect({
|
|
* prefix: '__#',
|
|
* cleanup: true
|
|
* });
|
|
*
|
|
* ## Caveats
|
|
*
|
|
* * With the default resolver, all constructors must be named and
|
|
* stored in the global variable under that name. This is required
|
|
* so that the prototypes can be looked up and reconnected at
|
|
* resurrection time.
|
|
*
|
|
* * The wrapper objects Boolean, String, and Number will be
|
|
* unwrapped. This means extra properties added to these objects
|
|
* will not be preserved.
|
|
*
|
|
* * Functions cannot ever be serialized. Resurrect will throw an
|
|
* error if a function is found when traversing a data structure.
|
|
*/
|
|
|
|
/**
|
|
* @namespace
|
|
* @constructor
|
|
*/
|
|
function Resurrect(options) {
|
|
this.table = null;
|
|
this.prefix = '#';
|
|
this.cleanup = false;
|
|
this.revive = true;
|
|
for (var option in options) {
|
|
if (options.hasOwnProperty(option)) {
|
|
this[option] = options[option];
|
|
}
|
|
}
|
|
this.refcode = this.prefix + 'id';
|
|
this.origcode = this.prefix + 'original';
|
|
this.buildcode = this.prefix + '.';
|
|
this.valuecode = this.prefix + 'v';
|
|
}
|
|
|
|
/* Helper Objects */
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
Resurrect.prototype.Error = function ResurrectError(message) {
|
|
this.message = message || '';
|
|
this.stack = new Error().stack;
|
|
};
|
|
Resurrect.prototype.Error.prototype = Object.create(Error.prototype);
|
|
Resurrect.prototype.Error.prototype.name = 'ResurrectError';
|
|
|
|
/**
|
|
* Resolves prototypes through the properties on an object and
|
|
* constructor names.
|
|
* @constructor
|
|
*/
|
|
Resurrect.NamespaceResolver = function(scope) {
|
|
this.scope = scope;
|
|
};
|
|
|
|
/**
|
|
* Gets the prototype of the given property name from an object. If
|
|
* not found, it throws an error.
|
|
* @param {string} name
|
|
* @method
|
|
*/
|
|
Resurrect.NamespaceResolver.prototype.getPrototype = function(name) {
|
|
var constructor = this.scope[name];
|
|
if (constructor) {
|
|
return constructor.prototype;
|
|
} else {
|
|
throw new Resurrect.prototype.Error('Unknown constructor: ' + name);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the prototype name for an object, to be fetched later with
|
|
* getPrototype.
|
|
* @returns {string} Null if the constructor is Object.
|
|
* @method
|
|
*/
|
|
Resurrect.NamespaceResolver.prototype.getName = function(object) {
|
|
var constructor = object.constructor.name;
|
|
if (constructor == null) { // IE
|
|
var funcPattern = /^\s*function\s*([A-Za-z0-9_$]*)/;
|
|
constructor = funcPattern.exec(object.constructor)[1];
|
|
}
|
|
|
|
if (constructor === '') {
|
|
var msg = "Can't serialize objects with anonymous constructors.";
|
|
throw new Resurrect.prototype.Error(msg);
|
|
} else if (constructor === 'Object' || constructor === 'Array') {
|
|
return null;
|
|
} else {
|
|
return constructor;
|
|
}
|
|
};
|
|
|
|
/* Set the default resolver searches the global object. */
|
|
Resurrect.prototype.resolver = new Resurrect.NamespaceResolver(window);
|
|
|
|
/**
|
|
* Create a DOM node from HTML source; behaves like a constructor.
|
|
* @constructor
|
|
*/
|
|
Resurrect.Node = function(html) {
|
|
var div = document.createElement('div');
|
|
div.innerHTML = html;
|
|
return div.firstChild;
|
|
};
|
|
|
|
/* Type Tests */
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @returns {Function} A function that tests for type.
|
|
*/
|
|
Resurrect.is = function(type) {
|
|
var string = '[object ' + type + ']';
|
|
return function(object) {
|
|
return Object.prototype.toString.call(object) === string;
|
|
};
|
|
};
|
|
|
|
Resurrect.isArray = Resurrect.is('Array');
|
|
Resurrect.isString = Resurrect.is('String');
|
|
Resurrect.isBoolean = Resurrect.is('Boolean');
|
|
Resurrect.isNumber = Resurrect.is('Number');
|
|
Resurrect.isFunction = Resurrect.is('Function');
|
|
Resurrect.isDate = Resurrect.is('Date');
|
|
Resurrect.isRegExp = Resurrect.is('RegExp');
|
|
Resurrect.isObject = Resurrect.is('Object');
|
|
|
|
Resurrect.isAtom = function(object) {
|
|
return !Resurrect.isObject(object) && !Resurrect.isArray(object);
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Create a reference (encoding) to an object.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.ref = function(object) {
|
|
var ref = {};
|
|
if (object === undefined) {
|
|
ref[this.prefix] = -1;
|
|
} else {
|
|
ref[this.prefix] = object[this.refcode];
|
|
}
|
|
return ref;
|
|
};
|
|
|
|
/**
|
|
* Lookup an object in the table by reference object.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.deref = function(ref) {
|
|
return this.table[ref[this.prefix]];
|
|
};
|
|
|
|
/**
|
|
* Put a temporary identifier on an object and store it in the table.
|
|
* @returns {number} The unique identifier number.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.tag = function(object) {
|
|
if (this.revive) {
|
|
var constructor = this.resolver.getName(object);
|
|
if (constructor) {
|
|
var proto = Object.getPrototypeOf(object);
|
|
if (this.resolver.getPrototype(constructor) !== proto) {
|
|
throw new this.Error('Constructor mismatch!');
|
|
} else {
|
|
object[this.prefix] = constructor;
|
|
}
|
|
}
|
|
}
|
|
object[this.refcode] = this.table.length;
|
|
this.table.push(object);
|
|
return object[this.refcode];
|
|
};
|
|
|
|
/**
|
|
* Create a builder object (encoding) for serialization.
|
|
* @param {string} name The name of the constructor.
|
|
* @param value The value to pass to the constructor.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.builder = function(name, value) {
|
|
var builder = {};
|
|
builder[this.buildcode] = name;
|
|
builder[this.valuecode] = value;
|
|
return builder;
|
|
};
|
|
|
|
/**
|
|
* Build a value from a deserialized builder.
|
|
* @method
|
|
* @see http://stackoverflow.com/a/14378462
|
|
*/
|
|
Resurrect.prototype.build = function(ref) {
|
|
var type = ref[this.buildcode].split(/\./).reduce(function(object, name) {
|
|
return object[name];
|
|
}, window);
|
|
/* Brilliant hack by kybernetikos: */
|
|
var args = [null].concat(ref[this.valuecode]);
|
|
var factory = type.bind.apply(type, args);
|
|
return new factory();
|
|
};
|
|
|
|
/**
|
|
* Dereference or build an object or value from an encoding.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.decode = function(ref) {
|
|
if (this.prefix in ref) {
|
|
return this.deref(ref);
|
|
} else if (this.buildcode in ref) {
|
|
return this.build(ref);
|
|
} else {
|
|
throw new this.Error('Unknown encoding.');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @returns True if the provided object is already tagged for serialization.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.isTagged = function(object) {
|
|
return (this.refcode in object) && (object[this.refcode] != null);
|
|
};
|
|
|
|
/**
|
|
* Visit root and all its ancestors, visiting atoms with f.
|
|
* @param {Function} f
|
|
* @returns A fresh copy of root to be serialized.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.visit = function(root, f) {
|
|
if (Resurrect.isAtom(root)) {
|
|
return f(root);
|
|
} else if (!this.isTagged(root)) {
|
|
var copy = null;
|
|
if (Resurrect.isArray(root)) {
|
|
copy = [];
|
|
root[this.refcode] = this.tag(copy);
|
|
for (var i = 0; i < root.length; i++) {
|
|
copy.push(this.visit(root[i], f));
|
|
}
|
|
} else { /* Object */
|
|
copy = Object.create(Object.getPrototypeOf(root));
|
|
root[this.refcode] = this.tag(copy);
|
|
for (var key in root) {
|
|
if (root.hasOwnProperty(key)) {
|
|
copy[key] = this.visit(root[key], f);
|
|
}
|
|
}
|
|
}
|
|
copy[this.origcode] = root;
|
|
return this.ref(copy);
|
|
} else {
|
|
return this.ref(root);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Manage special atom values, possibly returning an encoding.
|
|
*/
|
|
Resurrect.prototype.handleAtom = function(atom) {
|
|
if (Resurrect.isFunction(atom)) {
|
|
throw new this.Error("Can't serialize functions.");
|
|
} else if (atom instanceof Node) {
|
|
var xmls = new XMLSerializer();
|
|
return this.builder('Resurrect.Node', [xmls.serializeToString(atom)]);
|
|
} else if (Resurrect.isDate(atom)) {
|
|
return this.builder('Date', [atom.toISOString()]);
|
|
} else if (Resurrect.isRegExp(atom)) {
|
|
var args = atom.toString().match(/\/(.+)\/([gimy]*)/).slice(1);
|
|
return this.builder('RegExp', args);
|
|
} else if (atom === undefined) {
|
|
return this.ref(undefined);
|
|
} else {
|
|
return atom;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Serialize an arbitrary JavaScript object, carefully preserving it.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.stringify = function(object) {
|
|
if (Resurrect.isAtom(object)) {
|
|
return JSON.stringify(this.handleAtom(object));
|
|
} else {
|
|
this.table = [];
|
|
this.visit(object, this.handleAtom.bind(this));
|
|
for (var i = 0; i < this.table.length; i++) {
|
|
if (this.cleanup) {
|
|
delete this.table[i][this.origcode][this.refcode];
|
|
} else {
|
|
this.table[i][this.origcode][this.refcode] = null;
|
|
}
|
|
delete this.table[i][this.refcode];
|
|
delete this.table[i][this.origcode];
|
|
}
|
|
var table = this.table;
|
|
this.table = null;
|
|
return JSON.stringify(table);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Restore the __proto__ of the given object to the proper value.
|
|
* @returns The object.
|
|
*/
|
|
Resurrect.prototype.fixPrototype = function(object) {
|
|
if (this.prefix in object) {
|
|
var name = object[this.prefix];
|
|
var prototype = this.resolver.getPrototype(name);
|
|
if ('__proto__' in object) {
|
|
object.__proto__ = prototype;
|
|
if (this.cleanup) {
|
|
delete object[this.prefix];
|
|
}
|
|
return object;
|
|
} else { // IE
|
|
var copy = Object.create(prototype);
|
|
for (var key in object) {
|
|
if (object.hasOwnProperty(key) && key !== this.prefix) {
|
|
copy[key] = object[key];
|
|
}
|
|
}
|
|
return copy;
|
|
}
|
|
} else {
|
|
return object;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deserialize an encoded object, restoring circularity and behavior.
|
|
* @param {string} string
|
|
* @returns The decoded object or value.
|
|
* @method
|
|
*/
|
|
Resurrect.prototype.resurrect = function(string) {
|
|
var result = null;
|
|
var data = JSON.parse(string);
|
|
if (Resurrect.isArray(data)) {
|
|
this.table = data;
|
|
/* Restore __proto__. */
|
|
if (this.revive) {
|
|
for (var i = 0; i < this.table.length; i++) {
|
|
this.table[i] = this.fixPrototype(this.table[i]);
|
|
}
|
|
}
|
|
/* Re-establish object references and construct atoms. */
|
|
for (i = 0; i < this.table.length; i++) {
|
|
var object = this.table[i];
|
|
for (var key in object) {
|
|
if (object.hasOwnProperty(key)) {
|
|
if (!(Resurrect.isAtom(object[key]))) {
|
|
object[key] = this.decode(object[key]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result = this.table[0];
|
|
} else if (Resurrect.isObject(data)) {
|
|
this.table = [];
|
|
result = this.decode(data);
|
|
} else {
|
|
result = data;
|
|
}
|
|
this.table = null;
|
|
return result;
|
|
};
|