JSON: Improve serialization and deserialization on objects other than Item.

Closes #392
This commit is contained in:
Jürg Lehni 2016-02-14 17:16:40 +01:00
parent 75c40babc9
commit 56dd636f22
4 changed files with 78 additions and 30 deletions

View file

@ -46,6 +46,17 @@ Base.inject(/** @lends Base# */{
return this._class || ''; return this._class || '';
}, },
/**
* Imports (deserializes) the stored JSON data into the object, if the
* classes match. If they do not match, a newly created object is returned
* instead.
*
* @param {String} json the JSON data to import from
*/
importJSON: function(json) {
return Base.importJSON(json, this);
},
/** /**
* Exports (serializes) this object to a JSON data object or string. * Exports (serializes) this object to a JSON data object or string.
* *
@ -332,9 +343,9 @@ Base.inject(/** @lends Base# */{
serialize: function(obj, options, compact, dictionary) { serialize: function(obj, options, compact, dictionary) {
options = options || {}; options = options || {};
var root = !dictionary, var isRoot = !dictionary,
res; res;
if (root) { if (isRoot) {
options.formatter = new Formatter(options.precision); options.formatter = new Formatter(options.precision);
// Create a simple dictionary object that handles all the // Create a simple dictionary object that handles all the
// storing and retrieving of dictionary definitions and // storing and retrieving of dictionary definitions and
@ -376,17 +387,17 @@ Base.inject(/** @lends Base# */{
// identifier), see if _serialize didn't already add the class, // identifier), see if _serialize didn't already add the class,
// e.g. for classes that do not support compact form. // e.g. for classes that do not support compact form.
var name = obj._class; var name = obj._class;
if (name && !compact && !res._compact && res[0] !== name) // Enforce class names on root level, except if the class
// explicitly asks to be serialized in compact form (Project).
if (name && !obj._compactSerialize && (isRoot || !compact)
&& res[0] !== name) {
res.unshift(name); res.unshift(name);
}
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
res = []; res = [];
for (var i = 0, l = obj.length; i < l; i++) for (var i = 0, l = obj.length; i < l; i++)
res[i] = Base.serialize(obj[i], options, compact, res[i] = Base.serialize(obj[i], options, compact,
dictionary); dictionary);
// Mark array as compact, so obj._serialize handling above
// doesn't add the class name again.
if (compact)
res._compact = true;
} else if (Base.isPlainObject(obj)) { } else if (Base.isPlainObject(obj)) {
res = {}; res = {};
var keys = Object.keys(obj); var keys = Object.keys(obj);
@ -400,7 +411,7 @@ Base.inject(/** @lends Base# */{
} else { } else {
res = obj; res = obj;
} }
return root && dictionary.length > 0 return isRoot && dictionary.length > 0
? [['dictionary', dictionary.definitions], res] ? [['dictionary', dictionary.definitions], res]
: res; : res;
}, },
@ -414,9 +425,11 @@ Base.inject(/** @lends Base# */{
* The passed json data is recoursively traversed and converted, leaves * The passed json data is recoursively traversed and converted, leaves
* first * first
*/ */
deserialize: function(json, create, _data, _isDictionary) { deserialize: function(json, create, _data, _setDictionary, _isRoot) {
var res = json, var res = json,
isRoot = !_data; isFirst = !_data,
hasDictionary = isFirst && json && json.length
&& json[0][0] === 'dictionary';
// A _data side-car to deserialize that can hold any kind of // A _data side-car to deserialize that can hold any kind of
// 'global' data across a deserialization. It's currently only used // 'global' data across a deserialization. It's currently only used
// to hold dictionary definitions. // to hold dictionary definitions.
@ -431,19 +444,18 @@ Base.inject(/** @lends Base# */{
isDictionary = type === 'dictionary'; isDictionary = type === 'dictionary';
// First see if this is perhaps a dictionary reference, and // First see if this is perhaps a dictionary reference, and
// if so return its definition instead. // if so return its definition instead.
if (json.length == 1 && /^#/.test(type)) if (json.length == 1 && /^#/.test(type)) {
return _data.dictionary[type]; return _data.dictionary[type];
}
type = Base.exports[type]; type = Base.exports[type];
res = []; res = [];
// We need to set the dictionary object before further
// deserialization, because serialized symbols may contain
// references to serialized gradients
if (_isDictionary)
_data.dictionary = res;
// Skip first type entry for arguments // Skip first type entry for arguments
for (var i = type ? 1 : 0, l = json.length; i < l; i++) // Pass true for _isRoot in children if we have a dictionary,
// in which case we need to shift the root level one down.
for (var i = type ? 1 : 0, l = json.length; i < l; i++) {
res.push(Base.deserialize(json[i], create, _data, res.push(Base.deserialize(json[i], create, _data,
isDictionary)); isDictionary, hasDictionary));
}
if (type) { if (type) {
// Create serialized type and pass collected arguments to // Create serialized type and pass collected arguments to
// constructor(). // constructor().
@ -452,7 +464,7 @@ Base.inject(/** @lends Base# */{
// creation. This is used in #importJSON() to pass // creation. This is used in #importJSON() to pass
// on insert = false to all items except layers. // on insert = false to all items except layers.
if (create) { if (create) {
res = create(type, args, isRoot); res = create(type, args, isFirst || _isRoot);
} else { } else {
res = Base.create(type.prototype); res = Base.create(type.prototype);
type.apply(res, args); type.apply(res, args);
@ -460,16 +472,16 @@ Base.inject(/** @lends Base# */{
} }
} else if (Base.isPlainObject(json)) { } else if (Base.isPlainObject(json)) {
res = {}; res = {};
// See above why we have to set this before Base.deserialize() // We need to set the dictionary object before further
if (_isDictionary) // deserialization, because serialized symbols may contain
// references to serialized gradients
if (_setDictionary)
_data.dictionary = res; _data.dictionary = res;
for (var key in json) for (var key in json)
res[key] = Base.deserialize(json[key], create, _data); res[key] = Base.deserialize(json[key], create, _data);
} }
// Filter out deserialized dictionary. // Filter out deserialized dictionary:
return isRoot && json && json.length && json[0][0] === 'dictionary' return hasDictionary ? res[1] : res;
? res[1]
: res;
}, },
exportJSON: function(obj, options) { exportJSON: function(obj, options) {
@ -491,9 +503,11 @@ Base.inject(/** @lends Base# */{
&& target.constructor === ctor, && target.constructor === ctor,
obj = useTarget ? target obj = useTarget ? target
: Base.create(ctor.prototype), : Base.create(ctor.prototype),
// When reusing an object, try to initialize it // When reusing an object, try to (re)initialize it
// through _initialize (Item), fall-back to _set. // through _initialize (Item), fall-back to
init = useTarget ? obj._initialize || obj._set // initialize (Color & co), then _set.
init = useTarget
? obj._initialize || obj.initialize || obj._set
: ctor; : ctor;
// NOTE: We don't set insert false for layers since we // NOTE: We don't set insert false for layers since we
// want these to be created on the fly in the active // want these to be created on the fly in the active

View file

@ -34,6 +34,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
_class: 'Project', _class: 'Project',
_list: 'projects', _list: 'projects',
_reference: 'project', _reference: 'project',
_compactSerialize: true, // Never include the class name for Project
// TODO: Add arguments to define pages // TODO: Add arguments to define pages
/** /**
@ -75,7 +76,6 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
// into the active project automatically. We might want to add proper // into the active project automatically. We might want to add proper
// project serialization later, but deserialization of a layers array // project serialization later, but deserialization of a layers array
// will always work. // will always work.
// Pass true for compact, so 'Project' does not get added as the class
return Base.serialize(this._children, options, true, dictionary); return Base.serialize(this._children, options, true, dictionary);
}, },

View file

@ -631,7 +631,7 @@ var Color = Base.extend(new function() {
_serialize: function(options, dictionary) { _serialize: function(options, dictionary) {
var components = this.getComponents(); var components = this.getComponents();
return Base.serialize( return Base.serialize(
// We can ommit the type for gray and rgb: // We can omit the type for gray and rgb:
/^(gray|rgb)$/.test(this._type) /^(gray|rgb)$/.test(this._type)
? components ? components
: [this._type].concat(components), : [this._type].concat(components),

View file

@ -207,3 +207,37 @@ test('Color', function() {
}); });
testExportImportJSON(paper.project); testExportImportJSON(paper.project);
}); });
test('Color#importJSON()', function() {
var topLeft = [100, 100];
var bottomRight = [200, 200];
var path = new Path.Rectangle({
topLeft: topLeft,
bottomRight: bottomRight,
// Fill the path with a gradient of three color stops
// that runs between the two points we defined earlier:
fillColor: {
gradient: {
stops: ['yellow', 'red', 'blue']
},
origin: topLeft,
destination: bottomRight
}
});
var json = path.fillColor.exportJSON(),
id = path.fillColor.gradient._id,
color = new Color(),
str = '[["dictionary",{"#' + id + '":["Gradient",[[[1,1,0],0],[[1,0,0],0.5],[[0,0,1],1]],false]}],["Color","gradient",["#' + id + '"],[100,100],[200,200]]]';
equals(json, str);
equals(function() {
return color.importJSON(json) === color;
}, true);
equals(function() {
return color.equals(path.fillColor);
}, true);
});