/*
    store.js
    saving and loading Snap! projects
    written by Jens Mönig
    jens@moenig.org
    Copyright (C) 2022 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 .
    prerequisites:
    --------------
    needs morphic.js, xml.js, scenes.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, console,
WatcherMorph, Point, CustomBlockDefinition, Context, ReporterBlockMorph, Sound,
CommandBlockMorph, detect, CustomCommandBlockMorph, CustomReporterBlockMorph,
Color, List, newCanvas, Costume, Audio, IDE_Morph, ScriptsMorph, ArgLabelMorph,
BlockMorph, ArgMorph, InputSlotMorph, TemplateSlotMorph, CommandSlotMorph,
FunctionSlotMorph, MultiArgMorph, ColorSlotMorph, nop, CommentMorph, isNil,
localize, SVG_Costume, MorphicPreferences, Process, isSnapObject, Variable,
SyntaxElementMorph, BooleanSlotMorph, normalizeCanvas, contains, Scene,
Project*/
/*jshint esversion: 6*/
// Global stuff ////////////////////////////////////////////////////////
modules.store = '2022-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.root = {};
    this.isCollectingMedia = false;
    this.isExportingBlocksLibrary = false;
}
// XML_Serializer preferences settings:
XML_Serializer.prototype.idProperty = 'serializationID';
XML_Serializer.prototype.mediaIdProperty = 'serializationMediaID';
XML_Serializer.prototype.mediaDetectionProperty = 'isMedia';
XML_Serializer.prototype.version = 2; // increment on structural change
// XML_Serializer accessing:
XML_Serializer.prototype.serialize = function (object, forBlocksLibrary) {
    // public: answer an XML string representing the given object
    var xml;
    this.flush(); // in case an error occurred in an earlier attempt
    this.flushMedia();
    this.isExportingBlocksLibrary = forBlocksLibrary;
    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 (object instanceof Scene) {
        this.root = object;
    }
    if (this.isCollectingMedia && object[this.mediaDetectionProperty]) {
        this.addMedia(object, mediaID);
        return this.format(
            '',
            object[this.mediaIdProperty]
        );
    }
    if (object[this.idProperty]) {
        return this.format('', 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 = '';
    this.media.forEach(object => {
        var str = object.toXML(this).replace(
            '~',
            this.format('mediaID="@"', object[this.mediaIdProperty])
        );
        xml = xml + str;
    });
    return xml + '';
};
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
    this.contents.forEach(obj => delete obj[this.idProperty]);
    this.contents = [];
    this.root = {};
};
XML_Serializer.prototype.flushMedia = function () {
    // private - free all media objects and empty my media
    if (this.media instanceof Array) {
        this.media.forEach(obj => delete obj[this.mediaIdProperty]);
    }
    this.media = [];
    this.isExportingBlocksLibrary = false;
};
// 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 i = -1,
        values = arguments,
        value;
    return string.replace(/[@$%]([\d]+)?/g, (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 === '@' ?
                this.escape(value)
                    : spec === '$' ?
                        this.escape(value, true)
                            : value;
    });
};
// XML_Serializer loading:
XML_Serializer.prototype.load = function (xmlString) {
    // 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, assertVersion) {
    // answer an XML_Element representing the given XML String
    // optional assertVersion parameter for asserting a top-level
    // node to be consistent with the current serializer version
    var element = new XML_Element();
    element.parseString(xmlString);
    if (assertVersion) {
        if (+element.attributes.version > this.version) {
            throw 'Module uses newer version of Serializer';
        }
    }
    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! 7, https://snap.berkeley.edu';
SnapSerializer.prototype.thumbnailSize = new Point(160, 120);
SnapSerializer.prototype.watcherLabels = {
    xPosition: 'x position',
    yPosition: 'y position',
    direction: 'direction',
    getScale: 'size',
    reportShown: 'shown?',
    getTempo: 'tempo',
    getVolume: 'volume',
    getPan: 'balance',
    getPenDown: 'pen down?',
    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.scene = new Scene();
    this.objects = {};
    this.mediaDict = {};
};
// SnapSerializer saving:
XML_Serializer.prototype.mediaXML = function (name) {
    // under construction....
    var xml = '';
    this.media.forEach(object => {
        var str = object.toXML(this).replace(
            '~',
            this.format('mediaID="@"', object[this.mediaIdProperty])
        );
        xml = xml + str;
    });
    return xml + '';
};
// 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, remixID) {
    // 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,
        scenesModel = xmlNode.childNamed('scenes'),
        project = new Project();
    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.'
        );
    }
    if (scenesModel) {
        if (scenesModel.attributes.select) {
            project.sceneIdx = +scenesModel.attributes.select;
        }
        scenesModel.childrenNamed('scene').forEach(model => {
            ide.scene.captureGlobalSettings();
            project.scenes.add(this.loadScene(model));
            ide.scene.applyGlobalSettings();
        });
    } else {
        project.scenes.add(this.loadScene(xmlNode, remixID));
    }
    return project.initialize();
};
SnapSerializer.prototype.loadScene = function (xmlNode, remixID) {
    // private
    var scene = new Scene(),
        model,
        nameID;
    this.scene = scene;
    model = {scene: xmlNode };
    if (+xmlNode.attributes.version > this.version) {
        throw 'Project uses newer version of Serializer';
    }
    /* Project Info */
    this.objects = {};
    scene.name = model.scene.attributes.name;
    if (!scene.name) {
        nameID = 1;
        while (
            Object.prototype.hasOwnProperty.call(
                localStorage,
                '-snap-project-Untitled ' + nameID
            )
        ) {
            nameID += 1;
        }
        scene.name = 'Untitled ' + nameID;
    }
    scene.unifiedPalette = model.scene.attributes.palette === 'single';
    scene.showCategories = model.scene.attributes.categories !== 'false';
    scene.showPaletteButtons = model.scene.attributes.buttons !== 'false';
    scene.disableClickToRun = model.scene.attributes.clickrun === 'false';
    scene.penColorModel = model.scene.attributes.colormodel === 'hsl' ?
        'hsl' : 'hsv';
    model.notes = model.scene.childNamed('notes');
    if (model.notes) {
        scene.notes = model.notes.contents;
    }
    model.palette = model.scene.childNamed('palette');
    if (model.palette) {
        scene.customCategories = this.loadPalette(model.palette);
        SpriteMorph.prototype.customCategories = scene.customCategories;
    }
    model.globalVariables = model.scene.childNamed('variables');
    /* Stage */
    model.stage = model.scene.require('stage');
    scene.stage.remixID = remixID;
    if (Object.prototype.hasOwnProperty.call(
            model.stage.attributes,
            'id'
        )) {
        this.objects[model.stage.attributes.id] = scene.stage;
    }
    if (model.stage.attributes.name) {
        scene.stage.name = model.stage.attributes.name;
    }
    if (model.stage.attributes.color) {
        scene.stage.color = this.loadColor(model.stage.attributes.color);
        scene.stage.cachedColorDimensions = scene.stage.color[
            SpriteMorph.prototype.penColorModel
        ]();
    }
    if (model.stage.attributes.volume) {
        scene.stage.volume = +model.stage.attributes.volume;
    }
    if (model.stage.attributes.pan) {
        scene.stage.pan = +model.stage.attributes.pan;
    }
    if (model.stage.attributes.penlog) {
        scene.enablePenLogging =
            (model.stage.attributes.penlog === 'true');
    }
    model.pentrails = model.stage.childNamed('pentrails');
    if (model.pentrails) {
        scene.pentrails = new Image();
        scene.pentrails.onload = function () {
            if (scene.stage.trailsCanvas) { // work-around a bug in FF
                normalizeCanvas(scene.stage.trailsCanvas);
                var context = scene.stage.trailsCanvas.getContext('2d');
                context.drawImage(scene.pentrails, 0, 0);
                scene.stage.changed();
            }
        };
        scene.pentrails.src = model.pentrails.contents;
    }
    scene.stage.setTempo(model.stage.attributes.tempo);
    if (model.stage.attributes.width) {
        scene.stage.dimensions.x =
            Math.max(+model.stage.attributes.width, 240);
    }
    if (model.stage.attributes.height) {
        scene.stage.dimensions.y =
            Math.max(+model.stage.attributes.height, 180);
    }
    scene.stage.setExtent(scene.stage.dimensions);
    scene.useFlatLineEnds =
        model.stage.attributes.lines === 'flat';
    BooleanSlotMorph.prototype.isTernary =
        model.stage.attributes.ternary !== 'false';
    scene.enableHyperOps =
        model.stage.attributes.hyperops !== 'false';
    scene.stage.isThreadSafe =
        model.stage.attributes.threadsafe === 'true';
    scene.enableCodeMapping =
        model.stage.attributes.codify === 'true';
    scene.enableInheritance =
        model.stage.attributes.inheritance !== 'false';
    scene.enableSublistIDs =
        model.stage.attributes.sublistIDs === 'true';
    model.hiddenPrimitives = model.scene.childNamed('hidden');
    if (model.hiddenPrimitives) {
        model.hiddenPrimitives.contents.split(' ').forEach(
            sel => {
                var selector, migration;
                if (sel) {
                    migration = SpriteMorph.prototype.blockMigrations[sel];
                    selector = migration ? migration.selector : sel;
                    scene.hiddenPrimitives[selector] = true;
                }
            }
        );
    }
    model.codeHeaders = model.scene.childNamed('headers');
    if (model.codeHeaders) {
        model.codeHeaders.children.forEach(
            xml => scene.codeHeaders[xml.tag] = xml.contents
        );
    }
    model.codeMappings = model.scene.childNamed('code');
    if (model.codeMappings) {
        model.codeMappings.children.forEach(
            xml => scene.codeMappings[xml.tag] = xml.contents
        );
    }
    model.globalBlocks = model.scene.childNamed('blocks');
    if (model.globalBlocks) {
        this.loadCustomBlocks(scene.stage, model.globalBlocks, true);
        this.populateCustomBlocks(
            scene.stage,
            model.globalBlocks,
            true
        );
    }
    this.loadObject(scene.stage, model.stage);
    /* Sprites */
    model.sprites = model.stage.require('sprites');
    if (model.sprites.attributes.select) {
        scene.spriteIdx = +model.sprites.attributes.select;
    }
    scene.spritesDict[scene.stage.name] = scene.stage;
    model.sprites.childrenNamed('sprite').forEach(
        model => this.loadValue(model)
    );
    // restore inheritance and nesting associations
    this.scene.stage.children.forEach(sprite => {
        var exemplar, anchor;
        if (sprite.inheritanceInfo) { // only sprites can inherit
            exemplar = this.scene.spritesDict[
                sprite.inheritanceInfo.exemplar
            ];
            if (exemplar) {
                sprite.setExemplar(exemplar);
            }
            sprite.inheritedAttributes = sprite.inheritanceInfo.delegated || [];
            sprite.updatePropagationCache();
        }
        if (sprite.nestingInfo) { // only sprites may have nesting info
            anchor = this.scene.spritesDict[sprite.nestingInfo.anchor];
            if (anchor) {
                anchor.attachPart(sprite);
            }
            sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
        }
    });
    this.scene.stage.children.forEach(sprite => {
        var costume;
        if (sprite.nestingInfo) { // only sprites may have nesting info
            sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
            delete sprite.nestingInfo;
        }
        ['scripts', 'costumes', 'sounds'].forEach(att => {
            if (sprite.inheritsAttribute(att)) {
                sprite.refreshInheritedAttribute(att);
            }
        });
        if (sprite.inheritsAttribute('costumes')) {
            if (sprite.inheritsAttribute('costume #')) {
                costume = sprite.exemplar.costume;
            } else {
                costume = sprite.costumes.asArray()[
                    sprite.inheritanceInfo.costumeNumber - 1
                ];
            }
            if (costume) {
                if (costume.loaded) {
                    sprite.wearCostume(costume, true);
                } else {
                    costume.loaded = function () {
                        this.loaded = true;
                        sprite.wearCostume(costume, true);
                    };
                }
            }
        }
        delete sprite.inheritanceInfo;
    });
    /* Global Variables */
    if (model.globalVariables) {
        this.loadVariables(
            scene.globalVariables,
            model.globalVariables
        );
    }
    this.objects = {};
    /* Watchers */
    model.sprites.childrenNamed('watcher').forEach(model => {
        var watcher, color, target, hidden, extX, extY;
        color = this.loadColor(model.attributes.color);
        target = Object.prototype.hasOwnProperty.call(
            model.attributes,
            'scope'
        ) ? scene.spritesDict[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'
            )) {
            watcher = new WatcherMorph(
                model.attributes['var'],
                color,
                isNil(target) ? scene.globalVariables
                    : target.variables,
                model.attributes['var'],
                hidden
            );
        } else {
            watcher = new WatcherMorph(
                localize(this.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', true);
            watcher.setSliderMax(model.attributes.max || '100', true);
        }
        watcher.setPosition(
            scene.stage.topLeft().add(new Point(
                +model.attributes.x || 0,
                +model.attributes.y || 0
            )).multiplyBy(scene.stage.scale)
        );
        scene.stage.add(watcher);
        watcher.onNextStep = function () {this.currentValue = null; };
        // set watcher's contentsMorph's extent if it is showing a list and
        // its monitor dimensions are given
        if (watcher.currentValue instanceof List &&
                watcher.cellMorph.contentsMorph) {
            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.fixLayout();
        }
    });
    // clear sprites' inherited methods caches, if any
    this.scene.stage.children.forEach(
        sprite => sprite.inheritedMethodsCache = []
    );
    this.objects = {};
    return scene.initialize();
};
SnapSerializer.prototype.loadBlocks = function (xmlString, targetStage) {
    // public - answer a new dictionary of custom block definitions
    // represented by the given XML String
    var model = this.parse(xmlString);
    if (+model.attributes.version > this.version) {
        throw 'Module uses newer version of Serializer';
    }
    return this.loadBlocksModel(model, targetStage);
};
SnapSerializer.prototype.loadBlocksModel = function (model, targetStage) {
    // public - answer a new dictionary of custom block definitions
    // represented by the given already parsed XML Node
    var stage;
    this.scene = new Scene();
    this.scene.targetStage = targetStage; // for secondary block def look-up
    stage = this.scene.stage;
    model.palette = model.childNamed('palette');
    if (model.palette) {
        this.loadPalette(model.palette).forEach((value, key) =>
            SpriteMorph.prototype.customCategories.set(key, value)
        );
    }
    model.removeChild(model.palette);
    this.loadCustomBlocks(stage, model, true); // global
    this.populateCustomBlocks(stage, model, true); // global
    model.local = model.childNamed('local');
    if (model.local) {
        this.loadCustomBlocks(stage, model.local, false); // not global
        this.populateCustomBlocks( stage, model.local, false); // not global
    }
    this.objects = {};
    stage.globalBlocks.forEach(def => def.receiver = null);
    this.objects = {};
    this.scene = new Scene();
    this.mediaDict = {};
    return {
        global : stage.globalBlocks,
        local : stage.customBlocks
    };
};
SnapSerializer.prototype.loadSpritesModel = function (xmlNode, ide) {
    // public - import a set of sprites represented by an xml model
    // into the current scene of the ide
    var model = xmlNode,
        scene;
    this.scene = new Scene(ide.stage);
    scene = this.scene;
    scene.spritesDict[scene.stage.name] = scene.stage;
    model.childrenNamed('sprite').forEach(model => {
        var sprite  = new SpriteMorph(scene.globalVariables);
        if (model.attributes.id) {
            this.objects[model.attributes.id] = sprite;
        }
        if (model.attributes.name) {
            sprite.name = ide.newSpriteName(model.attributes.name);
            scene.spritesDict[sprite.name] = sprite;
        }
        if (model.attributes.color) {
            sprite.color = this.loadColor(model.attributes.color);
            sprite.cachedColorDimensions = sprite.color[sprite.penColorModel]();
        }
        if (model.attributes.pen) {
            sprite.penPoint = model.attributes.pen;
        }
        if (model.attributes.volume) {
            sprite.volume = +model.attributes.volume;
        }
        if (model.attributes.pan) {
            sprite.pan = +model.attributes.pan;
        }
        scene.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.gotoXY(+model.attributes.x || 0, +model.attributes.y || 0);
        this.loadObject(sprite, model);
        sprite.fixLayout();
        sprite.pauseGenericHatBlocks();
    });
    // restore inheritance and nesting associations
    scene.stage.children.forEach(sprite => {
        var exemplar, anchor;
        if (sprite.inheritanceInfo) { // only sprites can inherit
            exemplar = scene.spritesDict[
                sprite.inheritanceInfo.exemplar
            ];
            if (exemplar) {
                sprite.setExemplar(exemplar);
            }
        }
        if (sprite.nestingInfo) { // only sprites may have nesting info
            anchor = scene.spritesDict[sprite.nestingInfo.anchor];
            if (anchor) {
                anchor.attachPart(sprite);
            }
            sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
        }
    });
    scene.stage.children.forEach(sprite => {
        delete sprite.inheritanceInfo;
        if (sprite.nestingInfo) { // only sprites may have nesting info
            sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
            delete sprite.nestingInfo;
        }
    });
    this.objects = {};
    this.scene = new Scene();
    this.mediaDict = {};
    ide.stage.fixLayout();
    ide.stage.rerender();
    ide.createCorral();
    ide.fixLayout();
    ide.toggleAppMode(ide.isAppMode);
};
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 model = xmlNode;
    this.mediaDict = {};
    if (+model.attributes.version > this.version) {
        throw 'Module uses newer version of Serializer';
    }
    model.children.forEach(model => this.loadValue(model));
    return this.mediaDict;
};
SnapSerializer.prototype.loadObject = function (object, model) {
    // private
    var blocks = model.require('blocks'),
        dispatches = model.childNamed('dispatches'),
        node,
        costume;
    // load the instrument
    if (model.attributes.instrument) {
        object.instrument = +model.attributes.instrument;
    }
    this.loadInheritanceInfo(object, model);
    this.loadNestingInfo(object, model);
    // load the costume that's not in the wardrobe, if any
    node = model.childNamed('wear');
    if (node) {
        node = node.childNamed('costume') || node.childNamed('ref');
        if (!node) {
            console.log(object.name + ': missing costume to wear');
        } else {
            costume = this.loadValue(node, object);
            if (costume.loaded) {
                object.wearCostume(costume, true);
            } else {
                costume.loaded = function () {
                    this.loaded = true;
                    object.wearCostume(costume, true);
                };
            }
        }
    }
    // load costumes unless they're inherited
    if (!(object.inheritanceInfo &&
            (object.inheritanceInfo.delegated instanceof Array) &&
            contains(object.inheritanceInfo.delegated, 'costumes'))) {
        this.loadCostumes(object, model);
    }
    // load sounds unless they're inherited
    if (!(object.inheritanceInfo &&
            (object.inheritanceInfo.delegated instanceof Array) &&
            contains(object.inheritanceInfo.delegated, 'sounds'))) {
        this.loadSounds(object, model);
    }
    this.loadCustomBlocks(object, blocks);
    if (dispatches) {
        this.loadCustomBlocks(object, dispatches, false, true);
    }
    this.populateCustomBlocks(object, blocks);
    this.loadVariables(object.variables, model.require('variables'), object);
    // load scripts unless they're inherited
    if (!(object.inheritanceInfo &&
            (object.inheritanceInfo.delegated instanceof Array) &&
            contains(object.inheritanceInfo.delegated, 'scripts'))) {
        this.loadScripts(object, object.scripts, model.require('scripts'));
    }
    // note: the dispatches cache isn't cleared until after
    // *all* objects are loaded
};
SnapSerializer.prototype.loadInheritanceInfo = function (object, model) {
    // private
    var info = model.childNamed('inherit'),
        delegated;
    if (info) {
        object.inheritanceInfo = info.attributes;
        delegated = info.childNamed('list');
        if (delegated) {
            object.inheritanceInfo.delegated =
                this.loadValue(delegated).asArray();
        }
        object.inheritanceInfo.costumeNumber = model.attributes.costume;
    }
};
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',
            function () {
                console.log(object.name + ': missing required costumes list, ' +
                    'improvising...');
                return new XML_Element('list');
            }
        ));
        object.costumes.type = 'costume';
    }
    if (Object.prototype.hasOwnProperty.call(
            model.attributes,
            'costume'
        )) {
        costume = object.costumes.asArray()[model.attributes.costume - 1];
        if (costume) {
            if (costume.loaded) {
                object.wearCostume(costume, true);
            } else {
                costume.loaded = function () {
                    this.loaded = true;
                    object.wearCostume(costume, true);
                };
            }
        }
    }
};
SnapSerializer.prototype.loadSounds = function (object, model) {
    // private
    var sounds = model.childNamed('sounds');
    if (sounds) {
        // object.sounds = this.loadValue(sounds.require('list'));
        object.sounds = this.loadValue(sounds.require(
            'list',
            function () {
                console.log(object.name + ': missing required sounds list, ' +
                    'improvising...');
                return new XML_Element('list');
            }
        ));
        object.sounds.type = 'sound';
    }
};
SnapSerializer.prototype.loadVariables = function (varFrame, element, object) {
    // private
    element.children.forEach(child => {
        var v, value;
        if (child.tag !== 'variable') {
            return;
        }
        value = child.children[0];
        v = new Variable();
        v.isTransient = (child.attributes.transient === 'true');
        v.isHidden = (child.attributes.hidden === 'true');
        v.value = (v.isTransient || !value ) ? 0
                : this.loadValue(value, object);
        varFrame.vars[child.attributes.name] = v;
    });
};
SnapSerializer.prototype.loadCustomBlocks = function (
    object,
    element,
    isGlobal,
    isDispatch
) {
    // private
    element.children.forEach(child => {
        var definition, names, inputs, vars, header, code, trans, comment, i;
        if (child.tag !== 'block-definition') {
            return;
        }
        definition = new CustomBlockDefinition(
            child.attributes.s || '',
            object
        );
        definition.category = child.attributes.category || 'other';
        if (!SpriteMorph.prototype.allCategories().includes(
            definition.category
        )) {
            definition.category = 'other';
        }
        definition.type = child.attributes.type || 'command';
        definition.isHelper = (child.attributes.helper === 'true') || false;
        definition.isGlobal = (isGlobal === true);
        if (isDispatch) {
            object.inheritedMethodsCache.push(definition);
        } else {
            if (definition.isGlobal) {
                object.globalBlocks.push(definition);
            } else {
                object.customBlocks.push(definition);
            }
        }
        names = definition.parseSpec(definition.spec).filter(
            str => str.charAt(0) === '%' && str.length > 1
        ).map(str => str.substr(1));
        definition.names = names;
        inputs = child.childNamed('inputs');
        if (inputs) {
            i = -1;
            inputs.children.forEach(child => {
                var options = child.childNamed('options');
                if (child.tag !== 'input') {
                    return;
                }
                i += 1;
                definition.declarations.set(
                    names[i],
                    [
                        child.attributes.type,
                        contains(['%b', '%boolUE'], child.attributes.type) ?
                            (child.contents ? child.contents === 'true' : null)
                                : child.contents,
                        options ? options.contents : undefined,
                        child.attributes.readonly === 'true'
                    ]
                );
            });
        }
        vars = child.childNamed('variables');
        if (vars) {
            definition.variableNames = this.loadValue(
                vars.require('list')
            ).asArray();
        }
        header = child.childNamed('header');
        if (header) {
            definition.codeHeader = header.contents;
        }
        code = child.childNamed('code');
        if (code) {
            definition.codeMapping = code.contents;
        }
        trans = child.childNamed('translations');
        if (trans) {
            definition.updateTranslations(trans.contents);
        }
        comment = child.childNamed('comment');
        if (comment) {
            definition.comment = this.loadComment(comment);
        }
    });
};
SnapSerializer.prototype.populateCustomBlocks = function (
    object,
    element,
    isGlobal
) {
    // private
    element.children.forEach((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 ? this.loadScript(script, object) : null,
                null,
                object
            );
            definition.body.inputs = definition.names.slice(0);
        }
        scripts = child.childNamed('scripts');
        if (scripts) {
            definition.scripts = this.loadScriptsArray(scripts, object);
        }
        delete definition.names;
    });
};
SnapSerializer.prototype.loadScripts = function (object, scripts, model) {
    // private
    var scale = SyntaxElementMorph.prototype.scale;
    scripts.cachedTexture = IDE_Morph.prototype.scriptsPaneTexture;
    model.children.forEach(child => {
        var element;
        if (child.tag === 'script') {
            element = this.loadScript(child, object);
            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(comment => comment.align(element));
        } else if (child.tag === 'comment') {
            element = this.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, object) {
    // private - answer an array containting the model's scripts
    var scale = SyntaxElementMorph.prototype.scale,
        scripts = [];
    model.children.forEach(child => {
        var element;
        if (child.tag === 'script') {
            element = this.loadScript(child, object);
            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 = this.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.loadScriptModel = function (model, object) {
    // return a new script represented by the given xml model,
    // note: custom block definitions referenced here must be loaded before
    var script;
    this.scene = new Scene(object.parentThatIsA(StageMorph));
    script = this.loadScript(model, object);
    this.scene = new Scene();
    return script;
};
SnapSerializer.prototype.loadScript = function (model, object) {
    // private
    var topBlock, block, nextBlock;
    // Check whether we're importing a single script, not a script as part of a
    // whole scene
    if (!this.scene.stage) {
        this.scene.stage = object.parentThatIsA(StageMorph);
        this.scene.targetStage = this.scene.stage;
    }
    model.children.forEach(child => {
        nextBlock = this.loadBlock(child, false, object);
        if (!nextBlock) {
            return;
        }
        if (block) {
            if (block.nextBlock && (nextBlock instanceof CommandBlockMorph)) {
                block.nextBlock(nextBlock);
            } else {
                console.log(
                    'SNAP: expecting a command but getting a reporter:\n' +
                        '  ' + block.blockSpec + '\n' +
                        '  ' + nextBlock.blockSpec
                );
                return topBlock;
            }
        } 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, object) {
    // private
    var block, info, inputs, isGlobal, receiver, migration,
        migrationOffset = 0,
        migratoToVariadic = false;
    if (model.tag === 'block') {
        if (Object.prototype.hasOwnProperty.call(
                model.attributes,
                'var'
            )) {
            block = SpriteMorph.prototype.variableBlock(
                model.attributes['var']
            );
        } else {
            block = SpriteMorph.prototype.blockForSelector(model.attributes.s);
            migration = SpriteMorph.prototype.blockMigrations[
                model.attributes.s
            ];
            if (migration) {
                migrationOffset = migration.offset || 0;
                migratoToVariadic = migration.variadic;
            }
        }
    } else if (model.tag === 'custom-block') {
        isGlobal = model.attributes.scope ? false : true;
        receiver = isGlobal ? this.scene.stage : object;
        if (isGlobal) {
            info = detect(
                receiver.globalBlocks,
                block => block.blockSpec() === model.attributes.s
            );
            if (!info && this.scene.targetStage) { // importing block files
                info = detect(
                    this.scene.targetStage.globalBlocks,
                    block => block.blockSpec() === model.attributes.s
                );
            }
        } else {
            // lookup in inherited methods
            info = detect(
                receiver.customBlocks,
                block => block.blockSpec() === model.attributes.s
            ) || (
            	receiver.inheritedMethodsCache ?
                	detect(
                        receiver.inheritedMethodsCache,
                        block => block.blockSpec() === model.attributes.s
                	)
          		: null
          	);
        }
        if (!info || !contains(
        		// catch other forks' blocks
        		SpriteMorph.prototype.allCategories(), info.category
        )) {
            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((child, i) => {
        if (child.tag === 'variables') {
            this.loadVariables(block.variables, child, object);
        } else if (child.tag === 'comment') {
            block.comment = this.loadComment(child);
            block.comment.block = block;
        } else if (child.tag === 'receiver') {
            nop(); // ignore
        } else {
            if (migratoToVariadic) {
                // assume all formerly single inputs are now part of the first
                // one which is variadic and already expanded to hold them
                // example: migrate old infix addition to new variadic infix sum
                this.loadInput(
                    child,
                    inputs[0].inputs()[i],
                    inputs[0],
                    object
                );
            } else {
                this.loadInput(
                    child,
                    inputs[i + migrationOffset],
                    block,
                    object
                );
            }
        }
    });
    block.cachedInputs = null;
    return block;
};
SnapSerializer.prototype.obsoleteBlock = function (isReporter) {
    // private
    var block = isReporter ? new ReporterBlockMorph()
            : new CommandBlockMorph();
    block.selector = 'errorObsolete';
    block.color = new Color(200, 0, 20);
    block.setSpec('Undefined!');
    block.isDraggable = true;
    return block;
};
SnapSerializer.prototype.loadInput = function (model, input, block, object) {
    // private
    var inp, val;
    if (isNil(input)) {
        return;
    }
    if (model.tag === 'script') {
        inp = this.loadScript(model, object);
        if (inp) {
            if (block.selector === 'reifyReporter' ||
                    block.selector === 'reifyPredicate') {
                input.replaceInput(input.children[0], inp);
                input.fixLayout();
            } else {
                input.add(inp);
                input.fixLayout();
            }
        }
    } else if (model.tag === 'autolambda' && model.children[0]) {
        inp = this.loadBlock(model.children[0], true, object);
        if (inp) {
            input.replaceInput(input.children[0], inp);
            input.fixLayout();
        }
    } else if (model.tag === 'list') {
        while (input.inputs().length > 0 && input.removeInput) {
            input.removeInput();
        }
        model.children.forEach(item => {
            input.addInput();
            this.loadInput(
                item,
                input.children[input.children.length - 2],
                input,
                object
            );
        });
        input.fixLayout();
    } else if (model.tag === 'block' || model.tag === 'custom-block') {
        if (input.slotSpec === '%rcv') {
            // special case for migrating former SEND block inputs to
            // newer BROADCAST expansion slots for receivers
            // this can be removed once all SEND blocks have been
            // converted to v7
            input.replaceInput(
                input.inputs()[0],
                this.loadBlock(model, true, object)
            );
        } else {
            block.replaceInput(input, this.loadBlock(model, true, object));
        }
    } else if (model.tag === 'color') {
        input.setColor(this.loadColor(model.contents));
    } else {
        val = this.loadValue(model);
        if (!isNil(val) && !isNil(input) && input.setContents) {
            // checking whether "input" is nil should not
            // be necessary, but apparently is after retina support
            // was added.
            input.setContents(this.loadValue(model));
        }
    }
};
SnapSerializer.prototype.loadValue = function (model, object) {
    // private
    var v, i, lst, items, el, center, image, name, audio, option, bool, origin,
    	wish, def,
        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');
        if (option) {
            return [option.contents];
        }
        bool = model.childNamed('bool');
        if (bool) {
            return this.loadValue(bool);
        }
        wish = model.childNamed('wish');
        if (wish) {
            return this.loadValue(wish);
        }
        return model.contents;
    case 'bool':
        return model.contents === 'true';
    case 'list':
        if (model.attributes.hasOwnProperty('linked')) {
            if (model.attributes.struct === 'atomic') {
                v = Process.prototype.parseCSV(model.contents);
                v.becomeLinked();
                record();
                return v;
            }
            v = new List();
            v.isLinked = true;
            record();
            lst = v;
            items = model.childrenNamed('item');
            items.forEach((item, i) => {
                var value = item.children[0];
                if (!value) {
                    v.first = 0;
                } else {
                    v.first = this.loadValue(value, object);
                }
                var tail = model.childNamed('list') ||
                    model.childNamed('ref');
                if (tail) {
                    v.rest = this.loadValue(tail, object);
                } else {
                    if (i < (items.length - 1)) {
                        v.rest = new List();
                        v = v.rest;
                        v.isLinked = true;
                    }
                }
            });
            return lst;
        }
        if (model.attributes.struct === 'atomic') {
            v = Process.prototype.parseCSV(model.contents);
            record();
            return v;
        }
        v = new List();
        record();
        v.contents = model.childrenNamed('item').map(item => {
            var value = item.children[0];
            if (!value) {
                return 0;
            }
            return this.loadValue(value, object);
        });
        return v;
    case 'sprite':
        v  = new SpriteMorph(this.scene.globalVariables);
        if (model.attributes.id) {
            this.objects[model.attributes.id] = v;
        }
        if (model.attributes.name) {
            v.name = model.attributes.name;
            this.scene.spritesDict[model.attributes.name] = v;
        }
        if (model.attributes.idx) {
            v.idx = +model.attributes.idx;
        }
        if (model.attributes.color) {
            v.color = this.loadColor(model.attributes.color);
            v.cachedColorDimensions = v.color[v.penColorModel]();
        }
        if (model.attributes.pen) {
            v.penPoint = model.attributes.pen;
        }
        if (model.attributes.volume) {
            v.volume = +model.attributes.volume;
        }
        if (model.attributes.pan) {
            v.pan = +model.attributes.pan;
        }
        this.scene.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.gotoXY(+model.attributes.x || 0, +model.attributes.y || 0);
        this.loadObject(v, model);
        v.fixLayout();
        return v;
    case 'context':
        v = new Context(null);
        record();
        el = model.childNamed('origin');
        if (el) {
            el = el.childNamed('ref') || el.childNamed('sprite');
            if (el) {
                v.origin = this.loadValue(el);
            }
        }
        el = model.childNamed('receiver');
        if (el) {
            el = el.childNamed('ref') || el.childNamed('sprite');
            if (el) {
                v.receiver = this.loadValue(el);
            }
        }
        origin = v.origin || v.receiver || object; // for local blocks look up
        el = model.childNamed('script');
        if (el) {
            v.expression = this.loadScript(el, origin);
        } else {
            el = model.childNamed('block') ||
                model.childNamed('custom-block');
            if (el) {
                v.expression = this.loadBlock(el, null, origin);
            } else {
                el = model.childNamed('l');
                if (el) {
                    bool = el.childNamed('bool');
                    if (bool) {
                        v.expression = new BooleanSlotMorph(
                            this.loadValue(bool)
                        );
                    } else {
                        v.expression = new InputSlotMorph(el.contents);
                    }
                }
            }
        }
        if (v.expression instanceof BlockMorph) {
            // bind empty slots to implicit formal parameters
            i = 0;
            v.expression.allEmptySlots().forEach(slot => {
                i += 1;
                if (slot instanceof MultiArgMorph) {
                    slot.bindingID = ['arguments'];
                } else {
                    slot.bindingID = i;
                }
            });
            // and remember the number of detected empty slots
            v.emptySlots = i;
        }
        el = model.childNamed('inputs');
        if (el) {
            el.children.forEach(item => {
                if (item.tag === 'input') {
                    v.inputs.push(item.contents);
                }
            });
        }
        el = model.childNamed('variables');
        if (el) {
            this.loadVariables(v.variables, el, origin);
        }
        el = model.childNamed('context');
        if (el) {
            v.outerContext = this.loadValue(el, origin);
        }
        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),
                            true // nonRetina
                        ),
                        context = canvas.getContext('2d');
                    context.drawImage(image, 0, 0);
                    v.contents = canvas;
                    v.version = +new Date();
                    if (Object.prototype.hasOwnProperty.call(
                        model.attributes,
                        'embed'
                    )) {
                        v.embeddedData = model.attributes.embed;
                    }
                    if (typeof v.loaded === 'function') {
                        v.loaded();
                    } else {
                        v.loaded = true;
                    }
                };
            }
            image.src = model.attributes.image;
        }
        record();
        return v;
    case 'sound':
        audio = new Audio();
        v = new Sound(audio, model.attributes.name);
        audio.oncanplaythrough = () => v.loaded = true;
        audio.src = model.attributes.sound;
        if (Object.prototype.hasOwnProperty.call(
                model.attributes,
                'mediaID'
            )) {
            this.mediaDict[model.attributes.mediaID] = v;
        }
        record();
        return v;
    case 'wish':
    	def = new CustomBlockDefinition(model.attributes.s);
     	def.type = model.attributes.type;
      	def.category = model.attributes.category;
       	def.storedSemanticSpec = model.attributes.s;
        def.updateTranslations(model.contents);
        return def.blockInstance(true); // include translations
    }
    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.loadPalette = function (model) {
    // private
    var p = new Map();
    model.childrenNamed('category').forEach(node =>
        p.set(node.attributes.name, this.loadColor(node.attributes.color))
    );
    return p;
};
// SnapSerializer XML-representation of objects:
SnapSerializer.prototype.paletteToXML = function (aMap) {
    var xml;
    if (aMap.size === 0) {return ''; }
    xml = '';
    aMap.forEach((value, key) => {
        xml += this.format(
            '',
            key,
            value.r,
            value.g,
            value.b,
            value.a
        );
    });
    xml += '';
    return xml;
};
// Generics
Array.prototype.toXML = function (serializer) {
    return this.reduce(
        (xml, item) => xml + serializer.store(item),
        ''
    );
};
// Scenes & multi-scene projects
Project.prototype.toXML = function (serializer) {
    var thumbdata;
    // thumb data catch cross-origin tainting exception when using SVG costumes
    try {
        thumbdata = this.thumbnail.toDataURL('image/png');
    } catch (error) {
        thumbdata = null;
    }
    return serializer.format(
        '' +
            '$' +
            '$' +
            '%' +
            '',
        this.name || localize('Untitled'),
        serializer.app,
        serializer.version,
        this.notes || '',
        thumbdata,
        this.scenes.asArray().indexOf(
            this.currentScene) + 1,
        serializer.store(this.scenes.itemsArray())
    );
};
Scene.prototype.toXML = function (serializer) {
    var xml;
    function code(key) {
        var str = '';
        Object.keys(StageMorph.prototype[key]).forEach(
            selector => {
                str += (
                    '<' + selector + '>' +
                        XML_Element.prototype.escape(
                            StageMorph.prototype[key][selector]
                        ) +
                        '' + selector + '>'
                );
            }
        );
        return str;
    }
    serializer.scene = this; // keep the order of sprites in the corral
    xml = serializer.format(
        '' +
            '$' +
            '%' +
            '$' +
            '%' +
            '%' +
            '%' +
            '%' + // stage
            '%' +
            '',
        this.name || localize('Untitled'),
        this.unifiedPalette ? ' palette="single"' : '',
        this.unifiedPalette && !this.showCategories ?
            ' categories="false"' : '',
        this.unifiedPalette && !this.showPaletteButtons ?
            ' buttons="false"' : '',
        this.disableClickToRun ? ' clickrun="false"' : '',
        this.penColorModel === 'hsl' ? ' colormodel="hsl"' : '',
        this.notes || '',
        serializer.paletteToXML(this.customCategories),
        Object.keys(this.hiddenPrimitives).reduce(
                (a, b) => a + ' ' + b,
                ''
            ),
        code('codeHeaders'),
        code('codeMappings'),
        serializer.store(this.stage.globalBlocks),
        serializer.store(this.stage),
        serializer.store(this.globalVariables)
    );
    return xml;
};
// Sprites
StageMorph.prototype.toXML = function (serializer) {
    var costumeIdx = this.getCostumeIdx();
    this.removeAllClones();
    return serializer.format(
            '' +
            '$' +
            '%' + // current costume, if it's not in the wardrobe
            '%' +
            '%' +
            '%' +
            '%' +
            '%' +
            '%' +
            '',
        this.name,
        this.dimensions.x,
        this.dimensions.y,
        costumeIdx,
        this.color.r,
        this.color.g,
        this.color.b,
        this.color.a,
        this.getTempo(),
        this.isThreadSafe,
        this.enablePenLogging,
        this.instrument ?
                ' instrument="' + parseInt(this.instrument) + '" ' : '',
        this.volume,
        this.pan,
        SpriteMorph.prototype.useFlatLineEnds ? 'flat' : 'round',
        BooleanSlotMorph.prototype.isTernary,
        Process.prototype.enableHyperOps === true,
        this.enableCodeMapping,
        this.enableInheritance,
        this.enableSublistIDs,
        normalizeCanvas(this.trailsCanvas, true).toDataURL('image/png'),
        // current costume, if it's not in the wardrobe
        !costumeIdx && this.costume ?
            '' + serializer.store(this.costume) + ''
                : '',
        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.root.sprites.asArray().indexOf(
            serializer.root.currentSprite) + 1,
        serializer.store(this.children)
    );
};
SpriteMorph.prototype.toXML = function (serializer) {
    var idx = serializer.scene.sprites.asArray().indexOf(this) + 1,
        costumeIdx = this.getCostumeIdx(),
        noCostumes = this.inheritsAttribute('costumes'),
        noSounds = this.inheritsAttribute('sounds'),
        noScripts = this.inheritsAttribute('scripts');
    return serializer.format(
        '' +
            '%' + // inheritance info
            '%' + // nesting info
            '%' + // current costume
            (noCostumes ? '%' : '%') +
            (noSounds ? '%' : '%') +
            '%' +
            '%' +
            (this.exemplar ? '%' : '%') +
            (noScripts ? '%' : '%') +
            '',
        this.name,
        idx,
        this.xPosition(),
        this.yPosition(),
        this.heading,
        this.scale,
        this.volume,
        this.pan,
        this.rotationStyle,
        this.instrument ?
                ' instrument="' + parseInt(this.instrument) + '" ' : '',
        this.isDraggable,
        this.isVisible ? '' : ' hidden="true"',
        costumeIdx,
        this.color.r,
        this.color.g,
        this.color.b,
        this.color.a,
        this.penPoint,
        // inheritance info
        this.exemplar
            ? '' +
                    (this.inheritedAttributes.length ?
                        serializer.store(new List(this.inheritedAttributes))
                        : '') +
                    ''
            : '',
        // nesting info
        this.anchor
            ? ''
            : '',
        // current costume, if it's not in the wardrobe
        !costumeIdx && this.costume ?
            '' + serializer.store(this.costume) + ''
                : '',
        noCostumes ? '' : serializer.store(this.costumes, this.name + '_cst'),
        noSounds ? '' : serializer.store(this.sounds, this.name + '_snd'),
        !this.customBlocks ? '' : serializer.store(this.customBlocks),
        serializer.store(this.variables),
        this.exemplar ? serializer.store(this.inheritedMethods()) : '',
        noScripts ? '' : serializer.store(this.scripts)
    );
};
Costume.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
Costume.prototype.toXML = function (serializer) {
    return serializer.format(
        '',
        this.name,
        this.rotationCenter.x,
        this.rotationCenter.y,
        this instanceof SVG_Costume ? this.contents.src
                : normalizeCanvas(this.contents).toDataURL('image/png'),
        this.embeddedData ? serializer.format(' embed="@"', this.embeddedData)
            : ''
    );
};
Sound.prototype[XML_Serializer.prototype.mediaDetectionProperty] = true;
Sound.prototype.toXML = function (serializer) {
    return serializer.format(
        '',
        this.name,
        this.toDataURL()
    );
};
VariableFrame.prototype.toXML = function (serializer) {
    return Object.keys(this.vars).reduce((vars, v) => {
        var val = this.vars[v].value,
            transient = this.vars[v].isTransient,
            hidden = this.vars[v].isHidden,
            dta;
        if (transient || val === undefined || val === null) {
            dta = serializer.format(
                '',
                v
            );
        } else {
            dta = serializer.format(
                '%',
                v,
                typeof val === 'object' ?
                        (isSnapObject(val) ? ''
                                : serializer.store(val))
                                : typeof val === 'boolean' ?
                                        serializer.format(
                                            '$', val
                                        )
                                        : serializer.format('$', 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()
                ).divideBy(this.parent.scale)
            : this.topLeft();
    if (this.isTemporary()) {
        // do not save watchers on temporary variables
        return '';
    }
    return serializer.format(
        '',
        (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((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(
            '';
    return xml;
};
BlockMorph.prototype.toBlockXML = function (serializer) {
    return serializer.format(
        '%%',
        this.selector,
        serializer.store(this.inputs()),
        this.comment ? this.comment.toXML(serializer) : ''
    );
};
ReporterBlockMorph.prototype.toXML = function (serializer) {
    if (this.selector === 'reportGetVar') {
        if (!this.comment) {
            return serializer.format(
                '',
                this.blockSpec);
        } else {
            return serializer.format(
                '%',
                this.blockSpec,
                this.comment.toXML(serializer));
        }
    } else {
        return 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(
            '',
            position.x / scale,
            position.y / scale,
            this.toXML(serializer)
        );
    }
    return serializer.format('', this.toXML(serializer));
};
CustomCommandBlockMorph.prototype.toBlockXML = function (serializer) {
    var scope = this.isGlobal ? undefined : 'local';
    return serializer.format(
        '%%%',
        this.semanticSpec,
        this.isGlobal ?
                '' : serializer.format(' scope="@"', scope),
        serializer.store(this.inputs()),
        this.isGlobal &&
        	this.definition.variableNames.length &&
            !serializer.isExportingBlocksLibrary ?
                '' +
                    this.variables.toXML(serializer) +
                    ''
                        : '',
        this.comment ? this.comment.toXML(serializer) : ''
    );
};
CustomReporterBlockMorph.prototype.toBlockXML
    = CustomCommandBlockMorph.prototype.toBlockXML;
CustomBlockDefinition.prototype.toXML = function (serializer) {
    function encodeScripts(array) {
        return array.reduce((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(
        '' +
            '%' +
            (this.variableNames.length ? '%' : '@') +
            '' +
            '@' +
            '@' +
            '%%%' +
            '',
        this.spec,
        this.type,
        this.category || 'other',
        this.isHelper ? ' helper="true"' : '',
        this.comment ? this.comment.toXML(serializer) : '',
        (this.variableNames.length ?
                serializer.store(new List(this.variableNames)) : ''),
        this.codeHeader || '',
        this.codeMapping || '',
        this.translationsAsText(),
        Array.from(this.declarations.keys()).reduce((xml, decl) => {
            // to be refactored now that we've moved to ES6 Map:
                return xml + serializer.format(
                    '$%',
                    this.declarations.get(decl)[0],
                    this.declarations.get(decl)[3] ?
                            ' readonly="true"' : '',
                    this.declarations.get(decl)[1],
                    this.declarations.get(decl)[2] ?
                            serializer.format(
                                '@',
                                this.declarations.get(decl)[2]
                            ) : ''
                );
            }, ''),
        this.body ? serializer.store(this.body.expression) : '',
        this.scripts.length > 0 ?
                    '' + encodeScripts(this.scripts) + ''
                        : ''
    );
};
// Scripts - Inputs
ArgMorph.prototype.toXML = function () {
    return ''; // empty by default
};
BooleanSlotMorph.prototype.toXML = function () {
    return (typeof this.value === 'boolean') ?
            '' + this.value + ''
                    : '';
};
InputSlotMorph.prototype.toXML = function (serializer) {
	if (this.selectedBlock) {
 		return serializer.format(
        	'@',
            this.selectedBlock.semanticSpec,
         	this.selectedBlock instanceof CommandBlockMorph ? 'command'
          		: (this.selectedBlock.isPredicate ? 'predicate' : 'reporter'),
            this.selectedBlock.category,
            this.selectedBlock.storedTranslations
        );
 	}
    if (this.constant) {
        return serializer.format(
            '',
            this.constant
        );
    }
    return serializer.format('$', this.contents().text);
};
TemplateSlotMorph.prototype.toXML = function (serializer) {
    return serializer.format('$', this.contents());
};
CommandSlotMorph.prototype.toXML = function (serializer) {
    var block = this.nestedBlock();
    if (block instanceof BlockMorph) {
        if (block instanceof ReporterBlockMorph) {
            return serializer.format(
                '%',
                serializer.store(block)
            );
        }
        return serializer.store(block);
    }
    return '';
};
FunctionSlotMorph.prototype.toXML = CommandSlotMorph.prototype.toXML;
MultiArgMorph.prototype.toXML = function (serializer) {
    return serializer.format(
        '%
',
        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(
        '$,$,$,$',
        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, value;
    if (this.hasOnlyAtomicData() &&
            (!this.isLinked || !StageMorph.prototype.enableSublistIDs)) {
        // special case for a less cluttered format
        return serializer.format(
            '@
',
            this.asCSV()
        );
    }
    if (this.isLinked) {
        if (StageMorph.prototype.enableSublistIDs) {
            // recursively nest tails:
            xml = '';
            value = this.first;
            if (!isNil(value)) {
                xml += serializer.format(
                    '- %
 ',
                    typeof value === 'object' ?
                            (isSnapObject(value) ? ''
                                    : serializer.store(value, mediaContext))
                            : typeof value === 'boolean' ?
                                    serializer.format('$', value)
                                    : serializer.format('$', value)
                );
            }
            if (!isNil(this.rest)) {
                xml += serializer.store(this.rest, mediaContext);
            }
            return xml + '
';
        }
        // else shallow copy as array and mark as linked:
        return serializer.format(
            '%
',
            this.itemsArray().reduce((xml, item) => {
                return xml + serializer.format(
                    '- %
 ',
                    typeof item === 'object' ?
                            (isSnapObject(item) ? ''
                                    : serializer.store(item, mediaContext))
                            : typeof item === 'boolean' ?
                                    serializer.format('$', item)
                                    : serializer.format('$', item)
                );
            }, '')
        );
    }
    // dynamic array:
    return serializer.format(
        '%
',
        this.contents.reduce((xml, item) => {
            return xml + serializer.format(
                '- %
 ',
                typeof item === 'object' ?
                        (isSnapObject(item) ? ''
                                : serializer.store(item, mediaContext))
                        : typeof item === 'boolean' ?
                                serializer.format('$', item)
                                : serializer.format('$', item)
            );
        }, '')
    );
};
Context.prototype.toXML = function (serializer) {
    if (this.isContinuation) { // continuations are transient in Snap!
        return '';
    }
    return serializer.format(
        '%%' +
            '%%%%',
        this.inputs.reduce(
                (xml, input) => {
                    return xml + serializer.format('$', input);
                },
                ''
            ),
        this.variables ? serializer.store(this.variables) : '',
        this.expression ? serializer.store(this.expression) : '',
        this.receiver ? serializer.store(this.receiver) : '',
        this.receiver ? serializer.store(this.origin) : '',
        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(
            '%',
            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(
        '%',
        position.x / scale,
        position.y / scale,
        this.textWidth() / scale,
        this.isCollapsed,
        serializer.escape(this.text())
    );
};