From 56dd636f22c1e95c8599d0d69bb9738461acfd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 14 Feb 2016 17:16:40 +0100 Subject: [PATCH] JSON: Improve serialization and deserialization on objects other than Item. Closes #392 --- src/core/Base.js | 70 +++++++++++++++++++++++++++------------------ src/item/Project.js | 2 +- src/style/Color.js | 2 +- test/tests/JSON.js | 34 ++++++++++++++++++++++ 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/core/Base.js b/src/core/Base.js index 375d142d..2ce3e686 100644 --- a/src/core/Base.js +++ b/src/core/Base.js @@ -46,6 +46,17 @@ Base.inject(/** @lends Base# */{ 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. * @@ -332,9 +343,9 @@ Base.inject(/** @lends Base# */{ serialize: function(obj, options, compact, dictionary) { options = options || {}; - var root = !dictionary, + var isRoot = !dictionary, res; - if (root) { + if (isRoot) { options.formatter = new Formatter(options.precision); // Create a simple dictionary object that handles all the // 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, // e.g. for classes that do not support compact form. 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); + } } else if (Array.isArray(obj)) { res = []; for (var i = 0, l = obj.length; i < l; i++) res[i] = Base.serialize(obj[i], options, compact, 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)) { res = {}; var keys = Object.keys(obj); @@ -400,7 +411,7 @@ Base.inject(/** @lends Base# */{ } else { res = obj; } - return root && dictionary.length > 0 + return isRoot && dictionary.length > 0 ? [['dictionary', dictionary.definitions], res] : res; }, @@ -414,9 +425,11 @@ Base.inject(/** @lends Base# */{ * The passed json data is recoursively traversed and converted, leaves * first */ - deserialize: function(json, create, _data, _isDictionary) { + deserialize: function(json, create, _data, _setDictionary, _isRoot) { 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 // 'global' data across a deserialization. It's currently only used // to hold dictionary definitions. @@ -431,19 +444,18 @@ Base.inject(/** @lends Base# */{ isDictionary = type === 'dictionary'; // First see if this is perhaps a dictionary reference, and // if so return its definition instead. - if (json.length == 1 && /^#/.test(type)) + if (json.length == 1 && /^#/.test(type)) { return _data.dictionary[type]; + } type = Base.exports[type]; 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 - 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, - isDictionary)); + isDictionary, hasDictionary)); + } if (type) { // Create serialized type and pass collected arguments to // constructor(). @@ -452,7 +464,7 @@ Base.inject(/** @lends Base# */{ // creation. This is used in #importJSON() to pass // on insert = false to all items except layers. if (create) { - res = create(type, args, isRoot); + res = create(type, args, isFirst || _isRoot); } else { res = Base.create(type.prototype); type.apply(res, args); @@ -460,16 +472,16 @@ Base.inject(/** @lends Base# */{ } } else if (Base.isPlainObject(json)) { res = {}; - // See above why we have to set this before Base.deserialize() - if (_isDictionary) + // We need to set the dictionary object before further + // deserialization, because serialized symbols may contain + // references to serialized gradients + if (_setDictionary) _data.dictionary = res; for (var key in json) res[key] = Base.deserialize(json[key], create, _data); } - // Filter out deserialized dictionary. - return isRoot && json && json.length && json[0][0] === 'dictionary' - ? res[1] - : res; + // Filter out deserialized dictionary: + return hasDictionary ? res[1] : res; }, exportJSON: function(obj, options) { @@ -491,9 +503,11 @@ Base.inject(/** @lends Base# */{ && target.constructor === ctor, obj = useTarget ? target : Base.create(ctor.prototype), - // When reusing an object, try to initialize it - // through _initialize (Item), fall-back to _set. - init = useTarget ? obj._initialize || obj._set + // When reusing an object, try to (re)initialize it + // through _initialize (Item), fall-back to + // initialize (Color & co), then _set. + init = useTarget + ? obj._initialize || obj.initialize || obj._set : ctor; // NOTE: We don't set insert false for layers since we // want these to be created on the fly in the active diff --git a/src/item/Project.js b/src/item/Project.js index 67a2f552..564b293f 100644 --- a/src/item/Project.js +++ b/src/item/Project.js @@ -34,6 +34,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ _class: 'Project', _list: 'projects', _reference: 'project', + _compactSerialize: true, // Never include the class name for Project // 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 // project serialization later, but deserialization of a layers array // will always work. - // Pass true for compact, so 'Project' does not get added as the class return Base.serialize(this._children, options, true, dictionary); }, diff --git a/src/style/Color.js b/src/style/Color.js index 417df37d..971dfa93 100644 --- a/src/style/Color.js +++ b/src/style/Color.js @@ -631,7 +631,7 @@ var Color = Base.extend(new function() { _serialize: function(options, dictionary) { var components = this.getComponents(); 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) ? components : [this._type].concat(components), diff --git a/test/tests/JSON.js b/test/tests/JSON.js index 0945a826..1a424d9b 100644 --- a/test/tests/JSON.js +++ b/test/tests/JSON.js @@ -207,3 +207,37 @@ test('Color', function() { }); 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); +});