summaryrefslogtreecommitdiff
path: root/store.js
diff options
context:
space:
mode:
authorjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
committerjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
commit0b510366d230399a38f18a8db92dd396a0dad42e (patch)
treeffb841d64bca04799ebc470701b71e59e726017e /store.js
downloadsnap-0b510366d230399a38f18a8db92dd396a0dad42e.tar.gz
snap-0b510366d230399a38f18a8db92dd396a0dad42e.zip
Creation
Diffstat (limited to 'store.js')
-rw-r--r--store.js1641
1 files changed, 1641 insertions, 0 deletions
diff --git a/store.js b/store.js
new file mode 100644
index 0000000..efd9d3b
--- /dev/null
+++ b/store.js
@@ -0,0 +1,1641 @@
+/*
+
+ store.js
+
+ saving and loading Snap! projects
+
+ written by Jens Mšnig
+ jens@moenig.org
+
+ Copyright (C) 2013 by Jens Mšnig
+
+ This file is part of Snap!.
+
+ Snap! is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+ prerequisites:
+ --------------
+ needs morphic.js, xml.js, and most of Snap!'s other modules
+
+
+ hierarchy
+ ---------
+ the following tree lists all constructors hierarchically,
+ indentation indicating inheritance. Refer to this list to get a
+ contextual overview:
+
+ XML_Serializer
+ SnapSerializer
+
+
+ credits
+ -------
+ Nathan Dinsmore contributed to the design and implemented a first
+ working version of a complete XMLSerializer. I have taken much of the
+ overall design and many of the functions and methods in this file from
+ Nathan's fine original prototype.
+
+*/
+
+/*global modules, XML_Element, XML_Serializer, VariableFrame, StageMorph,
+SpriteMorph, WatcherMorph, Point, CustomBlockDefinition, Context,
+ReporterBlockMorph, CommandBlockMorph, HatBlockMorph, RingMorph, contains,
+detect, CustomCommandBlockMorph, CustomReporterBlockMorph, Color, List,
+newCanvas, Costume, Sound, Audio, IDE_Morph, ScriptsMorph, BlockMorph,
+ArgMorph, InputSlotMorph, TemplateSlotMorph, CommandSlotMorph,
+FunctionSlotMorph, MultiArgMorph, ColorSlotMorph, nop, CommentMorph, isNil,
+localize, sizeOf, ArgLabelMorph, SVG_Costume, MorphicPreferences*/
+
+// Global stuff ////////////////////////////////////////////////////////
+
+modules.store = '2013-March-14';
+
+
+// XML_Serializer ///////////////////////////////////////////////////////
+/*
+ I am an abstract protype for my heirs.
+
+ I manage object identities and keep track of circular data structures.
+ Objects are "touched" and a property named "serializationID" is added
+ to each, representing an index integer in the list, starting with 1.
+*/
+
+// XML_Serializer instance creation:
+
+function XML_Serializer() {
+ this.contents = [];
+ this.media = [];
+ this.isCollectingMedia = false;
+}
+
+// XML_Serializer preferences settings:
+
+XML_Serializer.prototype.idProperty = 'serializationID';
+XML_Serializer.prototype.mediaIdProperty = 'serializationMediaID';
+XML_Serializer.prototype.mediaDetectionProperty = 'isMedia';
+XML_Serializer.prototype.version = 1; // increment on structural change
+
+// XML_Serializer accessing:
+
+XML_Serializer.prototype.serialize = function (object) {
+ // public: answer an XML string representing the given object
+ var xml;
+ this.flush(); // in case an error occurred in an earlier attempt
+ this.flushMedia();
+ xml = this.store(object);
+ this.flush();
+ return xml;
+};
+
+XML_Serializer.prototype.store = function (object, mediaID) {
+ // private - mediaID is optional
+ if (isNil(object) || !object.toXML) {
+ // unsupported type, to be checked before calling store()
+ // when debugging, be sure to throw an error at this point
+ return '';
+ }
+ if (this.isCollectingMedia && object[this.mediaDetectionProperty]) {
+ this.addMedia(object, mediaID);
+ return this.format(
+ '<ref mediaID="@"></ref>',
+ object[this.mediaIdProperty]
+ );
+ }
+ if (object[this.idProperty]) {
+ return this.format('<ref id="@"></ref>', object[this.idProperty]);
+ }
+ this.add(object);
+ return object.toXML(this, mediaID).replace(
+ '~',
+ this.format('id="@"', object[this.idProperty])
+ );
+};
+
+XML_Serializer.prototype.mediaXML = function () {
+ // answer a project's collected media module as XML
+ var xml = '<media>',
+ myself = this;
+ this.media.forEach(function (object) {
+ var str = object.toXML(myself).replace(
+ '~',
+ myself.format('mediaID="@"', object[myself.mediaIdProperty])
+ );
+ xml = xml + str;
+ });
+ return xml + '</media>';
+};
+
+XML_Serializer.prototype.add = function (object) {
+ // private - mark the object with a serializationID property and add it
+ if (object[this.idProperty]) { // already present
+ return -1;
+ }
+ this.contents.push(object);
+ object[this.idProperty] = this.contents.length;
+ return this.contents.length;
+};
+
+XML_Serializer.prototype.addMedia = function (object, mediaID) {
+ // private - mark the object with a serializationMediaID property
+ // and add it to media
+ // if a mediaID is given, take it, otherwise generate one
+ if (object[this.mediaIdProperty]) { // already present
+ return -1;
+ }
+ this.media.push(object);
+ if (mediaID) {
+ object[this.mediaIdProperty] = mediaID + '_' + object.name;
+ } else {
+ object[this.mediaIdProperty] = this.media.length;
+ }
+ return this.media.length;
+};
+
+XML_Serializer.prototype.at = function (integer) {
+ // private
+ return this.contents[integer - 1];
+};
+
+XML_Serializer.prototype.flush = function () {
+ // private - free all objects and empty my contents
+ var myself = this;
+ this.contents.forEach(function (obj) {
+ delete obj[myself.idProperty];
+ });
+ this.contents = [];
+};
+
+XML_Serializer.prototype.flushMedia = function () {
+ // private - free all media objects and empty my media
+ var myself = this;
+ if (this.media instanceof Array) {
+ this.media.forEach(function (obj) {
+ delete obj[myself.mediaIdProperty];
+ });
+ }
+ this.media = [];
+};
+
+// XML_Serializer formatting:
+
+XML_Serializer.prototype.escape = XML_Element.prototype.escape;
+XML_Serializer.prototype.unescape = XML_Element.prototype.unescape;
+
+
+XML_Serializer.prototype.format = function (string) {
+ // private
+ var myself = this,
+ i = -1,
+ values = arguments,
+ value;
+
+ return string.replace(/[@$%]([\d]+)?/g, function (spec, index) {
+ index = parseInt(index, 10);
+ value = values[(isNaN(index) ? (i += 1) : index) + 1];
+ return spec === '@' ?
+ myself.escape(value)
+ : spec === '$' ?
+ myself.escape(value, true)
+ : value;
+ });
+};
+
+// XML_Serializer loading:
+
+XML_Serializer.prototype.load = function (xmlString) {
+ // public - answer a new object which is represented by the given
+ // XML string.
+ nop(xmlString);
+ throw new Error(
+ 'loading should be implemented in heir of XML_Serializer'
+ );
+};
+
+XML_Serializer.prototype.parse = function (xmlString) {
+ // private - answer an XML_Element representing the given XML String
+ var element = new XML_Element();
+ element.parseString(xmlString);
+ return element;
+};
+
+// SnapSerializer ////////////////////////////////////////////////////////////
+
+var SnapSerializer;
+
+// SnapSerializer inherits from XML_Serializer:
+
+SnapSerializer.prototype = new XML_Serializer();
+SnapSerializer.prototype.constructor = SnapSerializer;
+SnapSerializer.uber = XML_Serializer.prototype;
+
+// SnapSerializer constants:
+
+SnapSerializer.prototype.app = 'Snap! 4.0, http://snap.berkeley.edu';
+
+SnapSerializer.prototype.thumbnailSize = new Point(160, 120);
+
+SnapSerializer.prototype.watcherLabels = {
+ xPosition: 'x position',
+ yPosition: 'y position',
+ direction: 'direction',
+ getScale: 'size',
+ getLastAnswer: 'answer',
+ getTimer: 'timer',
+ getCostumeIdx: 'costume #'
+};
+
+// SnapSerializer instance creation:
+
+function SnapSerializer() {
+ this.init();
+}
+
+// SnapSerializer initialization:
+
+SnapSerializer.prototype.init = function () {
+ this.project = {};
+ this.objects = {};
+ this.mediaDict = {};
+};
+
+// SnapSerializer saving:
+
+XML_Serializer.prototype.mediaXML = function (name) {
+ // under construction....
+ var xml = '<media name="' +
+ (name || 'untitled') +
+ '" app="' + this.app +
+ '" version="' +
+ this.version +
+ '">',
+ myself = this;
+ this.media.forEach(function (object) {
+ var str = object.toXML(myself).replace(
+ '~',
+ myself.format('mediaID="@"', object[myself.mediaIdProperty])
+ );
+ xml = xml + str;
+ });
+ return xml + '</media>';
+};
+
+
+// SnapSerializer loading:
+
+SnapSerializer.prototype.load = function (xmlString) {
+ // public - answer a new Project represented by the given XML String
+ return this.loadProjectModel(this.parse(xmlString));
+};
+
+SnapSerializer.prototype.loadProjectModel = function (xmlNode) {
+ // public - answer a new Project represented by the given XML top node
+ var myself = this,
+ project = this.project = {sprites: {}},
+ model,
+ nameID;
+
+ model = {project: xmlNode };
+ if (+xmlNode.attributes.version > this.version) {
+ throw 'Project uses newer version of Serializer';
+ }
+
+ /* Project Info */
+
+ this.objects = {};
+ project.name = model.project.attributes.name;
+ if (!project.name) {
+ nameID = 1;
+ while (
+ localStorage.hasOwnProperty(
+ '-snap-project-Untitled ' + nameID
+ )
+ ) {
+ nameID += 1;
+ }
+ project.name = 'Untitled ' + nameID;
+ }
+ model.notes = model.project.childNamed('notes');
+ if (model.notes) {
+ project.notes = model.notes.contents;
+ }
+ model.globalVariables = model.project.childNamed('variables');
+ project.globalVariables = new VariableFrame();
+
+ /* Stage */
+
+ model.stage = model.project.require('stage');
+ StageMorph.prototype.frameRate = 0;
+ project.stage = new StageMorph(project.globalVariables);
+ if (model.stage.attributes.hasOwnProperty('id')) {
+ this.objects[model.stage.attributes.id] = project.stage;
+ }
+ if (model.stage.attributes.name) {
+ project.stage.name = model.stage.attributes.name;
+ }
+ if (model.stage.attributes.scheduled === 'true') {
+ project.stage.fps = 30;
+ StageMorph.prototype.frameRate = 30;
+ }
+ model.pentrails = model.stage.childNamed('pentrails');
+ if (model.pentrails) {
+ project.pentrails = new Image();
+ project.pentrails.onload = function () {
+ var context = project.stage.trailsCanvas.getContext('2d');
+ context.drawImage(project.pentrails, 0, 0);
+ project.stage.changed();
+ };
+ project.pentrails.src = model.pentrails.contents;
+ }
+ project.stage.setTempo(model.stage.attributes.tempo);
+ project.stage.setExtent(StageMorph.prototype.dimensions);
+ project.stage.isThreadSafe =
+ model.stage.attributes.threadsafe === 'true';
+
+ model.globalBlocks = model.project.childNamed('blocks');
+ if (model.globalBlocks) {
+ this.loadCustomBlocks(project.stage, model.globalBlocks, true);
+ this.populateCustomBlocks(
+ project.stage,
+ model.globalBlocks,
+ true
+ );
+ }
+ this.loadObject(project.stage, model.stage);
+
+ /* Sprites */
+
+ model.sprites = model.stage.require('sprites');
+ project.sprites[project.stage.name] = project.stage;
+
+ model.sprites.childrenNamed('sprite').forEach(function (model) {
+ myself.loadValue(model);
+ });
+
+ /* Global Variables */
+
+ if (model.globalVariables) {
+ this.loadVariables(
+ project.globalVariables,
+ model.globalVariables
+ );
+ }
+
+ /* Watchers */
+
+ model.sprites.childrenNamed('watcher').forEach(function (model) {
+ var watcher, color, target, hidden, extX, extY;
+
+ color = myself.loadColor(model.attributes.color);
+ target = model.attributes.hasOwnProperty('scope') ?
+ project.sprites[model.attributes.scope] : null;
+
+ // determine whether the watcher is hidden, slightly
+ // complicated to retain backward compatibility
+ // with former tag format: hidden="hidden"
+ // now it's: hidden="true"
+ hidden = model.attributes.hasOwnProperty('hidden')
+ && (model.attributes.hidden !== 'false');
+
+ if (model.attributes.hasOwnProperty('var')) {
+ watcher = new WatcherMorph(
+ model.attributes['var'],
+ color,
+ isNil(target) ? project.globalVariables
+ : target.variables,
+ model.attributes['var'],
+ hidden
+ );
+ } else {
+ watcher = new WatcherMorph(
+ localize(myself.watcherLabels[model.attributes.s]),
+ color,
+ target,
+ model.attributes.s,
+ hidden
+ );
+ }
+ watcher.setStyle(model.attributes.style || 'normal');
+ if (watcher.style === 'slider') {
+ watcher.setSliderMin(model.attributes.min || '1');
+ watcher.setSliderMax(model.attributes.max || '100');
+ }
+ watcher.setPosition(
+ project.stage.topLeft().add(new Point(
+ +model.attributes.x || 0,
+ +model.attributes.y || 0
+ ))
+ );
+ project.stage.add(watcher);
+ watcher.update();
+
+ // set watcher's contentsMorph's extent if it is showing a list and
+ // its monitor dimensions are given
+ if (watcher.currentValue instanceof List) {
+ extX = model.attributes.extX;
+ if (extX) {
+ watcher.cellMorph.contentsMorph.setWidth(+extX);
+ }
+ extY = model.attributes.extY;
+ if (extY) {
+ watcher.cellMorph.contentsMorph.setHeight(+extY);
+ }
+ // adjust my contentsMorph's handle position
+ watcher.cellMorph.contentsMorph.handle.drawNew();
+ }
+ });
+ this.objects = {};
+ return project;
+};
+
+SnapSerializer.prototype.loadBlocks = function (xmlString, targetStage) {
+ // public - answer a new Array of custom block definitions
+ // represented by the given XML String
+ var stage = new StageMorph(),
+ model;
+
+ this.project = {
+ stage: stage,
+ sprites: {},
+ targetStage: targetStage // for secondary custom block def look-up
+ };
+ model = this.parse(xmlString);
+ if (+model.attributes.version > this.version) {
+ throw 'Module uses newer version of Serializer';
+ }
+ this.loadCustomBlocks(stage, model, true);
+ this.populateCustomBlocks(
+ stage,
+ model,
+ true
+ );
+ this.objects = {};
+ stage.globalBlocks.forEach(function (def) {
+ def.receiver = null;
+ });
+ this.objects = {};
+ this.project = {};
+ this.mediaDict = {};
+ return stage.globalBlocks;
+};
+
+SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
+ // public - import a set of sprites represented by xmlString
+ // into the current project of the ide
+ var model, project, myself = this;
+
+ project = this.project = {
+ globalVariables: ide.globalVariables,
+ stage: ide.stage,
+ sprites: {}
+ };
+ project.sprites[project.stage.name] = project.stage;
+
+ model = this.parse(xmlString);
+ if (+model.attributes.version > this.version) {
+ throw 'Module uses newer version of Serializer';
+ }
+ model.childrenNamed('sprite').forEach(function (model) {
+ var sprite = new SpriteMorph(project.globalVariables);
+
+ if (model.attributes.id) {
+ myself.objects[model.attributes.id] = sprite;
+ }
+ if (model.attributes.name) {
+ sprite.name = model.attributes.name;
+ project.sprites[model.attributes.name] = sprite;
+ }
+ if (model.attributes.color) {
+ sprite.color = myself.loadColor(model.attributes.color);
+ }
+ if (model.attributes.pen) {
+ sprite.penPoint = model.attributes.pen;
+ }
+ project.stage.add(sprite);
+ ide.sprites.add(sprite);
+ sprite.scale = parseFloat(model.attributes.scale || '1');
+ sprite.rotationStyle = parseFloat(
+ model.attributes.rotation || '1'
+ );
+ sprite.isDraggable = model.attributes.draggable !== 'false';
+ sprite.isVisible = model.attributes.hidden !== 'true';
+ sprite.heading = parseFloat(model.attributes.heading) || 0;
+ sprite.drawNew();
+ sprite.gotoXY(+model.attributes.x || 0, +model.attributes.y || 0);
+ myself.loadObject(sprite, model);
+ });
+ this.objects = {};
+ this.project = {};
+ this.mediaDict = {};
+
+// ide.stage.drawNew();
+ ide.createCorral();
+ ide.fixLayout();
+};
+
+SnapSerializer.prototype.loadMedia = function (xmlString) {
+ // public - load the media represented by xmlString into memory
+ // to be referenced by a media-less project later
+ return this.loadMediaModel(this.parse(xmlString));
+};
+
+SnapSerializer.prototype.loadMediaModel = function (xmlNode) {
+ // public - load the media represented by xmlNode into memory
+ // to be referenced by a media-less project later
+ var myself = this,
+ model = xmlNode;
+ this.mediaDict = {};
+ if (+model.attributes.version > this.version) {
+ throw 'Module uses newer version of Serializer';
+ }
+ model.children.forEach(function (model) {
+ myself.loadValue(model);
+ });
+ return this.mediaDict;
+};
+
+SnapSerializer.prototype.loadObject = function (object, model) {
+ // private
+ var blocks = model.require('blocks');
+ this.loadCostumes(object, model);
+ this.loadSounds(object, model);
+ this.loadCustomBlocks(object, blocks);
+ this.populateCustomBlocks(object, blocks);
+ this.loadVariables(object.variables, model.require('variables'));
+ this.loadScripts(object.scripts, model.require('scripts'));
+};
+
+SnapSerializer.prototype.loadCostumes = function (object, model) {
+ // private
+ var costumes = model.childNamed('costumes'),
+ costume;
+ if (costumes) {
+ object.costumes = this.loadValue(costumes.require('list'));
+ }
+ if (model.attributes.hasOwnProperty('costume')) {
+ costume = object.costumes.asArray()[model.attributes.costume - 1];
+ if (costume) {
+ if (costume.loaded) {
+ object.wearCostume(costume);
+ } else {
+ costume.loaded = function () {
+ object.wearCostume(costume);
+ };
+ }
+ }
+ }
+};
+
+SnapSerializer.prototype.loadSounds = function (object, model) {
+ // private
+ var sounds = model.childNamed('sounds');
+ if (sounds) {
+ object.sounds = this.loadValue(sounds.require('list'));
+ }
+};
+
+SnapSerializer.prototype.loadVariables = function (varFrame, element) {
+ // private
+ var myself = this;
+
+ element.children.forEach(function (child) {
+ var value;
+ if (child.tag !== 'variable') {
+ return;
+ }
+ value = child.children[0];
+ varFrame.vars[child.attributes.name] = value ?
+ myself.loadValue(value) : 0;
+ });
+};
+
+SnapSerializer.prototype.loadCustomBlocks = function (
+ object,
+ element,
+ isGlobal
+) {
+ // private
+ element.children.forEach(function (child) {
+ var definition, names, inputs, i;
+ if (child.tag !== 'block-definition') {
+ return;
+ }
+ definition = new CustomBlockDefinition(
+ child.attributes.s || '',
+ object
+ );
+ definition.category = child.attributes.category || 'other';
+ definition.type = child.attributes.type || 'command';
+ definition.isGlobal = (isGlobal === true);
+ if (definition.isGlobal) {
+ object.globalBlocks.push(definition);
+ } else {
+ object.customBlocks.push(definition);
+ }
+
+ names = definition.parseSpec(definition.spec).filter(
+ function (str) {
+ return str.charAt(0) === '%';
+ }
+ ).map(function (str) {
+ return str.substr(1);
+ });
+
+ definition.names = names;
+ inputs = child.childNamed('inputs');
+ if (inputs) {
+ i = -1;
+ inputs.children.forEach(function (child) {
+ if (child.tag !== 'input') {
+ return;
+ }
+ definition.declarations[names[i += 1]]
+ = [child.attributes.type, child.contents];
+ });
+ }
+ });
+};
+
+SnapSerializer.prototype.populateCustomBlocks = function (
+ object,
+ element,
+ isGlobal
+) {
+ // private
+ var myself = this;
+ element.children.forEach(function (child, index) {
+ var definition, script, scripts;
+ if (child.tag !== 'block-definition') {
+ return;
+ }
+ definition = isGlobal ? object.globalBlocks[index]
+ : object.customBlocks[index];
+ script = child.childNamed('script');
+ if (script) {
+ definition.body = new Context(
+ null,
+ script ? myself.loadScript(script) : null,
+ null,
+ object
+ );
+ definition.body.inputs = definition.names.slice(0);
+ }
+ scripts = child.childNamed('scripts');
+ if (scripts) {
+ definition.scripts = myself.loadScriptsArray(scripts);
+ }
+
+ delete definition.names;
+ });
+};
+
+SnapSerializer.prototype.loadScripts = function (scripts, model) {
+ // private
+ var myself = this;
+ scripts.texture = 'scriptsPaneTexture.gif';
+ model.children.forEach(function (child) {
+ var element;
+ if (child.tag === 'script') {
+ element = myself.loadScript(child);
+ if (!element) {
+ return;
+ }
+ element.setPosition(new Point(
+ +child.attributes.x || 0,
+ +child.attributes.y || 0
+ ).add(scripts.topLeft()));
+ scripts.add(element);
+ element.fixBlockColor(null, true); // force zebra coloring
+ element.allComments().forEach(function (comment) {
+ comment.align(element);
+ });
+ } else if (child.tag === 'comment') {
+ element = myself.loadComment(child);
+ if (!element) {
+ return;
+ }
+ element.setPosition(new Point(
+ +child.attributes.x || 0,
+ +child.attributes.y || 0
+ ).add(scripts.topLeft()));
+ scripts.add(element);
+ }
+ });
+};
+
+SnapSerializer.prototype.loadScriptsArray = function (model) {
+ // private - answer an array containting the model's scripts
+ var myself = this,
+ scripts = [];
+ model.children.forEach(function (child) {
+ var element;
+ if (child.tag === 'script') {
+ element = myself.loadScript(child);
+ if (!element) {
+ return;
+ }
+ element.setPosition(new Point(
+ +child.attributes.x || 0,
+ +child.attributes.y || 0
+ ));
+ scripts.push(element);
+ element.fixBlockColor(null, true); // force zebra coloring
+ } else if (child.tag === 'comment') {
+ element = myself.loadComment(child);
+ if (!element) {
+ return;
+ }
+ element.setPosition(new Point(
+ +child.attributes.x || 0,
+ +child.attributes.y || 0
+ ));
+ scripts.push(element);
+ }
+ });
+ return scripts;
+};
+
+SnapSerializer.prototype.loadScript = function (model) {
+ // private
+ var topBlock, block, nextBlock,
+ myself = this;
+ model.children.forEach(function (child) {
+ nextBlock = myself.loadBlock(child);
+ if (!nextBlock) {
+ return;
+ }
+ if (block) {
+ block.nextBlock(nextBlock);
+ } else {
+ topBlock = nextBlock;
+ }
+ block = nextBlock;
+ });
+ return topBlock;
+};
+
+SnapSerializer.prototype.loadComment = function (model) {
+ // private
+ var comment = new CommentMorph(model.contents);
+ comment.isCollapsed = (model.attributes.collapsed === 'true');
+ comment.setTextWidth(+model.attributes.w);
+ return comment;
+};
+
+SnapSerializer.prototype.loadBlock = function (model, isReporter) {
+ // private
+ var block, info, inputs, isGlobal, rm, receiver;
+ if (model.tag === 'block') {
+ if (model.attributes.hasOwnProperty('var')) {
+ return SpriteMorph.prototype.variableBlock(
+ model.attributes['var']
+ );
+ }
+ block = SpriteMorph.prototype.blockForSelector(model.attributes.s);
+ } else if (model.tag === 'custom-block') {
+ isGlobal = model.attributes.scope ? false : true;
+ receiver = isGlobal ? this.project.stage
+ : this.project.sprites[model.attributes.scope];
+ rm = model.childNamed('receiver');
+ if (rm && rm.children[0]) {
+ receiver = this.loadValue(
+ model.childNamed('receiver').children[0]
+ );
+ }
+ if (!receiver) {
+ return this.obsoleteBlock(isReporter);
+ }
+ if (isGlobal) {
+ info = detect(receiver.globalBlocks, function (block) {
+ return block.blockSpec() === model.attributes.s;
+ });
+ if (!info && this.project.targetStage) { // importing block files
+ info = detect(
+ this.project.targetStage.globalBlocks,
+ function (block) {
+ return block.blockSpec() === model.attributes.s;
+ }
+ );
+ }
+ } else {
+ info = detect(receiver.customBlocks, function (block) {
+ return block.blockSpec() === model.attributes.s;
+ });
+ }
+ if (!info) {
+ return this.obsoleteBlock(isReporter);
+ }
+ block = info.type === 'command' ? new CustomCommandBlockMorph(
+ info,
+ false
+ ) : new CustomReporterBlockMorph(
+ info,
+ info.type === 'predicate',
+ false
+ );
+ }
+ if (block === null) {
+ block = this.obsoleteBlock(isReporter);
+ }
+ block.isDraggable = true;
+ inputs = block.inputs();
+ model.children.forEach(function (child, i) {
+ if (child.tag === 'comment') {
+ block.comment = this.loadComment(child);
+ block.comment.block = block;
+ } else if (child.tag === 'receiver') {
+ nop(); // ignore
+ } else {
+ this.loadInput(child, inputs[i], block);
+ }
+ }, this);
+ return block;
+};
+
+SnapSerializer.prototype.obsoleteBlock = function (isReporter) {
+ // private
+ var block = isReporter ? new ReporterBlockMorph()
+ : new CommandBlockMorph();
+ block.selector = 'nop';
+ block.color = new Color(200, 0, 20);
+ block.setSpec('Obsolete!');
+ block.isDraggable = true;
+ return block;
+};
+
+SnapSerializer.prototype.loadInput = function (model, input, block) {
+ // private
+ var inp, val, myself = this;
+ if (model.tag === 'script') {
+ inp = this.loadScript(model);
+ if (inp) {
+ input.add(inp);
+ input.fixLayout();
+ }
+ } else if (model.tag === 'autolambda' && model.children[0]) {
+ inp = this.loadBlock(model.children[0], true);
+ if (inp) {
+ input.silentReplaceInput(input.children[0], inp);
+ input.fixLayout();
+ }
+ } else if (model.tag === 'list') {
+ while (input.inputs().length > 0) {
+ input.removeInput();
+ }
+ model.children.forEach(function (item) {
+ input.addInput();
+ myself.loadInput(
+ item,
+ input.children[input.children.length - 2],
+ input
+ );
+ });
+ input.fixLayout();
+ } else if (model.tag === 'block' || model.tag === 'custom-block') {
+ block.silentReplaceInput(input, this.loadBlock(model, true));
+ } else if (model.tag === 'color') {
+ input.setColor(this.loadColor(model.contents));
+ } else {
+ val = this.loadValue(model);
+ if (val) {
+ input.setContents(this.loadValue(model));
+ }
+ }
+};
+
+SnapSerializer.prototype.loadValue = function (model) {
+ // private
+ var v, items, el, center, image, name, audio, option,
+ myself = this;
+
+ function record() {
+ if (model.attributes.hasOwnProperty('id')) {
+ myself.objects[model.attributes.id] = v;
+ }
+ if (model.attributes.hasOwnProperty('mediaID')) {
+ myself.mediaDict[model.attributes.mediaID] = v;
+ }
+ }
+ switch (model.tag) {
+ case 'ref':
+ if (model.attributes.hasOwnProperty('id')) {
+ return this.objects[model.attributes.id];
+ }
+ if (model.attributes.hasOwnProperty('mediaID')) {
+ return this.mediaDict[model.attributes.mediaID];
+ }
+ throw new Error('expecting a reference id');
+ case 'l':
+ option = model.childNamed('option');
+ return option ? [option.contents] : model.contents;
+ case 'bool':
+ return model.contents === 'true';
+ case 'list':
+ if (model.attributes.hasOwnProperty('linked')) {
+ items = model.childrenNamed('item');
+ if (items.length === 0) {
+ v = new List();
+ record();
+ return v;
+ }
+ items.forEach(function (item) {
+ var value = item.children[0];
+ if (v === undefined) {
+ v = new List();
+ record();
+ } else {
+ v = v.rest = new List();
+ }
+ v.isLinked = true;
+ if (!value) {
+ v.first = 0;
+ } else {
+ v.first = myself.loadValue(value);
+ }
+ });
+ return v;
+ }
+ v = new List();
+ record();
+ v.contents = model.childrenNamed('item').map(function (item) {
+ var value = item.children[0];
+ if (!value) {
+ return 0;
+ }
+ return myself.loadValue(value);
+ });
+ return v;
+ case 'sprite':
+ v = new SpriteMorph(myself.project.globalVariables);
+ if (model.attributes.id) {
+ myself.objects[model.attributes.id] = v;
+ }
+ if (model.attributes.name) {
+ v.name = model.attributes.name;
+ myself.project.sprites[model.attributes.name] = v;
+ }
+ if (model.attributes.idx) {
+ v.idx = +model.attributes.idx;
+ }
+ if (model.attributes.color) {
+ v.color = myself.loadColor(model.attributes.color);
+ }
+ if (model.attributes.pen) {
+ v.penPoint = model.attributes.pen;
+ }
+ myself.project.stage.add(v);
+ v.scale = parseFloat(model.attributes.scale || '1');
+ v.rotationStyle = parseFloat(
+ model.attributes.rotation || '1'
+ );
+ v.isDraggable = model.attributes.draggable !== 'false';
+ v.isVisible = model.attributes.hidden !== 'true';
+ v.heading = parseFloat(model.attributes.heading) || 0;
+ v.drawNew();
+ v.gotoXY(+model.attributes.x || 0, +model.attributes.y || 0);
+ myself.loadObject(v, model);
+ return v;
+ case 'context':
+ v = new Context(null);
+ record();
+ el = model.childNamed('script');
+ if (el) {
+ v.expression = this.loadScript(el);
+ } else {
+ el = model.childNamed('block') ||
+ model.childNamed('custom-block');
+ if (el) {
+ v.expression = this.loadBlock(el);
+ } else {
+ el = model.childNamed('l');
+ if (el) {
+ v.expression = new InputSlotMorph(el.contents);
+ }
+ }
+ }
+ el = model.childNamed('receiver');
+ if (el) {
+ el = el.childNamed('ref') || el.childNamed('sprite');
+ if (el) {
+ v.receiver = this.loadValue(el);
+ }
+ }
+ el = model.childNamed('inputs');
+ if (el) {
+ el.children.forEach(function (item) {
+ if (item.tag === 'input') {
+ v.inputs.push(item.contents);
+ }
+ });
+ }
+ el = model.childNamed('variables');
+ if (el) {
+ this.loadVariables(v.variables, el);
+ }
+ el = model.childNamed('context');
+ if (el) {
+ v.outerContext = this.loadValue(el);
+ }
+ return v;
+ case 'costume':
+ center = new Point();
+ if (model.attributes.hasOwnProperty('center-x')) {
+ center.x = parseFloat(model.attributes['center-x']);
+ }
+ if (model.attributes.hasOwnProperty('center-y')) {
+ center.y = parseFloat(model.attributes['center-y']);
+ }
+ if (model.attributes.hasOwnProperty('name')) {
+ name = model.attributes.name;
+ }
+ if (model.attributes.hasOwnProperty('image')) {
+ image = new Image();
+ if (model.attributes.image.indexOf('data:image/svg+xml') === 0
+ && !MorphicPreferences.rasterizeSVGs) {
+ v = new SVG_Costume(null, name, center);
+ image.onload = function () {
+ v.contents = image;
+ v.version = +new Date();
+ if (typeof v.loaded === 'function') {
+ v.loaded();
+ } else {
+ v.loaded = true;
+ }
+ };
+ } else {
+ v = new Costume(null, name, center);
+ image.onload = function () {
+ var canvas = newCanvas(
+ new Point(image.width, image.height)
+ ),
+ context = canvas.getContext('2d');
+ context.drawImage(image, 0, 0);
+ v.contents = canvas;
+ v.version = +new Date();
+ if (typeof v.loaded === 'function') {
+ v.loaded();
+ } else {
+ v.loaded = true;
+ }
+ };
+ }
+ image.src = model.attributes.image;
+ }
+ record();
+ return v;
+ case 'sound':
+ audio = new Audio();
+ audio.src = model.attributes.sound;
+ v = new Sound(audio, model.attributes.name);
+ if (model.attributes.hasOwnProperty('mediaID')) {
+ myself.mediaDict[model.attributes.mediaID] = v;
+ }
+ return v;
+ }
+ return undefined;
+};
+
+SnapSerializer.prototype.loadColor = function (colorString) {
+ // private
+ var c = (colorString || '').split(',');
+ return new Color(
+ parseFloat(c[0]),
+ parseFloat(c[1]),
+ parseFloat(c[2]),
+ parseFloat(c[3])
+ );
+};
+
+SnapSerializer.prototype.openProject = function (project, ide) {
+ var stage = ide.stage,
+ sprites = [],
+ sprite,
+ scripts;
+ if (!project || !project.stage) {
+ return;
+ }
+ ide.projectName = project.name;
+ ide.projectNotes = project.notes || '';
+ if (ide.globalVariables) {
+ ide.globalVariables = project.globalVariables;
+ }
+ if (stage) {
+ stage.destroy();
+ }
+ ide.add(ide.stage = project.stage);
+ sprites = ide.stage.children.filter(function (child) {
+ return child instanceof SpriteMorph;
+ });
+ sprites.sort(function (x, y) {
+ return x.idx - y.idx;
+ });
+ ide.sprites = new List(sprites);
+ sprite = sprites[0] || project.stage;
+ scripts = sprite.scripts;
+
+ if (sizeOf(this.mediaDict) > 0) {
+ ide.hasChangedMedia = false;
+ this.mediaDict = {};
+ } else {
+ ide.hasChangedMedia = true;
+ }
+ project.stage.drawNew();
+ ide.createCorral();
+ ide.selectSprite(sprite);
+ ide.fixLayout();
+ ide.world().keyboardReceiver = project.stage;
+};
+
+// SnapSerializer XML-representation of objects:
+
+// Generics
+
+Array.prototype.toXML = function (serializer) {
+ return this.reduce(function (xml, item) {
+ return xml + serializer.store(item);
+ }, '');
+};
+
+// Sprites
+
+StageMorph.prototype.toXML = function (serializer) {
+ var thumbnail = this.thumbnail(SnapSerializer.prototype.thumbnailSize),
+ thumbdata,
+ ide = this.parentThatIsA(IDE_Morph);
+
+ // catch cross-origin tainting exception when using SVG costumes
+ try {
+ thumbdata = thumbnail.toDataURL('image/png');
+ } catch (error) {
+ thumbdata = null;
+ }
+
+ this.removeAllClones();
+ return serializer.format(
+ '<project name="@" app="@" version="@">' +
+ '<notes>$</notes>' +
+ '<thumbnail>$</thumbnail>' +
+ '<stage name="@" costume="@" tempo="@" threadsafe="@" ' +
+ 'scheduled="@" ~>' +
+ '<pentrails>$</pentrails>' +
+ '<costumes>%</costumes>' +
+ '<sounds>%</sounds>' +
+ '<variables>%</variables>' +
+ '<blocks>%</blocks>' +
+ '<scripts>%</scripts><sprites>%</sprites>' +
+ '</stage>' +
+ '<blocks>%</blocks>' +
+ '<variables>%</variables>' +
+ '</project>',
+ (ide && ide.projectName) ? ide.projectName : 'Untitled',
+ serializer.app,
+ serializer.version,
+ (ide && ide.projectNotes) ? ide.projectNotes : '',
+ thumbdata,
+ this.name,
+ this.getCostumeIdx(),
+ this.getTempo(),
+ this.isThreadSafe,
+ StageMorph.prototype.frameRate !== 0,
+ this.trailsCanvas.toDataURL('image/png'),
+ serializer.store(this.costumes, this.name + '_cst'),
+ serializer.store(this.sounds, this.name + '_snd'),
+ serializer.store(this.variables),
+ serializer.store(this.customBlocks),
+ serializer.store(this.scripts),
+ serializer.store(this.children),
+ serializer.store(this.globalBlocks),
+ (ide && ide.globalVariables) ?
+ serializer.store(ide.globalVariables) : ''
+ );
+};
+
+SpriteMorph.prototype.toXML = function (serializer) {
+ var stage = this.parentThatIsA(StageMorph),
+ position = stage ?
+ this.center().subtract(stage.center()) : this.center(),
+ ide = stage ? stage.parentThatIsA(IDE_Morph) : null,
+ idx = ide ? ide.sprites.asArray().indexOf(this) + 1 : 0;
+
+ return serializer.format(
+ '<sprite name="@" idx="@" x="@" y="@"' +
+ ' heading="@"' +
+ ' scale="@"' +
+ ' rotation="@"' +
+ ' draggable="@"' +
+ '%' +
+ ' costume="@" color="@,@,@" pen="@" ~>' +
+ '<costumes>%</costumes>' +
+ '<sounds>%</sounds>' +
+ '<variables>%</variables>' +
+ '<blocks>%</blocks>' +
+ '<scripts>%</scripts>' +
+ '</sprite>',
+ this.name,
+ idx,
+ position.x,
+ -position.y,
+ this.heading,
+ this.scale,
+ this.rotationStyle,
+ this.isDraggable,
+ this.isVisible ? '' : ' hidden="true"',
+ this.getCostumeIdx(),
+ this.color.r,
+ this.color.g,
+ this.color.b,
+ this.penPoint,
+ serializer.store(this.costumes, this.name + '_cst'),
+ serializer.store(this.sounds, this.name + '_snd'),
+ serializer.store(this.variables),
+ !this.customBlocks ?
+ '' : serializer.store(this.customBlocks),
+ serializer.store(this.scripts)
+ );
+};
+
+Costume.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
+
+Costume.prototype.toXML = function (serializer) {
+ return serializer.format(
+ '<costume name="@" center-x="@" center-y="@" image="@" ~/>',
+ this.name,
+ this.rotationCenter.x,
+ this.rotationCenter.y,
+ this instanceof SVG_Costume ?
+ this.contents.src : this.contents.toDataURL('image/png')
+ );
+};
+
+Sound.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
+
+Sound.prototype.toXML = function (serializer) {
+ return serializer.format(
+ '<sound name="@" sound="@" ~/>',
+ this.name,
+ this.toDataURL()
+ );
+};
+
+VariableFrame.prototype.toXML = function (serializer) {
+ var myself = this;
+ return Object.keys(this.vars).reduce(function (vars, v) {
+ var val = myself.vars[v],
+ dta;
+ if (val === undefined || val === null) {
+ dta = serializer.format('<variable name="@"/>', v);
+ } else {
+ dta = serializer.format(
+ '<variable name="@">%</variable>',
+ v,
+ typeof val === 'object' ? serializer.store(val)
+ : typeof val === 'boolean' ?
+ serializer.format('<bool>$</bool>', val)
+ : serializer.format('<l>$</l>', val)
+ );
+ }
+ return vars + dta;
+ }, '');
+};
+
+
+
+// Watchers
+
+WatcherMorph.prototype.toXML = function (serializer) {
+ var isVar = this.target instanceof VariableFrame,
+ isList = this.currentValue instanceof List,
+ color = this.readoutColor,
+ position = this.parent ?
+ this.topLeft().subtract(this.parent.topLeft())
+ : this.topLeft();
+
+ return serializer.format(
+ '<watcher% % style="@"% x="@" y="@" color="@,@,@"%%/>',
+ (isVar && this.target.owner) || (!isVar && this.target) ?
+ serializer.format(' scope="@"',
+ isVar ? this.target.owner.name : this.target.name)
+ : '',
+ serializer.format(isVar ? 'var="@"' : 's="@"', this.getter),
+ this.style,
+ isVar && this.style === 'slider' ? serializer.format(
+ ' min="@" max="@"',
+ this.sliderMorph.start,
+ this.sliderMorph.stop
+ ) : '',
+ position.x,
+ position.y,
+ color.r,
+ color.g,
+ color.b,
+ !isList ? ''
+ : serializer.format(
+ ' extX="@" extY="@"',
+ this.cellMorph.contentsMorph.width(),
+ this.cellMorph.contentsMorph.height()
+ ),
+ this.isVisible ? '' : ' hidden="true"'
+ );
+};
+
+// Scripts
+
+ScriptsMorph.prototype.toXML = function (serializer) {
+ return this.children.reduce(function (xml, child) {
+ if (child instanceof BlockMorph) {
+ return xml + child.toScriptXML(serializer, true);
+ }
+ if (child instanceof CommentMorph && !child.block) { // unattached
+ return xml + child.toXML(serializer);
+ }
+ return xml;
+ }, '');
+};
+
+BlockMorph.prototype.toXML = BlockMorph.prototype.toScriptXML = function (
+ serializer,
+ savePosition
+) {
+ var position,
+ xml,
+ block = this;
+
+ // determine my position
+ if (this.parent) {
+ position = this.topLeft().subtract(this.parent.topLeft());
+ } else {
+ position = this.topLeft();
+ }
+
+ // save my position to xml
+ if (savePosition) {
+ xml = serializer.format(
+ '<script x="@" y="@">',
+ position.x,
+ position.y
+ );
+ } else {
+ xml = '<script>';
+ }
+
+ // recursively add my next blocks to xml
+ do {
+ xml += block.toBlockXML(serializer);
+ block = block.nextBlock();
+ } while (block);
+ xml += '</script>';
+ return xml;
+};
+
+BlockMorph.prototype.toBlockXML = function (serializer) {
+ return serializer.format(
+ '<block s="@">%%</block>',
+ this.selector,
+ serializer.store(this.inputs()),
+ this.comment ? this.comment.toXML(serializer) : ''
+ );
+};
+
+ReporterBlockMorph.prototype.toXML = function (serializer) {
+ return this.selector === 'reportGetVar' ? serializer.format(
+ '<block var="@"/>',
+ this.blockSpec
+ ) : this.toBlockXML(serializer);
+};
+
+ReporterBlockMorph.prototype.toScriptXML = function (
+ serializer,
+ savePosition
+) {
+ var position;
+
+ // determine my save-position
+ if (this.parent) {
+ position = this.topLeft().subtract(this.parent.topLeft());
+ } else {
+ position = this.topLeft();
+ }
+
+ if (savePosition) {
+ return serializer.format(
+ '<script x="@" y="@">%</script>',
+ position.x,
+ position.y,
+ this.toXML(serializer)
+ );
+ }
+ return serializer.format('<script>%</script>', this.toXML(serializer));
+};
+
+CustomCommandBlockMorph.prototype.toBlockXML = function (serializer) {
+ var scope = this.definition.isGlobal ? undefined
+ : this.definition.receiver.name;
+ return serializer.format(
+ '<custom-block s="@"%>%%%</custom-block>',
+ this.blockSpec,
+ this.definition.isGlobal ?
+ '' : serializer.format(' scope="@"', scope),
+ serializer.store(this.inputs()),
+ this.comment ? this.comment.toXML(serializer) : '',
+ scope && !this.definition.receiver[serializer.idProperty] ?
+ '<receiver>' +
+ serializer.store(this.definition.receiver) +
+ '</receiver>'
+ : ''
+ );
+};
+
+CustomReporterBlockMorph.prototype.toBlockXML
+ = CustomCommandBlockMorph.prototype.toBlockXML;
+
+CustomBlockDefinition.prototype.toXML = function (serializer) {
+ var myself = this;
+
+ function encodeScripts(array) {
+ return array.reduce(function (xml, element) {
+ if (element instanceof BlockMorph) {
+ return xml + element.toScriptXML(serializer, true);
+ }
+ if (element instanceof CommentMorph && !element.block) {
+ return xml + element.toXML(serializer);
+ }
+ return xml;
+ }, '');
+ }
+
+ return serializer.format(
+ '<block-definition s="@" type="@" category="@">' +
+ '<inputs>%</inputs>%%' +
+ '</block-definition>',
+ this.spec,
+ this.type,
+ this.category || 'other',
+ Object.keys(this.declarations).reduce(function (xml, decl) {
+ return xml + serializer.format(
+ '<input type="@">$</input>',
+ myself.declarations[decl][0],
+ myself.declarations[decl][1]
+ );
+ }, ''),
+ this.body ? serializer.store(this.body.expression) : '',
+ this.scripts.length > 0 ?
+ '<scripts>' + encodeScripts(this.scripts) + '</scripts>'
+ : ''
+ );
+};
+
+// Scripts - Inputs
+
+ArgMorph.prototype.toXML = function () {
+ return '<l/>'; // empty by default
+};
+
+InputSlotMorph.prototype.toXML = function (serializer) {
+ if (this.constant) {
+ return serializer.format(
+ '<l><option>$</option></l>',
+ this.constant
+ );
+ }
+ return serializer.format('<l>$</l>', this.contents().text);
+};
+
+TemplateSlotMorph.prototype.toXML = function (serializer) {
+ return serializer.format('<l>$</l>', this.contents());
+};
+
+CommandSlotMorph.prototype.toXML = function (serializer) {
+ var block = this.children[0];
+ if (block instanceof BlockMorph) {
+ if (block instanceof ReporterBlockMorph) {
+ return serializer.format(
+ '<autolambda>%</autolambda>',
+ serializer.store(block)
+ );
+ }
+ return serializer.store(block);
+ }
+ return '<script></script>';
+};
+
+FunctionSlotMorph.prototype.toXML = CommandSlotMorph.prototype.toXML;
+
+MultiArgMorph.prototype.toXML = function (serializer) {
+ return serializer.format(
+ '<list>%</list>',
+ serializer.store(this.inputs())
+ );
+};
+
+ArgLabelMorph.prototype.toXML = function (serializer) {
+ return serializer.format(
+ '%',
+ serializer.store(this.inputs()[0])
+ );
+};
+
+ColorSlotMorph.prototype.toXML = function (serializer) {
+ return serializer.format(
+ '<color>$,$,$,$</color>',
+ this.color.r,
+ this.color.g,
+ this.color.b,
+ this.color.a
+ );
+};
+
+// Values
+
+List.prototype.toXML = function (serializer, mediaContext) {
+ // mediaContext is an optional name-stub
+ // when collecting media into a separate module
+ var xml, item;
+ if (this.isLinked) {
+ xml = '<list linked="linked" ~>';
+ item = this;
+ do {
+ xml += serializer.format(
+ '<item>%</item>',
+ serializer.store(item.first)
+ );
+ item = item.rest;
+ } while (item !== undefined && (item !== null));
+ return xml + '</list>';
+ }
+ return serializer.format(
+ '<list ~>%</list>',
+ this.contents.reduce(function (xml, item) {
+ return xml + serializer.format(
+ '<item>%</item>',
+ typeof item === 'object' ?
+ serializer.store(item, mediaContext)
+ : typeof item === 'boolean' ?
+ serializer.format('<bool>$</bool>', item)
+ : serializer.format('<l>$</l>', item)
+ );
+ }, '')
+ );
+};
+
+
+Context.prototype.toXML = function (serializer) {
+ if (this.isContinuation) { // continuations are transient in Snap!
+ return '';
+ }
+ return serializer.format(
+ '<context% ~><inputs>%</inputs><variables>%</variables>' +
+ '%<receiver>%</receiver>%</context>',
+ this.isLambda ? ' lambda="lambda"' : '',
+ this.inputs.reduce(
+ function (xml, input) {
+ return xml + serializer.format('<input>$</input>', input);
+ },
+ ''
+ ),
+ this.variables ? serializer.store(this.variables) : '',
+ this.expression ? serializer.store(this.expression) : '',
+ this.receiver ? serializer.store(this.receiver) : '',
+ this.outerContext ? serializer.store(this.outerContext) : ''
+ );
+};
+
+// Comments
+
+CommentMorph.prototype.toXML = function (serializer) {
+ var position;
+
+ if (this.block) { // attached to a block
+ return serializer.format(
+ '<comment w="@" collapsed="@">%</comment>',
+ this.textWidth(),
+ this.isCollapsed,
+ serializer.escape(this.text())
+ );
+ }
+
+ // free-floating, determine my save-position
+ if (this.parent) {
+ position = this.topLeft().subtract(this.parent.topLeft());
+ } else {
+ position = this.topLeft();
+ }
+ return serializer.format(
+ '<comment x="@" y="@" w="@" collapsed="@">%</comment>',
+ position.x,
+ position.y,
+ this.textWidth(),
+ this.isCollapsed,
+ serializer.escape(this.text())
+ );
+};