text: /*
  
      store.js
  
      saving and loading Snap! projects
  
      written by Jens Mönig
      jens@moenig.org
  
      Copyright (C) 2015 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, 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,
  SyntaxElementMorph, Variable*/
  
  // Global stuff ////////////////////////////////////////////////////////
  
  modules.store = '2015-April-26';
  
  
  // 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);
  
          if (isNaN(index)) {
              i += 1;
              value = values[i + 1];
          } else {
              value = values[index + 1];
          }
          // original line of code - now frowned upon by JSLint:
          // 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',
      getTempo: 'tempo',
      getLastAnswer: 'answer',
      getLastMessage: 'message',
      getTimer: 'timer',
      getCostumeIdx: 'costume #',
      reportMouseX: 'mouse x',
      reportMouseY: 'mouse y',
      reportThreadCount: 'processes'
  };
  
  // 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, ide) {
      // public - answer a new Project represented by the given XML String
      return this.loadProjectModel(this.parse(xmlString), ide);
  };
  
  SnapSerializer.prototype.loadProjectModel = function (xmlNode, ide) {
      // public - answer a new Project represented by the given XML top node
      // show a warning if the origin apps differ
  
      var appInfo = xmlNode.attributes.app,
          app = appInfo ? appInfo.split(' ')[0] : null;
  
      if (ide && app && app !== this.app.split(' ')[0]) {
          ide.inform(
              app + ' Project',
              'This project has been created by a different app:\n\n' +
                  app +
                  '\n\nand may be incompatible or fail to load here.'
          );
      }
      return this.rawLoadProjectModel(xmlNode);
  };
  
  SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode) {
      // private
      var myself = this,
          project = {sprites: {}},
          model,
          nameID;
  
      this.project = project;
  
      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 (
              Object.prototype.hasOwnProperty.call(
                  localStorage,
                  '-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 (Object.prototype.hasOwnProperty.call(
              model.stage.attributes,
              '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);
      StageMorph.prototype.dimensions = new Point(480, 360);
      if (model.stage.attributes.width) {
          StageMorph.prototype.dimensions.x =
              Math.max(+model.stage.attributes.width, 480);
      }
      if (model.stage.attributes.height) {
          StageMorph.prototype.dimensions.y =
              Math.max(+model.stage.attributes.height, 180);
      }
      project.stage.setExtent(StageMorph.prototype.dimensions);
      SpriteMorph.prototype.useFlatLineEnds =
          model.stage.attributes.lines === 'flat';
      project.stage.isThreadSafe =
          model.stage.attributes.threadsafe === 'true';
      StageMorph.prototype.enableCodeMapping =
          model.stage.attributes.codify === 'true';
  
      model.hiddenPrimitives = model.project.childNamed('hidden');
      if (model.hiddenPrimitives) {
          model.hiddenPrimitives.contents.split(' ').forEach(
              function (sel) {
                  if (sel) {
                      StageMorph.prototype.hiddenPrimitives[sel] = true;
                  }
              }
          );
      }
  
      model.codeHeaders = model.project.childNamed('headers');
      if (model.codeHeaders) {
          model.codeHeaders.children.forEach(function (xml) {
              StageMorph.prototype.codeHeaders[xml.tag] = xml.contents;
          });
      }
  
      model.codeMappings = model.project.childNamed('code');
      if (model.codeMappings) {
          model.codeMappings.children.forEach(function (xml) {
              StageMorph.prototype.codeMappings[xml.tag] = xml.contents;
          });
      }
  
      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);
      });
  
      // restore nesting associations
      myself.project.stage.children.forEach(function (sprite) {
          var anchor;
          if (sprite.nestingInfo) { // only sprites may have nesting info
              anchor = myself.project.sprites[sprite.nestingInfo.anchor];
              if (anchor) {
                  anchor.attachPart(sprite);
              }
              sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
          }
      });
      myself.project.stage.children.forEach(function (sprite) {
          if (sprite.nestingInfo) { // only sprites may have nesting info
              sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
              delete sprite.nestingInfo;
          }
      });
  
      /* Global Variables */
  
      if (model.globalVariables) {
          this.loadVariables(
              project.globalVariables,
              model.globalVariables
          );
      }
  
      this.objects = {};
  
      /* Watchers */
  
      model.sprites.childrenNamed('watcher').forEach(function (model) {
          var watcher, color, target, hidden, extX, extY, vFrame;
  
          color = myself.loadColor(model.attributes.color);
          target = Object.prototype.hasOwnProperty.call(
              model.attributes,
              '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 = Object.prototype.hasOwnProperty.call(
              model.attributes,
              'hidden'
          ) && (model.attributes.hidden !== 'false');
  
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'var'
              )) {
              vFrame = isNil(target) ? project.globalVariables
                      : target.variables;
              if (Object.prototype.hasOwnProperty.call(
                      vFrame.vars,
                      model.attributes['var']
                  )) {
                  watcher = new WatcherMorph(
                      model.attributes['var'],
                      color,
                      vFrame,
                      model.attributes['var'],
                      hidden
                  );
              }
          } else {
              watcher = new WatcherMorph(
                  localize(myself.watcherLabels[model.attributes.s]),
                  color,
                  target,
                  model.attributes.s,
                  hidden
              );
          }
          if (watcher) {
              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.onNextStep = function () {this.currentValue = null; };
  
              // set watcher's contentsMorph's extent if it is showing a list
              // and if 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);
      });
  
      // restore nesting associations
      project.stage.children.forEach(function (sprite) {
          var anchor;
          if (sprite.nestingInfo) { // only sprites may have nesting info
              anchor = project.sprites[sprite.nestingInfo.anchor];
              if (anchor) {
                  anchor.attachPart(sprite);
              }
              sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
          }
      });
      project.stage.children.forEach(function (sprite) {
          if (sprite.nestingInfo) { // only sprites may have nesting info
              sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
              delete sprite.nestingInfo;
          }
      });
  
      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.loadNestingInfo(object, model);
      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.loadNestingInfo = function (object, model) {
      // private
      var info = model.childNamed('nest');
      if (info) {
          object.nestingInfo = info.attributes;
      }
  };
  
  SnapSerializer.prototype.loadCostumes = function (object, model) {
      // private
      var costumes = model.childNamed('costumes'),
          costume;
      if (costumes) {
          object.costumes = this.loadValue(costumes.require('list'));
      }
      if (Object.prototype.hasOwnProperty.call(
              model.attributes,
              'costume'
          )) {
          costume = object.costumes.asArray()[model.attributes.costume - 1];
          if (costume) {
              if (costume.loaded) {
                  object.wearCostume(costume);
              } else {
                  costume.loaded = function () {
                      object.wearCostume(costume);
                      this.loaded = true;
                  };
              }
          }
      }
  };
  
  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] = new Variable(value ?
                  myself.loadValue(value) : 0);
      });
  };
  
  SnapSerializer.prototype.loadCustomBlocks = function (
      object,
      element,
      isGlobal
  ) {
      // private
      var myself = this;
      element.children.forEach(function (child) {
          var definition, names, inputs, header, code, comment, 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) === '%' && str.length > 1;
              }
          ).map(function (str) {
              return str.substr(1);
          });
  
          definition.names = names;
          inputs = child.childNamed('inputs');
          if (inputs) {
              i = -1;
              inputs.children.forEach(function (child) {
                  var options = child.childNamed('options');
                  if (child.tag !== 'input') {
                      return;
                  }
                  i += 1;
                  definition.declarations[names[i]] = [
                      child.attributes.type,
                      child.contents,
                      options ? options.contents : undefined,
                      child.attributes.readonly === 'true'
                  ];
              });
          }
  
          header = child.childNamed('header');
          if (header) {
              definition.codeHeader = header.contents;
          }
  
          code = child.childNamed('code');
          if (code) {
              definition.codeMapping = code.contents;
          }
  
          comment = child.childNamed('comment');
          if (comment) {
              definition.comment = myself.loadComment(comment);
          }
      });
  };
  
  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,
          scale = SyntaxElementMorph.prototype.scale;
      scripts.cachedTexture = IDE_Morph.prototype.scriptsPaneTexture;
      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) * scale,
                  (+child.attributes.y || 0) * scale
              ).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) * scale,
                  (+child.attributes.y || 0) * scale
              ).add(scripts.topLeft()));
              scripts.add(element);
          }
      });
  };
  
  SnapSerializer.prototype.loadScriptsArray = function (model) {
      // private - answer an array containting the model's scripts
      var myself = this,
          scale = SyntaxElementMorph.prototype.scale,
          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) * scale,
                  (+child.attributes.y || 0) * scale
              ));
              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) * scale,
                  (+child.attributes.y || 0) * scale
              ));
              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),
          scale = SyntaxElementMorph.prototype.scale;
      comment.isCollapsed = (model.attributes.collapsed === 'true');
      comment.setTextWidth(+model.attributes.w * scale);
      return comment;
  };
  
  SnapSerializer.prototype.loadBlock = function (model, isReporter) {
      // private
      var block, info, inputs, isGlobal, rm, receiver;
      if (model.tag === 'block') {
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  '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) {
              if (!isGlobal) {
                  receiver = this.project.stage;
              } else {
                  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);
      block.cachedInputs = null;
      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 (!isNil(val) && input.setContents) {
              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 (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'id'
              )) {
              myself.objects[model.attributes.id] = v;
          }
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'mediaID'
              )) {
              myself.mediaDict[model.attributes.mediaID] = v;
          }
      }
      switch (model.tag) {
      case 'ref':
          if (Object.prototype.hasOwnProperty.call(model.attributes, 'id')) {
              return this.objects[model.attributes.id];
          }
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  '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);
          }
          if (v.outerContext && v.receiver &&
                  !v.outerContext.variables.parentFrame) {
              v.outerContext.variables.parentFrame = v.receiver.variables;
          }
          return v;
      case 'costume':
          center = new Point();
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'center-x'
              )) {
              center.x = parseFloat(model.attributes['center-x']);
          }
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'center-y'
              )) {
              center.y = parseFloat(model.attributes['center-y']);
          }
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  'name'
              )) {
              name = model.attributes.name;
          }
          if (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  '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 (Object.prototype.hasOwnProperty.call(
                  model.attributes,
                  '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;
      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(project.stage);
      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;
  
      if (sizeOf(this.mediaDict) > 0) {
          ide.hasChangedMedia = false;
          this.mediaDict = {};
      } else {
          ide.hasChangedMedia = true;
      }
      project.stage.drawNew();
      ide.createCorral();
      ide.selectSprite(sprite);
      ide.fixLayout();
  
      // force watchers to update
      //project.stage.watchers().forEach(function (watcher) {
      //  watcher.onNextStep = function () {this.currentValue = null;};
      //})
  
      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;
      }
  
      function code(key) {
          var str = '';
          Object.keys(StageMorph.prototype[key]).forEach(
              function (selector) {
                  str += (
                      '<' + selector + '>' +
                          XML_Element.prototype.escape(
                              StageMorph.prototype[key][selector]
                          ) +
                          '</' + selector + '>'
                  );
              }
          );
          return str;
      }
  
      this.removeAllClones();
      return serializer.format(
          '<project name="@" app="@" version="@">' +
              '<notes>$</notes>' +
              '<thumbnail>$</thumbnail>' +
              '<stage name="@" width="@" height="@" ' +
              'costume="@" tempo="@" threadsafe="@" ' +
              'lines="@" ' +
              'codify="@" ' +
              'scheduled="@" ~>' +
              '<pentrails>$</pentrails>' +
              '<costumes>%</costumes>' +
              '<sounds>%</sounds>' +
              '<variables>%</variables>' +
              '<blocks>%</blocks>' +
              '<scripts>%</scripts><sprites>%</sprites>' +
              '</stage>' +
              '<hidden>$</hidden>' +
              '<headers>%</headers>' +
              '<code>%</code>' +
              '<blocks>%</blocks>' +
              '<variables>%</variables>' +
              '</project>',
          (ide && ide.projectName) ? ide.projectName : localize('Untitled'),
          serializer.app,
          serializer.version,
          (ide && ide.projectNotes) ? ide.projectNotes : '',
          thumbdata,
          this.name,
          StageMorph.prototype.dimensions.x,
          StageMorph.prototype.dimensions.y,
          this.getCostumeIdx(),
          this.getTempo(),
          this.isThreadSafe,
          SpriteMorph.prototype.useFlatLineEnds ? 'flat' : 'round',
          this.enableCodeMapping,
          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),
          Object.keys(StageMorph.prototype.hiddenPrimitives).reduce(
                  function (a, b) {return a + ' ' + b; },
                  ''
              ),
          code('codeHeaders'),
          code('codeMappings'),
          serializer.store(this.globalBlocks),
          (ide && ide.globalVariables) ?
                      serializer.store(ide.globalVariables) : ''
      );
  };
  
  SpriteMorph.prototype.toXML = function (serializer) {
      var stage = this.parentThatIsA(StageMorph),
          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="@" ~>' +
              '%' + // nesting info
              '<costumes>%</costumes>' +
              '<sounds>%</sounds>' +
              '<variables>%</variables>' +
              '<blocks>%</blocks>' +
              '<scripts>%</scripts>' +
              '</sprite>',
          this.name,
          idx,
          this.xPosition(),
          this.yPosition(),
          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,
  
          // nesting info
          this.anchor
              ? '<nest anchor="' +
                      this.anchor.name +
                      '" synch="'
                      + this.rotatesWithAnchor
                      + (this.scale === this.nestingScale ? '' :
                              '"'
                              + ' scale="'
                              + this.nestingScale)
  
                      + '"/>'
              : '',
  
          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].value,
              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,
          scale = SyntaxElementMorph.prototype.scale,
          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 / scale,
              position.y / scale
          );
      } 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,
          scale = SyntaxElementMorph.prototype.scale;
  
      // 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 / scale,
              position.y / scale,
              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="@">' +
              '%' +
              '<header>@</header>' +
              '<code>@</code>' +
              '<inputs>%</inputs>%%' +
              '</block-definition>',
          this.spec,
          this.type,
          this.category || 'other',
          this.comment ? this.comment.toXML(serializer) : '',
          this.codeHeader || '',
          this.codeMapping || '',
          Object.keys(this.declarations).reduce(function (xml, decl) {
                  return xml + serializer.format(
                      '<input type="@"$>$%</input>',
                      myself.declarations[decl][0],
                      myself.declarations[decl][3] ?
                              ' readonly="true"' : '',
                      myself.declarations[decl][1],
                      myself.declarations[decl][2] ?
                              '<options>' + myself.declarations[decl][2] +
                                  '</options>'
                                  : ''
                  );
              }, ''),
          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.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,
          scale = SyntaxElementMorph.prototype.scale;
  
      if (this.block) { // attached to a block
          return serializer.format(
              '<comment w="@" collapsed="@">%</comment>',
              this.textWidth() / scale,
              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 / scale,
          position.y / scale,
          this.textWidth() / scale,
          this.isCollapsed,
          serializer.escape(this.text())
      );
  };
  