UNPKG

sb-edit

Version:

Import, edit, and export Scratch project files

851 lines (850 loc) 43 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); var Block_1 = require("../../Block"); var sb3 = __importStar(require("./interfaces")); var OpCode_1 = require("../../OpCode"); var BIS = sb3.BlockInputStatus; function toSb3(project, options) { // Serialize a project. Returns an object containing the text to be stored // in the caller's project.json output file. toSb3 should be bound or applied // so that 'this' refers to the Project object to be serialized. if (options === void 0) { options = {}; } var warn = function () { return undefined; }; if (options.warn) { warn = options.warn; } function serializeInputsToFields(inputs, fieldEntries) { // Serialize provided inputs into a "fields" mapping that can be stored // on a serialized block. // // Where the Scratch 3.0 term "input" refers to a slot that accepts blocks, // the term "field" is any user-interactive slot that cannot be obscured as // such. Fields are also the interactive element within any shadow block, // making their presence key to nearly all inputs. // // While inputs are fairly complex to serialize, fields are comparatively // simple. A field always contains only its selected or entered value, so // it's not concerned with a variety of structures like an input is. // The format for a field mapping looks something like this: // // { // VARIABLE: ["my variable", "theVariablesId"] // } // // Some fields take an ID; some don't. Here's another example: // // { // DISTANCETOMENU: ["_mouse_"] // } // // The important thing to remember with fields during serialization is that // they refer to slots that don't accept blocks: non-droppable menus, // primarily, but the value in a (non-compressed) shadow input too. var fields = {}; if (!fieldEntries) { return fields; } for (var _i = 0, _a = Object.keys(fieldEntries); _i < _a.length; _i++) { var key = _a[_i]; // TODO: remove type assertion var input = inputs[key]; // Fields are stored as a plain [value, id?] pair. var valueOrName = void 0; var id = void 0; switch (input.type) { case "variable": case "list": valueOrName = input.value.name; id = input.value.id; break; default: valueOrName = input.value; id = null; break; } fields[key] = [valueOrName, id]; } return fields; } function serializeInputShadow(value, options) { // Serialize the shadow block representing a provided value and type. // // To gather an understanding of what shadow blocks are used for, have // a look at serializeInputsToInputs; the gist is that they represent the // actual place in which you type a value or select an option from a // dropdown menu, and they can be obscured by having a block placed in // their place. // // A shadow block's only concerns are with revealing an interactive field // to the user. They exist so that inputs can let a non-shadow block stand // in place of that field. // // There are two forms in which a shadow block can be serialized. The rarer // structure, though the more basic one, is simply that of a typical block, // only with the "shadow: true" flag set. A shadow block contains a single // field; this field is stored the same as in any non-shadow block. Such a // serialized shadow block might look like this: // // { // opcode: "math_number", // shadow: true, // parent: "someBlockId", // fields: { // NUM: [50] // } // } // // The second form contains essentially the same data, but in a more // "compressed" form. In this form, the shadow block is stored as a simple // [type, value] pair, where the type is a constant representing the opcode // and field name in which the value should be placed when deserializing. // Because it's so much more concise than the non-compressed form, most // inputs are serialized in this way. The same input above in compressed // form would look like this: // // [4, 50] // // As described in serializeInputsToInputs, compressed shadow blocks are // stored inline with the input they correspond to, not as separate blocks // with IDs. // // Within this code, we use the primimtiveOrOpCode option to determine how // the shadow should be serialized. If it is a number, it's referring to a // "primitive", the term uses for shadows when they are in the compressed // form. If it's a string, it is a (shadow) block opcode, and should be // serialized in the expanded form. var _a; var blockData = options.blockData, parentId = options.parentId, primitiveOrOpCode = options.primitiveOrOpCode, shadowId = options.shadowId; var shadowValue = null; if (primitiveOrOpCode === BIS.BROADCAST_PRIMITIVE) { // Broadcast primitives, unlike all other primitives, expect two values: // the broadcast name and its ID. We just reuse the name for its ID; // after all, the name is the unique identifier sb-edit uses to refer to // the broadcast. shadowValue = [BIS.BROADCAST_PRIMITIVE, value, value]; } else if (primitiveOrOpCode === BIS.COLOR_PICKER_PRIMITIVE) { // Color primitive. Convert the {r, g, b} object into hex form. // TODO: remove type assertion and actually check if the value is an RGB literal var hex = function (k) { return (value || { r: 0, g: 0, b: 0 })[k].toString(16).padStart(2, "0"); }; shadowValue = [BIS.COLOR_PICKER_PRIMITIVE, "#" + hex("r") + hex("g") + hex("b")]; } else if (typeof primitiveOrOpCode === "number") { // Primitive shadow, can be stored in compressed form. shadowValue = [primitiveOrOpCode, String(value)]; } else { // Note: Only 1-field shadow blocks are supported. var shadowOpCode = primitiveOrOpCode; var fieldEntries = sb3.fieldTypeMap[shadowOpCode]; if (fieldEntries) { var fieldKey = Object.keys(fieldEntries)[0]; var fields = (_a = {}, _a[fieldKey] = [value], _a); blockData[shadowId] = { opcode: shadowOpCode, next: null, parent: parentId, fields: fields, inputs: {}, mutation: undefined, shadow: true, topLevel: false }; shadowValue = shadowId; } } return shadowValue; } function serializeInputsToInputs(inputs, options) { // Serialize provided inputs into an "inputs" mapping that can be stored // on a serialized block. // // In any Scratch block, the majority of behavior configuration is provided // by setting values for fields and inputs. In Scratch 3.0, the term // "input" refers to any slot in a block where a block may be placed. // (Fields are slots which don't accept blocks - non-droppable dropdown // menus, most of the time.) // // During serialization, there are three fundamental ways to describe an // input, each associated with a particular constant numeral. They're all // based on the concept of a "shadow block", which is a representation of // the non-block contents of an input. They're described in the following // list, and are detailed further in serializeInputShadow. // // (1) INPUT_SAME_BLOCK_SHADOW: // The input contains only a shadow block - no non-shadow. Take the // number input (612), for example. The element you type in to change // that value is the shadow block. Because there is nothing obscuring // the shadow block, the input is serialized as INPUT_SAME_BLOCK_SHADOW. // (2) INPUT_BLOCK_NO_SHADOW: // The input contains a non-shadow block - but no shadow. These are // relatively rare, since most inputs contain a shadow block (even when // obscured - see (3) below). Examples of inputs that don't are boolean // and substack slots, both prominent in blocks in the Control category. // (3) INPUT_DIFF_BLOCK_SHADOW: // The input contains a non-shadow block - and a shadow block, too. // This is the case when you've placed an ordinary Scratch block into the // input, obscuring the shadow block in its place. It's worth noting that // Scratch 3.0 remembers the type and value of an obscured shadow: if you // place a block ((4) * (13)) into that number input (612), and later // remove it, Scratch will reveal the (612) shadow again. // // There is one other way an input may be serialized, which is simply not // storing it at all. This occurs when an input contains neither a shadow // nor a non-shadow, as in INPUT_BLOCK_NO_SHADOW (2) but with the block // removed. (It's technically also valid to output null as the ID of the // block inside an INPUT_BLOCK_NO_SHADOW to represent such inputs. In this // code we choose not to store them at all.) // // When all is said and done, a block's inputs are stored as a mapping of // each input's ID to an array whose first item is one of the constants // described above, and whose following items depend on which constant. // For the block "go to x: (50) y: ((x position) of (dog))", that mapping // might look something like this: // // inputs: { // X: [INPUT_SAME_BLOCK_SHADOW, [4, 50]], // Y: [INPUT_DIFF_BLOCK_SHADOW, "some block id", [4, -50]] // } // // The arrays [4, 50] and [4, -50] represent the two shadow blocks these // inputs hold (the latter obscured by the "of" block). The value 4 is a // constant referring to a "math_number" - essentially, it is the type of // the shadow contained within that input. // // It's worth noting that some shadows are serialized as // actual blocks on the target's "blocks" dictionary, and referred to by // ID; the inputs in "switch to costume (item (random) of (costumes))" // follow this structure: // // inputs: { // COSTUME: [INPUT_DIFF_BLOCK_SHADOW, "id 1", "id 2"] // } // // ...where id 2 is the ID of the obscured shadow block. Specific details // on how shadow blocks are serialized and whether they're stored as // arrays or referenced by block ID is described in serializeInputShadow. // As far as the input mapping is concerned, all that matters is that the // two formats may be interchanged with one another. // // Also note that there are a couple blocks (specifically the variable and // list-contents getters) which are serialized altogether in the compressed // much the same as a compressed shadow, irregardless of the type of the // input they've been placed inside. This is because they're so common in // a project, and do not have any inputs of their own - only a field to // identify which variable or list the block corresponds to. The input // mapping for the block "set x to (spawn x)" would look something like // this: // // inputs: { // X: [INPUT_DIFF_BLOCK_SHADOW, [12, "spawn x", "someId"], [4, 0]] // } // // ...where someId is the ID of the variable, and [4, 0] is the obscured // shadow block, as usual. var block = options.block, blockData = options.blockData, initialBroadcastName = options.initialBroadcastName, customBlockDataMap = options.customBlockDataMap, initialValues = options.initialValues, inputEntries = options.inputEntries, target = options.target; var resultInputs = {}; for (var _i = 0, _a = Object.entries(inputEntries); _i < _a.length; _i++) { var _b = _a[_i], key = _b[0], entry = _b[1]; var input = inputs[key]; if (entry === sb3.BooleanOrSubstackInputStatus) { var blockId = null; if (input) { var options_1 = { target: target, blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap, parent: block }; switch (input.type) { case "blocks": if (input.value !== null) blockId = serializeBlockStack(input.value, options_1); break; case "block": blockId = serializeBlock(input.value, options_1); break; } } if (blockId) { resultInputs[key] = [BIS.INPUT_BLOCK_NO_SHADOW, blockId]; } } else { var valueForShadow = void 0; if (input.type === "block") { valueForShadow = initialValues[key]; // Special-case some input opcodes for more realistic initial values. switch (entry) { case OpCode_1.OpCode.looks_costume: if (target.costumes[0]) { valueForShadow = target.costumes[0].name; } break; case OpCode_1.OpCode.sound_sounds_menu: if (target.sounds[0]) { valueForShadow = target.sounds[0].name; } break; case OpCode_1.OpCode.event_broadcast_menu: valueForShadow = initialBroadcastName; break; } } else { valueForShadow = input.value; } var shadowValue = serializeInputShadow(valueForShadow, { blockData: blockData, parentId: block.id, shadowId: block.id + "-" + key, primitiveOrOpCode: entry }); if (input.type === "block") { var obscuringBlockValue = void 0; switch (input.value.opcode) { case OpCode_1.OpCode.data_variable: { var _c = input.value.inputs.VARIABLE.value, variableId = _c.id, variableName = _c.name; obscuringBlockValue = [BIS.VAR_PRIMITIVE, variableName, variableId]; break; } case OpCode_1.OpCode.data_listcontents: { var _d = input.value.inputs.LIST.value, listId = _d.id, listName = _d.name; obscuringBlockValue = [BIS.LIST_PRIMITIVE, listName, listId]; break; } default: { obscuringBlockValue = serializeBlock(input.value, { blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap, parent: block, target: target }); break; } } if (shadowValue) { resultInputs[key] = [BIS.INPUT_DIFF_BLOCK_SHADOW, obscuringBlockValue, shadowValue]; } else { resultInputs[key] = [BIS.INPUT_BLOCK_NO_SHADOW, obscuringBlockValue]; } } else { resultInputs[key] = [BIS.INPUT_SAME_BLOCK_SHADOW, shadowValue]; } } } return resultInputs; } function serializeInputs(block, options) { // Serialize a block's inputs, returning the data which should be stored on // the serialized block, as well as any associated blockData. // // This function looks more intimidating than it ought to; most of the meat // here is related to serializing specific blocks whose resultant data must // be generated differently than other blocks. (Custom blocks are related // the main ones to blame.) // // serializeInputs is in charge of converting the inputs on the provided // block into the structures that Scratch 3.0 expects. There are (usually) // two mappings into which inputs are stored: fields and inputs. Specific // details on how these are serialized is discussed in their corresponding // functions (which serializeInputs defers to for most blocks), but the // gist is: // // * Fields store actual data, while inputs refer to shadow blocks. // (Each shadow block contains a field for storing the value of that // input, though often they are serialized as a "compressed" form that // doesn't explicitly label that field. See serializeInputsToInputs.) // * Reporter blocks can be placed only into inputs - not fields. // (In actuality, the input is not replaced by a block; rather, the way // it is stored is changed to refer to the ID of the placed block, and // the shadow block contianing the field value is maintained, "obscured" // but able to be recovered if the obscuring block is moved elsewhere.) // // Blocks may also have a "mutation" field. This is an XML attribute // mapping containing data specific to a particular instance of a block // that wouldn't fit on the block's input and field mappings. Specific // details may vary greatly based on the opcode. var blockData = options.blockData, target = options.target, initialBroadcastName = options.initialBroadcastName, customBlockDataMap = options.customBlockDataMap; var fields = serializeInputsToFields(block.inputs, sb3.fieldTypeMap[block.opcode]); var inputs = {}; var mutation; if (block.isKnownBlock()) { switch (block.opcode) { case OpCode_1.OpCode.procedures_definition: { var prototypeId = block.id + "-prototype"; var _a = customBlockDataMap[block.inputs.PROCCODE.value], args = _a.args, warp = _a.warp; var prototypeInputs = {}; for (var _i = 0, args_1 = args; _i < args_1.length; _i++) { var arg = args_1[_i]; var shadowId = arg.id + "-prototype-shadow"; blockData[shadowId] = { opcode: { boolean: OpCode_1.OpCode.argument_reporter_boolean, numberOrString: OpCode_1.OpCode.argument_reporter_string_number }[arg.type], next: null, parent: prototypeId, inputs: {}, fields: { VALUE: [arg.name] }, mutation: undefined, shadow: true, topLevel: false }; prototypeInputs[arg.id] = [BIS.INPUT_SAME_BLOCK_SHADOW, shadowId]; } blockData[prototypeId] = { opcode: OpCode_1.OpCode.procedures_prototype, next: null, parent: block.id, inputs: prototypeInputs, fields: {}, shadow: true, topLevel: false, mutation: { tagName: "mutation", children: [], proccode: block.inputs.PROCCODE.value, argumentids: JSON.stringify(args.map(function (arg) { return arg.id; })), argumentnames: JSON.stringify(args.map(function (arg) { return arg.name; })), argumentdefaults: JSON.stringify(args.map(function (arg) { return arg.default; })), warp: JSON.stringify(warp) } }; inputs.custom_block = [BIS.INPUT_SAME_BLOCK_SHADOW, prototypeId]; break; } case OpCode_1.OpCode.procedures_call: { var proccode = block.inputs.PROCCODE.value; var customBlockData = customBlockDataMap[proccode]; if (!customBlockData) { warn("Missing custom block prototype for proccode ".concat(proccode, " (").concat(block.id, " in ").concat(target.name, "); skipping this block")); return null; } var args = customBlockData.args, warp = customBlockData.warp; mutation = { tagName: "mutation", children: [], proccode: proccode, argumentids: JSON.stringify(args.map(function (arg) { return arg.id; })), warp: JSON.stringify(warp) }; var inputEntries = {}; var constructedInputs = {}; var initialValues = {}; for (var i = 0; i < args.length; i++) { var _b = args[i], type = _b.type, id = _b.id; switch (type) { case "boolean": inputEntries[id] = sb3.BooleanOrSubstackInputStatus; // A boolean input's initialValues entry will never be // referenced (because empty boolean inputs don't contain // shadow blocks), so there's no need to set it. break; case "numberOrString": inputEntries[id] = BIS.TEXT_PRIMITIVE; initialValues[id] = ""; break; } constructedInputs[id] = block.inputs.INPUTS.value[i]; } inputs = serializeInputsToInputs(constructedInputs, { target: target, blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap, block: block, initialValues: initialValues, inputEntries: inputEntries }); break; } default: { var inputEntries = sb3.inputPrimitiveOrShadowMap[block.opcode]; var initialValues = {}; for (var _c = 0, _d = Object.keys(inputEntries); _c < _d.length; _c++) { var key = _d[_c]; var defaultInput = Block_1.BlockBase.getDefaultInput(block.opcode, key); if (defaultInput) { initialValues[key] = defaultInput.initial; } } inputs = serializeInputsToInputs(block.inputs, { target: target, blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap, block: block, initialValues: initialValues, inputEntries: inputEntries }); break; } } } return { inputs: inputs, fields: fields, mutation: mutation }; } function serializeBlock(block, options) { // Serialize a block, mutating the passed block data and returning the // ID which should be used when referring to this block, or null if no // such block could be serialized. // // As discussed in serializeTarget, blocks are serialized into a single // flat dictionary (per target), rather than an abstract syntax tree. // Within a serialized block, it's common to find reference to another // block by its ID. This is seen in linking to the next and parent blocks, // and to inputs. // // In Scratch 3.0 (contrasting with 2.0 as well as the intermediate format // created for sb-edit), "inputs" are stored in not one but two containers // per block: inputs, and fields. The difference is discussed in their // corresponding functions. Blocks may also carry a mutation, a mapping of // XML property names and values, for use in some blocks (notably those // associated with custom blocks). All this data is serialized and detailed // in serializeInputs. // // serializeBlock is in charge of serializing an individual block, as well // as its following block, and building the links between it and its parent // and siblings. As with other block-related functions, data is collected // into a flat mapping of IDs to their associated serialized block. // // It's possible for a block to be skipped altogether during serialization, // because it referred to some value which could not be converted into // valid SB3 data. For reporters, this means leaving an empty input; for // stack blocks, it means skipping to the next block in the sibling array // (or leaving an empty connection if there is none). It's up to the caller // to handle serializeBlock returning a null blockId usefully. // // Note that while serializeBlock will recursively serialize input blocks, // it will not serialize the following sibling block. As such, the // serialized block will always contain {next: null}. The caller is // responsible for updating this and setting it to the following block ID. // (The function serializeBlockStack is generally where this happens.) var blockData = options.blockData, initialBroadcastName = options.initialBroadcastName, customBlockDataMap = options.customBlockDataMap, parent = options.parent, target = options.target; var serializeInputsResult = serializeInputs(block, { target: target, blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap }); if (!serializeInputsResult) { return null; } var inputs = serializeInputsResult.inputs, fields = serializeInputsResult.fields, mutation = serializeInputsResult.mutation; var obj = { opcode: block.opcode, parent: parent ? parent.id : null, next: null, topLevel: !parent, inputs: inputs, fields: fields, mutation: mutation, shadow: false }; if (obj.topLevel) { obj.x = options.x; obj.y = options.y; } var blockId = block.id; blockData[blockId] = obj; return blockId; } function serializeBlockStack(blocks, options) { // Serialize a stack of blocks, returning the ID of the first successfully // serialized block, or null if there is none. // // When serializing a block returns null, there is an expectation that the // block should be "skipped" by the caller. When dealing with stack blocks, // that means making a connection between the previous block and the first // successfully successfully serialized following block. This function // handles that case, as well as building the connections between stack // blocks in general. // // Note that the passed options object will be mutated, to change the // parent block to the previous block in the stack. var blockData = options.blockData; var previousBlockId = null; var firstBlockId = null; for (var _i = 0, blocks_1 = blocks; _i < blocks_1.length; _i++) { var block = blocks_1[_i]; var blockId = serializeBlock(block, options); if (!blockId) { continue; } if (!firstBlockId) { firstBlockId = blockId; } if (previousBlockId) { blockData[previousBlockId].next = blockId; } previousBlockId = blockId; options.parent = block; } return firstBlockId; } function collectCustomBlockData(target) { // Parse the scripts in a target, collecting metadata about each custom // block's arguments and other info, and return a mapping of proccode to // the associated data. // // It's necesary to collect this data prior to serializing any associated // procedures_call blocks, because they require access to data only found // on the associated procedures_definition. (Specifically, the types of // each input on the custom block, since those will influence the initial // value & shadow type in the serialized caller block's inputs.) var data = {}; for (var _i = 0, _a = target.scripts; _i < _a.length; _i++) { var script = _a[_i]; var block = script.blocks[0]; if (block.opcode !== OpCode_1.OpCode.procedures_definition) { continue; } var proccode = block.inputs.PROCCODE.value; var warp = block.inputs.WARP.value; var args = []; var argData = block.inputs.ARGUMENTS.value; for (var i = 0; i < argData.length; i++) { var _b = argData[i], name_1 = _b.name, type = _b.type; if (type === "label") { continue; } var id = "".concat(block.id, "-argument-").concat(i); args.push({ id: id, name: name_1, type: type, default: { boolean: "false", numberOrString: "" }[type] }); } data[proccode] = { args: args, warp: warp }; } return data; } function serializeTarget(target, options) { // Serialize a target. This function typically isn't used on its own, in // favor of the specialized functions for sprites and stage. It contains // the base code shared across all targets - sounds and costumes, variables // and lists, and, of course, blocks, for example. // // In Scratch 3.0, the representation for the code in a sprite is a flat, // one-dimensional mapping of block ID to block data. To identify which // blocks are the first block in a "script", a topLevel flag is used. // This differs considerably from 2.0, where the scripts property of any // target contained an AST (abstract syntax tree) representation. // // When a block is serialized, a flat block mapping is returned, and this // is combined into the mapping of whatever is consuming the serialized // data. Eventually, all blocks (and their subblocks, inputs, etc) have // been serialized, and the collected data is stored on the target. // // serializeTarget also handles converting costumes, sounds, variables, // etc into the structures Scratch 3.0 expects. function mapToIdObject(values, fn) { // Map an Array of objects with an "id` property // (e.g [{id: 1, prop: "val"}, ...]) // into an object whose keys are the `id` property, // and whose values are the passed objects transformed by `fn`. var ret = {}; for (var _i = 0, values_1 = values; _i < values_1.length; _i++) { var object = values_1[_i]; ret[object.id] = fn(object); } return ret; } var broadcasts = options.broadcasts, initialBroadcastName = options.initialBroadcastName; var blockData = {}; var customBlockDataMap = collectCustomBlockData(target); for (var _i = 0, _a = target.scripts; _i < _a.length; _i++) { var script = _a[_i]; serializeBlockStack(script.blocks, { target: target, blockData: blockData, initialBroadcastName: initialBroadcastName, customBlockDataMap: customBlockDataMap, x: script.x, y: script.y }); } return { name: target.name, isStage: target.isStage, currentCostume: target.costumeNumber, layerOrder: target.layerOrder, volume: target.volume, blocks: blockData, broadcasts: broadcasts, // @todo sb-edit doesn't support comments (as of feb 12, 2020) comments: {}, sounds: target.sounds.map(function (sound) { var _a, _b; return ({ name: sound.name, dataFormat: sound.ext, assetId: sound.md5, md5ext: sound.md5 + "." + sound.ext, sampleCount: (_a = sound.sampleCount) !== null && _a !== void 0 ? _a : undefined, rate: (_b = sound.sampleRate) !== null && _b !== void 0 ? _b : undefined }); }), costumes: target.costumes.map(function (costume) { var _a, _b; return ({ name: costume.name, assetId: costume.md5, md5ext: costume.md5 + "." + costume.ext, bitmapResolution: costume.bitmapResolution, dataFormat: costume.ext, rotationCenterX: (_a = costume.centerX) !== null && _a !== void 0 ? _a : undefined, rotationCenterY: (_b = costume.centerY) !== null && _b !== void 0 ? _b : undefined }); }), variables: mapToIdObject(target.variables, function (_a) { var name = _a.name, value = _a.value, cloud = _a.cloud; if (cloud) { return [name, value, cloud]; } else { return [name, value]; } }), lists: mapToIdObject(target.lists, function (_a) { var name = _a.name, value = _a.value; return [name, value]; }) }; } var rotationStyleMap = { normal: "all around", leftRight: "left-right", none: "don't rotate" }; function serializeSprite(sprite, options) { // Serialize a sprite. Extending from a serialized target, sprites carry // a variety of properties for their on-screen position and appearance. var initialBroadcastName = options.initialBroadcastName; return __assign(__assign({}, serializeTarget(sprite, { initialBroadcastName: initialBroadcastName, // Broadcasts are stored on the stage, not on any sprite. broadcasts: {} })), { isStage: false, x: sprite.x, y: sprite.y, size: sprite.size, direction: sprite.direction, rotationStyle: rotationStyleMap[sprite.rotationStyle], draggable: sprite.isDraggable, visible: sprite.visible }); } function serializeStage(stage, options) { // Serialize a stage. Extending from a serialized target, the stage carries // additional properties for values shared across the project - notably, // the broadcast dictionary, as well as values for some extensions. var broadcasts = options.broadcasts, initialBroadcastName = options.initialBroadcastName; return __assign(__assign({}, serializeTarget(stage, { broadcasts: broadcasts, initialBroadcastName: initialBroadcastName })), { isStage: true, tempo: options.tempo, textToSpeechLanguage: options.textToSpeechLanguage, videoState: options.videoState, videoTransparency: options.videoTransparency }); } function serializeProject(project) { // Serialize a project. This is the master function used when project.toSb3 // is called. The main purpose of serializeProject is to serialize each // target (sprite or stage) and collect them together in the final output // format. It also provides utility functions shared across every target's // serialization, e.g. broadcast utilities. // Set the broadcast name used in obscured broadcast inputs to the first // sorted-alphabetically broadcast's name. While we're parsing through // all the broadcast names in the project, also store them on a simple // mapping of (name -> name), to be stored on the stage. (toSb3 uses a // broadcast's name as its ID.) var lowestName; var broadcasts = {}; for (var _i = 0, _a = __spreadArray([project.stage], project.sprites, true); _i < _a.length; _i++) { var target = _a[_i]; for (var _b = 0, _c = target.blocks; _b < _c.length; _b++) { var block = _c[_b]; if (block.opcode === OpCode_1.OpCode.event_whenbroadcastreceived || block.opcode === OpCode_1.OpCode.event_broadcast || block.opcode === OpCode_1.OpCode.event_broadcastandwait) { var broadcastInput = block.opcode === OpCode_1.OpCode.event_whenbroadcastreceived ? block.inputs.BROADCAST_OPTION : block.inputs.BROADCAST_INPUT; if (broadcastInput.type === "broadcast") { var currentName = broadcastInput.value; if (typeof lowestName === "undefined" || currentName < lowestName) { lowestName = currentName; } broadcasts[currentName] = currentName; } } } } var initialBroadcastName = lowestName || "message1"; return { targets: __spreadArray([ serializeStage(project.stage, { initialBroadcastName: initialBroadcastName, broadcasts: broadcasts, tempo: project.tempo, textToSpeechLanguage: project.textToSpeechLanguage, videoState: project.videoOn ? "on" : "off", videoTransparency: project.videoAlpha }) ], project.sprites.map(function (sprite) { return serializeSprite(sprite, { initialBroadcastName: initialBroadcastName }); }), true), meta: { semver: "3.0.0" } }; } return { json: JSON.stringify(serializeProject(project)) }; } exports.default = toSb3;