From 1401d54add9ac733a57277e7acce461ab65cd8f5 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Mon, 21 May 2018 11:30:22 -0400 Subject: [PATCH 1/6] Parse SB2 comments and attach block comments to the blocks they belong to. Send comment xml on workspace update so they can be rendered. --- src/engine/blocks.js | 25 +++++++-- src/engine/comment.js | 52 +++++++++++++++++ src/engine/target.js | 8 +-- src/serialization/sb2.js | 117 ++++++++++++++++++++++++++++++++++----- src/virtual-machine.js | 6 +- 5 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 src/engine/comment.js diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ccc4cb209..360d3fe9b 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record'); const Clone = require('../util/clone'); const {Map} = require('immutable'); const BlocksExecuteCache = require('./blocks-execute-cache'); +const log = require('../util/log'); /** * @fileoverview @@ -736,8 +737,8 @@ class Blocks { * by a Blockly/scratch-blocks workspace. * @return {string} String of XML representing this object's blocks. */ - toXML () { - return this._scripts.map(script => this.blockToXML(script)).join(); + toXML (comments) { + return this._scripts.map(script => this.blockToXML(script, comments)).join(); } /** @@ -746,7 +747,7 @@ class Blocks { * @param {!string} blockId ID of block to encode. * @return {string} String of XML representing this block and any children. */ - blockToXML (blockId) { + blockToXML (blockId, comments) { const block = this._blocks[blockId]; // Encode properties of this block. const tagName = (block.shadow) ? 'shadow' : 'block'; @@ -756,6 +757,18 @@ class Blocks { type="${block.opcode}" ${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''} >`; + const commentId = block.comment; + if (commentId) { + if (comments) { + if (comments.hasOwnProperty(commentId)) { + xmlString += comments[commentId].toXML(); + } else { + log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`); + } + } else { + log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`); + } + } // Add any mutation. Must come before inputs. if (block.mutation) { xmlString += this.mutationToXML(block.mutation); @@ -768,11 +781,11 @@ class Blocks { if (blockInput.block || blockInput.shadow) { xmlString += ``; if (blockInput.block) { - xmlString += this.blockToXML(blockInput.block); + xmlString += this.blockToXML(blockInput.block, comments); } if (blockInput.shadow && blockInput.shadow !== blockInput.block) { // Obscured shadow. - xmlString += this.blockToXML(blockInput.shadow); + xmlString += this.blockToXML(blockInput.shadow, comments); } xmlString += ''; } @@ -798,7 +811,7 @@ class Blocks { } // Add blocks connected to the next connection. if (block.next) { - xmlString += `${this.blockToXML(block.next)}`; + xmlString += `${this.blockToXML(block.next, comments)}`; } xmlString += ``; return xmlString; diff --git a/src/engine/comment.js b/src/engine/comment.js new file mode 100644 index 000000000..32a82f499 --- /dev/null +++ b/src/engine/comment.js @@ -0,0 +1,52 @@ +/** + * @fileoverview + * Object representing a Scratch Comment (block or workspace). + */ + +const uid = require('../util/uid'); +const cast = require('../util/cast'); + +class Comment { + /** + * @param {string} id Id of the variable. + * @param {string} name Name of the variable. + * @param {string} type Type of the variable, one of '' or 'list' + * @param {boolean} isCloud Whether the variable is stored in the cloud. + * @constructor + */ /* TODO should the comment constructor take in an id? will we need this for sb3? */ + constructor (id, text, x, y, width, height, minimized) { + this.id = id || uid(); + this.text = text; + this.x = cast.toNumber(x); + this.y = cast.toNumber(y); + this.width = Math.max(cast.toNumber(width), Comment.MIN_WIDTH); + this.height = Math.max(cast.toNumber(height), Comment.MIN_HEIGHT); + this.minimized = minimized || false; + this.blockId = null; + } + + toXML () { + return `${this.text}`; + } + + // TODO choose min and defaults for width and height + static get MIN_WIDTH () { + return 20; + } + + static get MIN_HEIGHT () { + return 20; + } + + static get DEFAULT_WIDTH () { + return 100; + } + + static get DEFAULT_HEIGHT () { + return 100; + } + +} + +module.exports = Comment; diff --git a/src/engine/target.js b/src/engine/target.js index 9861c455d..223254221 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -43,16 +43,16 @@ class Target extends EventEmitter { this.blocks = blocks; /** * Dictionary of variables and their values for this target. - * Key is the variable name. + * Key is the variable id. * @type {Object.} */ this.variables = {}; /** - * Dictionary of lists and their contents for this target. - * Key is the list name. + * Dictionary of comments for this target. + * Key is the comment id. * @type {Object.} */ - this.lists = {}; + this.comments = {}; /** * Dictionary of custom state for this target. * This can be used to store target-specific custom state for blocks which need it. diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 4ef47ba15..20688f35b 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -13,6 +13,7 @@ const log = require('../util/log'); const uid = require('../util/uid'); const StringUtil = require('../util/string-util'); const specMap = require('./sb2_specmap'); +const Comment = require('../engine/comment'); const Variable = require('../engine/variable'); const MonitorRecord = require('../engine/monitor-record'); const StageLayering = require('../engine/stage-layering'); @@ -110,15 +111,26 @@ const flatten = function (blocks) { * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retreive a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. - * @return {Array.} Scratch VM-format block list. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The current index of the top block in this list if it were in a flattened + * list of all blocks for the target + * @return {[Array., int]} Tuple where first item is the Scratch VM-format block list, and + * second item is the updated comment index */ -const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions) { +const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) { const resultingList = []; let previousBlock = null; // For setting next. for (let i = 0; i < blockList.length; i++) { const block = blockList[i]; // eslint-disable-next-line no-use-before-define - const parsedBlock = parseBlock(block, addBroadcastMsg, getVariableId, extensions); + const parsedBlockAndComments = parseBlock(block, addBroadcastMsg, getVariableId, + extensions, comments, commentIndex); + const parsedBlock = parsedBlockAndComments[0]; + // Update commentIndex + commentIndex = parsedBlockAndComments[1]; + if (typeof parsedBlock === 'undefined') continue; if (previousBlock) { parsedBlock.parent = previousBlock.id; @@ -127,7 +139,7 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte previousBlock = parsedBlock; resultingList.push(parsedBlock); } - return resultingList; + return [resultingList, commentIndex]; }; /** @@ -138,14 +150,21 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retreive a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. + * @param {object} comments Comments that need to be attached to the blocks that need to be parsed */ -const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions) { +const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions, comments) { + // Keep track of the index of the current script being + // parsed in order to attach block comments correctly + let scriptIndexForComment = 0; + for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; const scriptX = script[0]; const scriptY = script[1]; const blockList = script[2]; - const parsedBlockList = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions); + const [parsedBlockList, newCommentIndex] = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions, + comments, scriptIndexForComment); + scriptIndexForComment = newCommentIndex; if (parsedBlockList[0]) { // Adjust script coordinates to account for // larger block size in scratch-blocks. @@ -242,11 +261,13 @@ const parseMonitorObject = (object, runtime, targets, extensions) => { // Create var id getter to make block naming/parsing easier, variables already created. const getVariableId = generateVariableIdGetter(target.id, false); // eslint-disable-next-line no-use-before-define - const block = parseBlock( + const [block, _] = parseBlock( [object.cmd, object.param], // Scratch 2 monitor blocks only have one param. null, // `addBroadcastMsg`, not needed for monitor blocks. getVariableId, - extensions + extensions, + null, // `comments`, not needed for monitor blocks + null // `commentIndex`, not needed for monitor blocks ); // Monitor blocks have special IDs to match the toolbox obtained from the getId @@ -423,9 +444,51 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) } } + // If included, parse any and all comments on the object (this includes top-level + // workspace comments as well as comments attached to specific blocks) + const blockComments = {}; + if (object.hasOwnProperty('scriptComments')) { + const comments = object.scriptComments.map(commentDesc => { + const newComment = new Comment( + null, // generate a new id for this comment + commentDesc[6], // text content of sb2 comment + commentDesc[0] * 1.5, // x position of comment + commentDesc[1] * 2.2, // y position of comment + commentDesc[2] * 1.5, // width of comment + commentDesc[3] * 2.2, // height of comment + !commentDesc[4] // commentDesc[4] -- false means minimized, true means full screen + ); + if (commentDesc[5] >= 0) { + // commentDesc[5] refers to the index of the block that this + // comment is attached to -- in a flattened version of the + // scripts array. + // If commentDesc[5] is -1, this is a workspace comment (we don't need to do anything + // extra at this point), otherwise temporarily save the flattened script array + // index as the blockId property of the new comment. We will + // change this to refer to the actual block id of the corresponding + // block when that block gets created + newComment.blockId = commentDesc[5]; + // Add this comment to the block comments object with its script index + // as the key + if (blockComments.hasOwnProperty(commentDesc[5])) { + blockComments[commentDesc[5]].push(newComment); + } else { + blockComments[commentDesc[5]] = [newComment]; + } + } + return newComment; + }); + + // Add all the comments that were just created to the target.comments, + // referenced by id + comments.forEach(comment => { + target.comments[comment.id] = comment; + }); + } + // If included, parse any and all scripts/blocks on the object. if (object.hasOwnProperty('scripts')) { - parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions); + parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments); } // Update stage specific blocks (e.g. sprite clicked <=> stage clicked) @@ -619,9 +682,15 @@ const specMapBlock = function (block) { * @param {Function} addBroadcastMsg function to update broadcast message name map * @param {Function} getVariableId function to retrieve a variable's ID based on name * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. - * @return {object} Scratch VM format block, or null if unsupported object. + * @param {object} comments - Comments from sb2 project that need to be attached to blocks. + * They are indexed in this object by the sb2 flattened block list index indicating + * which block they should attach to. + * @param {int} commentIndex The comment index for the block to be parsed if it were in a flattened + * list of all blocks for the target + * @return {[object, int]} Tuple where first item is the Scratch VM-format block (or null if unsupported object), + * and second item is the updated comment index (after this block and its children are parsed) */ -const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions) { +const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) { const blockMetadata = specMapBlock(sb2block); if (!blockMetadata) { return; @@ -645,6 +714,21 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension shadow: false, // No shadow blocks in an SB2 by default. children: [] // Store any generated children, flattened in `flatten`. }; + + // Attach any comments to this block.. + const commentsForParsedBlock = (comments && typeof commentIndex === 'number' && !isNaN(commentIndex)) ? + comments[commentIndex] : null; + if (commentsForParsedBlock) { + // TODO currently only attaching the last comment to the block if there are multiple... + // not sure what to do here.. concatenate all the messages in all the comments and only + // keep one around? + activeBlock.comment = commentsForParsedBlock[commentsForParsedBlock.length - 1].id; + commentsForParsedBlock.forEach(comment => { + comment.blockId = activeBlock.id; + }); + } + commentIndex++; + // For a procedure call, generate argument map from proc string. if (oldOpcode === 'call') { blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); @@ -671,10 +755,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension let innerBlocks; if (typeof providedArg[0] === 'object' && providedArg[0]) { // Block list occupies the input. - innerBlocks = parseBlockList(providedArg, addBroadcastMsg, getVariableId, extensions); + [innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId, + extensions, comments, commentIndex); } else { // Single block occupies the input. - innerBlocks = [parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions)]; + const parsedBlockDesc = parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions, + comments, commentIndex); + innerBlocks = [parsedBlockDesc[0]]; + // Update commentIndex + commentIndex = parsedBlockDesc[1]; } let previousBlock = null; for (let j = 0; j < innerBlocks.length; j++) { @@ -914,7 +1003,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension break; } } - return activeBlock; + return [activeBlock, commentIndex]; }; module.exports = { diff --git a/src/virtual-machine.js b/src/virtual-machine.js index d236650ff..1709674c3 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1005,12 +1005,16 @@ class VirtualMachine extends EventEmitter { ); const variables = Object.keys(variableMap).map(k => variableMap[k]); + const wkspcComments = Object.keys(this.editingTarget.comments) + .map(k => this.editingTarget.comments[k]) + .filter(c => c.blockId === null); const xmlString = ` ${variables.map(v => v.toXML()).join()} - ${this.editingTarget.blocks.toXML()} + ${wkspcComments.map(c => c.toXML()).join()} + ${this.editingTarget.blocks.toXML(this.editingTarget.comments)} `; this.emit('workspaceUpdate', {xml: xmlString}); From 1639444a4d89686264dd5f38713a723f89cbef49 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Mon, 21 May 2018 13:51:43 -0400 Subject: [PATCH 2/6] Fix lint and tests. --- src/engine/blocks.js | 2 ++ src/sprites/rendered-target.js | 3 --- test/integration/import_sb2.js | 2 -- test/unit/engine_target.js | 1 - test/unit/serialization_sb2.js | 2 -- test/unit/virtual-machine.js | 43 +++++++++++++++++++++++++++++++--- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 360d3fe9b..e73f4b27f 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -735,6 +735,7 @@ class Blocks { /** * Encode all of `this._blocks` as an XML string usable * by a Blockly/scratch-blocks workspace. + * @param {object} comments Map of comments referenced by id * @return {string} String of XML representing this object's blocks. */ toXML (comments) { @@ -745,6 +746,7 @@ class Blocks { * Recursively encode an individual block and its children * into a Blockly/scratch-blocks XML string. * @param {!string} blockId ID of block to encode. + * @param {object} comments Map of comments referenced by id * @return {string} String of XML representing this block and any children. */ blockToXML (blockId, comments) { diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index f8a4bb1eb..b6df05e6c 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -930,7 +930,6 @@ class RenderedTarget extends Target { newClone.rotationStyle = this.rotationStyle; newClone.effects = JSON.parse(JSON.stringify(this.effects)); newClone.variables = JSON.parse(JSON.stringify(this.variables)); - newClone.lists = JSON.parse(JSON.stringify(this.lists)); newClone.initDrawable(StageLayering.SPRITE_LAYER); newClone.updateAllDrawableProperties(); // Place behind the current target. @@ -957,7 +956,6 @@ class RenderedTarget extends Target { newTarget.rotationStyle = this.rotationStyle; newTarget.effects = JSON.parse(JSON.stringify(this.effects)); newTarget.variables = JSON.parse(JSON.stringify(this.variables)); - newTarget.lists = JSON.parse(JSON.stringify(this.lists)); newTarget.updateAllDrawableProperties(); newTarget.goBehindOther(this); return newTarget; @@ -1049,7 +1047,6 @@ class RenderedTarget extends Target { rotationStyle: this.rotationStyle, blocks: this.blocks._blocks, variables: this.variables, - lists: this.lists, costumes: costumes, sounds: this.getSounds(), tempo: this.tempo, diff --git a/test/integration/import_sb2.js b/test/integration/import_sb2.js index 04c41d46a..905ce8fe0 100644 --- a/test/integration/import_sb2.js +++ b/test/integration/import_sb2.js @@ -30,7 +30,6 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); - t.type(targets[0].lists, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -41,7 +40,6 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); - t.type(targets[1].lists, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0); diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js index 30ff5eb82..df327f698 100644 --- a/test/unit/engine_target.js +++ b/test/unit/engine_target.js @@ -12,7 +12,6 @@ test('spec', t => { t.type(target.id, 'string'); t.type(target.blocks, 'object'); t.type(target.variables, 'object'); - t.type(target.lists, 'object'); t.type(target._customState, 'object'); t.type(target.createVariable, 'function'); diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js index 4d8c906b4..e20834314 100644 --- a/test/unit/serialization_sb2.js +++ b/test/unit/serialization_sb2.js @@ -29,7 +29,6 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); - t.type(targets[0].lists, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -40,7 +39,6 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); - t.type(targets[1].lists, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index f6731b194..70d06fee3 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -307,6 +307,17 @@ test('duplicateSprite assigns duplicated sprite a fresh name', t => { test('emitWorkspaceUpdate', t => { const vm = new VirtualMachine(); + const blocksToXML = comments => { + let blockString = 'blocks\n'; + if (comments) { + for (const commentId in comments) { + const comment = comments[commentId]; + blockString += `A Block Comment: ${comment.toXML()}`; + } + + } + return blockString; + }; vm.runtime.targets = [ { isStage: true, @@ -316,7 +327,13 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + aStageComment: { + toXML: () => 'aStageComment', + blockId: null + } } }, { variables: { @@ -325,7 +342,13 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + someBlockComment: { + toXML: () => 'someBlockComment', + blockId: 'someBlockId' + } } }, { variables: { @@ -334,7 +357,17 @@ test('emitWorkspaceUpdate', t => { } }, blocks: { - toXML: () => 'blocks' + toXML: blocksToXML + }, + comments: { + someOtherComment: { + toXML: () => 'someOtherComment', + blockId: null + }, + aBlockComment: { + toXML: () => 'aBlockComment', + blockId: 'a block' + } } } ]; @@ -347,6 +380,10 @@ test('emitWorkspaceUpdate', t => { t.notEqual(xml.indexOf('local'), -1); t.equal(xml.indexOf('unused'), -1); t.notEqual(xml.indexOf('blocks'), -1); + t.equal(xml.indexOf('aStageComment'), -1); + t.equal(xml.indexOf('someBlockComment'), -1); + t.notEqual(xml.indexOf('someOtherComment'), -1); + t.notEqual(xml.indexOf('A Block Comment: aBlockComment'), -1); t.end(); }); From b0aa288916dd0d949d16263eac807be40da7376b Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Thu, 24 May 2018 16:23:08 -0400 Subject: [PATCH 3/6] Add info about minimized state to comment xml. --- src/engine/comment.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine/comment.js b/src/engine/comment.js index 32a82f499..acd796681 100644 --- a/src/engine/comment.js +++ b/src/engine/comment.js @@ -26,8 +26,9 @@ class Comment { } toXML () { - return `${this.text}`; + return `${this.text}`; } // TODO choose min and defaults for width and height From f079eb4bbd6aa2024bb5755edf70ec6b666fa18c Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Mon, 28 May 2018 21:24:47 -0400 Subject: [PATCH 4/6] Don't serialize x and y for sb2 comments so that they can be auto positioned. --- src/engine/comment.js | 4 ++-- src/serialization/sb2.js | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/engine/comment.js b/src/engine/comment.js index acd796681..2049e5729 100644 --- a/src/engine/comment.js +++ b/src/engine/comment.js @@ -17,8 +17,8 @@ class Comment { constructor (id, text, x, y, width, height, minimized) { this.id = id || uid(); this.text = text; - this.x = cast.toNumber(x); - this.y = cast.toNumber(y); + this.x = x; + this.y = y; this.width = Math.max(cast.toNumber(width), Comment.MIN_WIDTH); this.height = Math.max(cast.toNumber(height), Comment.MIN_HEIGHT); this.minimized = minimized || false; diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 20688f35b..9669b1f5a 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -449,16 +449,19 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) const blockComments = {}; if (object.hasOwnProperty('scriptComments')) { const comments = object.scriptComments.map(commentDesc => { + const isBlockComment = commentDesc[5] >= 0; const newComment = new Comment( null, // generate a new id for this comment commentDesc[6], // text content of sb2 comment - commentDesc[0] * 1.5, // x position of comment - commentDesc[1] * 2.2, // y position of comment + // Only serialize x & y position of comment if it's a workspace comment + // If it's a block comment, we'll let scratch-blocks handle positioning + isBlockComment ? null : commentDesc[0] * 1.5, // x position of comment + isBlockComment ? null : commentDesc[1] * 2.2, // y position of comment commentDesc[2] * 1.5, // width of comment commentDesc[3] * 2.2, // height of comment !commentDesc[4] // commentDesc[4] -- false means minimized, true means full screen ); - if (commentDesc[5] >= 0) { + if (isBlockComment) { // commentDesc[5] refers to the index of the block that this // comment is attached to -- in a flattened version of the // scripts array. From acd728dc2bcd531d9ef46bd4f27b2c2eed1d8eef Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Wed, 30 May 2018 15:34:27 -0400 Subject: [PATCH 5/6] No more xml differentiation for 'scratch' type of comments. --- src/engine/comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/comment.js b/src/engine/comment.js index 2049e5729..123e8e897 100644 --- a/src/engine/comment.js +++ b/src/engine/comment.js @@ -26,7 +26,7 @@ class Comment { } toXML () { - return `${this.text}`; } From 537dc9bcd5f6993683deaf15825f4f62f427f3ab Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Thu, 31 May 2018 16:33:42 -0400 Subject: [PATCH 6/6] Addressing PR comments. --- src/engine/comment.js | 19 ++++--- src/serialization/sb2.js | 42 +++++++++------ src/virtual-machine.js | 4 +- test/fixtures/comments.sb2 | Bin 0 -> 55084 bytes test/integration/comments.js | 91 +++++++++++++++++++++++++++++++++ test/integration/import_sb2.js | 2 + test/unit/engine_target.js | 1 + test/unit/serialization_sb2.js | 2 + 8 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 test/fixtures/comments.sb2 create mode 100644 test/integration/comments.js diff --git a/src/engine/comment.js b/src/engine/comment.js index 123e8e897..6dec2f0f4 100644 --- a/src/engine/comment.js +++ b/src/engine/comment.js @@ -4,23 +4,26 @@ */ const uid = require('../util/uid'); -const cast = require('../util/cast'); +const Cast = require('../util/cast'); class Comment { /** - * @param {string} id Id of the variable. - * @param {string} name Name of the variable. - * @param {string} type Type of the variable, one of '' or 'list' - * @param {boolean} isCloud Whether the variable is stored in the cloud. + * @param {string} id Id of the comment. + * @param {string} text Text content of the comment. + * @param {number} x X position of the comment on the workspace. + * @param {number} y Y position of the comment on the workspace. + * @param {number} width The width of the comment when it is full size. + * @param {number} height The height of the comment when it is full size. + * @param {boolean} minimized Whether the comment is minimized. * @constructor - */ /* TODO should the comment constructor take in an id? will we need this for sb3? */ + */ constructor (id, text, x, y, width, height, minimized) { this.id = id || uid(); this.text = text; this.x = x; this.y = y; - this.width = Math.max(cast.toNumber(width), Comment.MIN_WIDTH); - this.height = Math.max(cast.toNumber(height), Comment.MIN_HEIGHT); + this.width = Math.max(Cast.toNumber(width), Comment.MIN_WIDTH); + this.height = Math.max(Cast.toNumber(height), Comment.MIN_HEIGHT); this.minimized = minimized || false; this.blockId = null; } diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 9669b1f5a..5efd5c9da 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -37,6 +37,12 @@ const CORE_EXTENSIONS = [ 'sound' ]; +// Adjust script coordinates to account for +// larger block size in scratch-blocks. +// @todo: Determine more precisely the right formulas here. +const WORKSPACE_X_SCALE = 1.5; +const WORKSPACE_Y_SCALE = 2.2; + /** * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") * into an argument map. This allows us to provide the expected inputs @@ -166,11 +172,8 @@ const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, comments, scriptIndexForComment); scriptIndexForComment = newCommentIndex; if (parsedBlockList[0]) { - // Adjust script coordinates to account for - // larger block size in scratch-blocks. - // @todo: Determine more precisely the right formulas here. - parsedBlockList[0].x = scriptX * 1.5; - parsedBlockList[0].y = scriptY * 2.2; + parsedBlockList[0].x = scriptX * WORKSPACE_X_SCALE; + parsedBlockList[0].y = scriptY * WORKSPACE_Y_SCALE; parsedBlockList[0].topLevel = true; parsedBlockList[0].parent = null; } @@ -449,17 +452,26 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) const blockComments = {}; if (object.hasOwnProperty('scriptComments')) { const comments = object.scriptComments.map(commentDesc => { + const [ + commentX, + commentY, + commentWidth, + commentHeight, + commentFullSize, + flattenedBlockIndex, + commentText + ] = commentDesc; const isBlockComment = commentDesc[5] >= 0; const newComment = new Comment( null, // generate a new id for this comment - commentDesc[6], // text content of sb2 comment + commentText, // text content of sb2 comment // Only serialize x & y position of comment if it's a workspace comment // If it's a block comment, we'll let scratch-blocks handle positioning - isBlockComment ? null : commentDesc[0] * 1.5, // x position of comment - isBlockComment ? null : commentDesc[1] * 2.2, // y position of comment - commentDesc[2] * 1.5, // width of comment - commentDesc[3] * 2.2, // height of comment - !commentDesc[4] // commentDesc[4] -- false means minimized, true means full screen + isBlockComment ? null : commentX * WORKSPACE_X_SCALE, + isBlockComment ? null : commentY * WORKSPACE_Y_SCALE, + commentWidth * WORKSPACE_X_SCALE, + commentHeight * WORKSPACE_Y_SCALE, + !commentFullSize ); if (isBlockComment) { // commentDesc[5] refers to the index of the block that this @@ -470,13 +482,13 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) // index as the blockId property of the new comment. We will // change this to refer to the actual block id of the corresponding // block when that block gets created - newComment.blockId = commentDesc[5]; + newComment.blockId = flattenedBlockIndex; // Add this comment to the block comments object with its script index // as the key - if (blockComments.hasOwnProperty(commentDesc[5])) { - blockComments[commentDesc[5]].push(newComment); + if (blockComments.hasOwnProperty(flattenedBlockIndex)) { + blockComments[flattenedBlockIndex].push(newComment); } else { - blockComments[commentDesc[5]] = [newComment]; + blockComments[flattenedBlockIndex] = [newComment]; } } return newComment; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 1709674c3..ca6d8d173 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1005,7 +1005,7 @@ class VirtualMachine extends EventEmitter { ); const variables = Object.keys(variableMap).map(k => variableMap[k]); - const wkspcComments = Object.keys(this.editingTarget.comments) + const workspaceComments = Object.keys(this.editingTarget.comments) .map(k => this.editingTarget.comments[k]) .filter(c => c.blockId === null); @@ -1013,7 +1013,7 @@ class VirtualMachine extends EventEmitter { ${variables.map(v => v.toXML()).join()} - ${wkspcComments.map(c => c.toXML()).join()} + ${workspaceComments.map(c => c.toXML()).join()} ${this.editingTarget.blocks.toXML(this.editingTarget.comments)} `; diff --git a/test/fixtures/comments.sb2 b/test/fixtures/comments.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..081fbe68a5cd5f2b411125fa623bf68544807a95 GIT binary patch literal 55084 zcmeEu1z42p*7nfd-9y(fOa~<;pnzfr7GjH_g6($Ot*F~>F_17&$^eUy?odELx`!EN zV21kFGwjfP+~=I{`_32Fb$*rMnfIM}-}S7x*S*%7F~C<$To!}DNMm;Fx#!oOBA87R z$6(&bU@-D#0O|cHS z>))XL&L?WCj&GFf+O#dAAY^ zZ|J?akF z;25G=WDHfq;8|us-1_ZvlWngW?d9Y>7M$@;2#~*AG+EOsR#kJd)5$g6Y7Fs`K-Ah?dN6vv|DzW$?P$Gp;NhyWX^`SY*1xoK}-3 z7mHt}M#Zg&;i#|wbhEd2XwF>vI;W~V;wjIo_WvTo-QUa-_RY?I06;QoEbjvO&K{gPPG6F}YRcC)EnNimvs{E*11yCugiyth2^^4|K>iFIc` zt-2;SIEZt%)IDe!rQ=1uR2!7C#_ZC%eNJ4WgjV~M`x&p?CVf8FFgn+HQ`H5WD+!BhFcJR&Wd(~A>gPJTYBf9739G>@bN}B!30ck_e z_a{m{r|Rt5ba@pe*WLcYY#$k$W*5%+z)5z=*PrBkn&QU!P?kKmDu?qxznp5es#sCnWaCqa~`WTuN%(s zNt#^O>3dQ!T_dF+ysh(mk95m{?oL7J`{g2=R(DC{#xYOFHzId3dd$9_jZNf+^zCrl)^KLjBE*&nRd}5t+xNx(tPO? z4-CgjHXev8(bv5KF4@iw(xZ|01+(X}%? zE=e^P9I5YXt(tYN%X{*dxaN}A=Fjg_4++?K=|x2?ubtJVs-^0Y*?sSof4{YU`55bzsAS04+Q=B*&Bi%>{mijU_^3`V2@e%44!!jVY26&8aL z|7nV++wAS1dn$+ufJ`5r8ni_2usODbcF-wD1QUK1j%lNRgk|X8+r1+24CgTbI5gos3Z_}0}<@PpmDa&!3Fuux|cBA#f1 zUAw~BBseI-1iRhYgs^eD33kV?zqhSj5wXVEgh@pQ*Q{K-dQAkpg-AqCx2|2e%{>gg z153a%saQB{f?XLJv^->`-SVK|^{Y3BZ3%@7hpyZPFZkmn+c$(bgl`NAUg>PIar4UX zm7BM&gd1-N2@Q8PSrZYl(ZSw++qP}^ZDf4d=GFEj0)b!;694)VhwUM2L)ZW4g+wNk zX)io$;-Vmn#X7AP{z41k{``V=tlYemJaG~Wf1EZ3MXbTDS{oALY+~x==0+uYnqb2t zHixYT$xPw4@W%uT8}KKQ@Dv&eM`z**WM;4(JR?%=2zUyWXh+492^2dbJTmR*cshk- zN5)eLWDC>no{sR~h8~y`4<1ZBiN?Uu33wWbiKFAGBpQxJ!c)mq92rlelW;T&oFN6n z6&O?+j)W&L7_g=@NH`)DPozzOp?Hu*o5(P48;*a;o?ox*mS*#ByYKkM8J{h3fPT%HW-pukn= z!Jtnjg9$gL5y>C}flkFy&<;WK1Ue1WKw(f|8)(lUArn+ZBU5lx*bxa1Flb~P4b;N; z;qHl`ccdx;nE|_jErB%&LOsHML8nyMI@}OMfK9?R>0~mzj!FD(9S-kef+olyCmr5J zrQ4x9(tdPD4;r2c8=z6)wG1Q}m5KIA7H;Z~P5+@KC;LA<_b(LptFt2CBs1_NA|1z| z;ptRbFabxfV}L=xneZe#A|19uMEW9rxFKZTqJ6|Mluh>o%x2}ecBfoBkLAmVo#{3B$(%dn)S#HIgilKlq;qaaWt;~0R* zL=uD(+8M%P5(rKPyqbV+0AWbPU_Rmx(=G}_rf|a~NKAM-sF4bp>h~4Ohk*Bxh)F1G zGRYKBF7;OxksztVyHQ4>Qc)rxG10A1ofRIPv1RxY7TR>Ah0eApJJC#Njx+^3>2yn=7kd?v7 z=rjb@H1eNKIB{$6axxKJ6cP;)G)RG8AP35I-#!b6h+~4i!J{EP0k;4?g5+f){9+0v zh0PMtb&%JA$A9A+bKaMKZ~AyDC(U{E-NHi9<$ z?X%w_|9dbt6%vMD83Xt!37iA?2Zao50HPV(0r>=th8%;&MA>R0>X>9EIu6*NhdSG;$Zh5?E;yF1{46j5iv9r*)(7z$N;~uLm>SDBOyaDPsjiv2gCyM4CGgk z0)h~j0-fmraYO)uLk9t9xbH;&{@L$4{|58E<*$rCUhrRmLJ|{f3*vy_LaCIhn5jY-r{KTsen?|8Y1HC~)f)^o}{Ih=xsUI1XP6cCtjDSy& zA1UxoxH27xBe)b89-I}>4|WLF7YYioMHD(8paFOrmHvk=zg02mUxZ!&mUKuP5IRI) zW*`FS5(J}=5muoXBNJ%|YCtBy74ly=A-op&D!c$f1quitY$z-l->&^g5NV)$I&e#R z2*MNyb%<&L@Ihn^6qN3SKqZ7C z1XO^*2rFQJOn_yGBPt4Z$Tc{SgoIKKP+|Zd5{SxxXawnzr6FLUJ^~~lQXu%qfGu=j zjpT3FKawOIpjVmw>Yj@HgVOYd@&37*G^YQ9Opg?I^%P zAp{8&zwfN`M=JX}fJ28=4)q1f!n6=@TMFnDkd#IVL5?L%;4~Uhh(rX05yGO0OrwK~ z!XwK5WI7lUB`a`05&$V^6_JK-p8RP4en49(Lh=euJwfm(6ITLNOn?}W5(W`fWCRKW zM}=cRp&)q@aWs&F&O{JHfocz)(Setu+GT?EK;3}61^FV>a9~iOJm8~0c1EZt=u}kU zlAufgpP-_xfbT(^f~m>y1f?VD_m+m*6d)hi9VFpz)_&ir^RM*(AETBiz0slKfQ?fp z_#K2EpbqSgj_MpHJc29IeoTM?foMn*=pB$#ghdM#N~3SqzvIRCUh*H4k^#v9(mDhY zjXvQm5G+FYr3*6*1EofE&J`>T%8Cs3N#8n0Z{{70NxFi%Qx$PiIl($K@end)Hs>o0Cd1B zKzE1$AbP@TmCX1vt)4(Q&?RyX5+raSED&N~e3VdrTmEw*{?;h^wIqc0)XLzXjm{>* z?$_^?;)b;mD>sL%gu7ef_O~=)S); zv;O3dzX`aV=xjoUcQYB#--9(oE{#Efz8VRR10SH$;5e)S&Dvlq<05}t!iP{hh$ccdI zkOdHc2{RlSk}OaHqL5W1`U%$;=6oo~;PS#Ui8N73!{t4o5ko@!7{Mrxh6*||4$>6^ zr2JOazrrWe-wK#tH%9>S0VoUvADj`W5X$TfVRs3%i{b$e!U|PvOxOVd9fGhx6$mOG z(OUR|A8s4H8d5U67=RWogD&>lI~X3&R)ks*M}{O1WdR5;lm(Osy8P{#P_lnySo(j) zuoTGTkm*PaAjnLiWl>B)G6F6K*#YWb8fpST6(p=a{>reB-a%VJMu^flRp<=RqeCeY zDl#w@fq@t!B0xgJ!sSU6Fs!h%MT0zpY8xW7ec%wd65=gTBT#^9q8_ENC4~D<)_=;f zfG<$-;m8281eAi2a#0-(+7}vh!gj*ykt!_5nM`P#{%TNUT*Q=rGu5A|^aK`tW84Yr z3fptP`UBmALInjt%L?UO1b0Gl0DqyR{q31hxE~q!+d6?dnc@bq{YOLVH*i1>OocuK z4T>4a(j*#Ge?b1Bs)FkRy+huGf|bTZkV&OMPC!%^c_W;KtN=_GnFr+~L=J)M{7pAc z$O{m@4+*Ok5(RV!8wRG2N+$}kH>xW^7pR*J&p?|v3V<}~8hH%j=mZ9IY3PH1N0 z^8GYu&+#GBOl=GX{h(EC`D#Q3qV{OvU8AGiyYJ#=sjpd(Z$x*!A?!uAae7KBxo zkm{p45Bvu8I)JV)|01e7$44DIaNqCL^8K?v{Pue;{auR(g&Ev{ z216479Kax`vfz;d3>okU>PJYUkO@%o2MD5|qz_w%e1Xss0W=fJK?pwza037&gr=w# z_;JVz=_~>#V5_hT)R0EDf}8>BgA1aTF|1Lxo&Y{FG%3)n5pM)Qgf*3cdMRiu2&oRT zFjxSFBA`8>ZJ^JIUJB!f}k&iool)Rt@dX^$6^g0Bv)S6iRr|13(<|FVvry=-?Bn z3~*F3O8G(@L=+Qgi308lb|4CCG9nXdAXE&42h*UoL?@s|2M33%0z)MMKq3MEY%^hZ z4sr(a07ML^!m5c5J#j$Ji3edp09J)Isjx~Fng~^)U`?v9BM1Btr4%M$>vvlC{@FJg z|F7s7oEf5(fk^0YtwJ<~LLLb{WvEUea)I+O{&uGzE_6|lB~bGO6;p^wAx#p5%@rEx z5Y^Ukr=3;7a~E|?gJ2sO>`F7|iw z6dIDj_F-g3BTuw+z*CXC(xLW5OBA>iH2*-uS)^S$q<1izu>35hZvX>n8@5Olnuac%v4dGXC}_qCAqXxCiTgX@ zzkl}o-v33Z1`05k8lbd>3<}TwWK5_Sji#T1r@G|(%!1r?wMK4SpaMxQhQ{}Fzo1&ErEp}~h2;NH+c1NUH{5(dVd zU=wIdqkQ!@{0vbGDySWl46lP*Axe+5k0=ot#D>TObwb-5dK+*E`7d%!)S`tCBETB5 z6*x5IzkTd5QH;T@{@ce6fD$lv03}fAA!tG%Y^40pj~&1Q2ps<}jUB*)gnk5L2R|yr z8FE)B0{x&7FPsSpjq?Yw_&eBw=9>`m0FJ*+9Z-q=-#>Lg(-Qco4Oo}*kH!ul8yY)6 z-GIgp0C9*^!JHR?31T9sYX8fz1EBmLV}~DQjo@#l4!~+A;1nVcS}bVj0F$A=nmV94 z-t*>|H-KXT=KW61BA=(LkH-$|2}j;pN;x&8akjhoDeJihlUQQ+3n6fpBy>>b3sD~Ag^fX0JIei z9nj|{gunif?HeWh!=b}=+l{d-_(C4~K|=>J{-69(-k#sSl*bf-FA!%C4s*h;WQ)%- zx#L?FhuJ&t|9>m*?|tj=-{YHiu*-=r*!|^~&;I|n^1k_+>c8PP@BZGm4*%Y_@(M8= zNtqkr4WEt!Wc{nZmFL#qavgULgF!zC5()TiL0e}|@$xc*1M}SGPF}Sk0;>!sMBuN< zTnxrD9)l6ZC}UOxMFe5sH7%IwBAz0vMJOW9BDI+Hn2zy?aryDvV?krP#tx0`9*cm# zJH{T4wT)dGUxFFK+!2u$T_Ji}R90-E*bK4hVxeLRg{&K;l;lL5{=0sL1${;e$ge zyr_X%Hk-AL^^WDjTHj~dzm6LtaKoIIJSdl-Sg$gwZmbcYR;zqS@u=J$>BkawqBlpA z`4@S6Ir~^=dKG$=dQJLvvhMe1a9F(2;bxKR(zQwlHJr5fYG2fRr1n91rDC?+QfUkE zcjGq%9Bvf*a38mOO81^_`|hLNw|dN35$rqM3_*aXsmv3l6`D!9Ci;!KPqmJy6)ECm zDUvTlMaO3kZyL1b9O)bDVs}J!D0ek_=vc3`hwmr}6^w`%sV0&#@vy?s@Q8UGl(;C zH!RS-u5nY@S8ko;0a2&%rs1(cBaU&OPS;?YNt<1}Ql~@rt=_)=L)@yNzOfb3lT-$^ z=NauVUV^=)zenq6_J!?81wx~A;Hg9X0)PA8$m34Dq_t3eq z?b1xuJl%2QMza`G32d@%w1%qkeA!~i!qj0BZ=CgW_ujTCU*qeg8(59gT6T1F^rW#r z@=8ay%A8UUGdN);W_iT?tnqAp2dyC$vV5#$ALjC~3~vK#UDt!wyhfS&6JLB9#F_~m zmwUR{b%RI7iezXS6^6IWKUxf$zQgA0hiM0B)G3F_gkZw?UpbF@2RqlcwSGh z)Ud1jKIMNYZ#L*&#Q8Y10wb*usbgX0g>%Kb+l-h?n2hVUYlW)H$X*)v=au(rbQm^2 ztIzqYS9zxVP36Yg1Ysys|)W{K~D@7tN%Iaz%* z-(-5**xTTqDo^Y#SE8L#7gDKGnpq@S_)DQzab@|L&kw#j^voMNDs@`B%OaboK)*`y zx3jcfX(2GpHIX-LRDCQO$GP7^uenjat@upg-F&Bl-9?%et)Hu!`g-$*4HVWH&cZcN z#Tb%gwylHpQOi1uL9+#h-pbBn8ohfPBR=gcn^Zixa463#|3Fb;W$u@o?eYWiVyD!M zOg9s}8QbWtgfBKE>!((Wtv;B4)r*kj@xodc)to60Ek03*Dd@<3nx|WQrRqteSC8b# zV+DP~0^2!M5yp1XdfQSfYwHo~ajR)2%hepl_jgULJ5X`7IIgg_pfoQcZ&QJPSyYWv zn-5n+YJ<*ht2ty6U6*WT%eRQN47ahjyKEh15H0P^scLAgurJwAR9`4sP?WnZ_d`K) zxmClL-m4g*y0uxQ{TQ{D5@Xk48DeE=v(*kXYjH>8{Fr2?#AjkjO98g%c+u5@*1RYA z6~*hT9a=vRol=;IRkAatZl$K!zq8_67TR3IS>Uy7R_Uimar$4@$Ch>Ezt5XnkXF!^ zZ&namWLaTUzo&1Z_yQdV>k85`+A318jfmxBYn<(U+;-cu#^y>T{HLw=s!9rz^8E5M z@+ar_=2;ZtN_W-F@7y^0NG;U-BGHB(NwvY-SUs{TwONU?!1bClG^Ir9yUlC#i|zBM zd1nh03(x02&+jR8uVgkK8yZo1V7kFRnmSH-jz4cjv|eZX(N@CtoW)VyKIvNS*QU7g zb-60pvp<&R{an~qbh5asthjbfPnd+aK^*=HeG|QxaM`ND!o+qTey2UfRv&v>@vy+J zBjNMBBE_7o*>X8I@+1px6`d~AuhHu`Ic~0X&sv6R1Vb`y++?fuwk-T4`+2w*rrXpu zjbnPu>N1OWeoV-W$m;&MH+M(=Sdn@axmk}drTWO?1^F0*NGri_xAw%nBHSRB z(RwX*f+f{>ugpDnD3g(SCaWylDHmHvs0eQy4b%p)1>Wo^YN7= zOX_U$1G^w&CB9I!iO*e?^ub!{oCo>UZ`vnb2+y~aD=_v6`mrMh1i@E8h_M#VOIq|f9) zq6a~VlF6uLET&}I@O2i8m-j#V8eS<~Som>i#>m^$H&*YBb9^dJwyKWIRVA7o!q<^g zDK7{)_W9&@v}v@p#M|b!YR#kRT@`iFrQvzYv$ZmE-|D_o%~UGfR=c}*l7xe;_KroLgBq^+=tZk+BDv~vfQXDO1tAITP4rr zjNJmtd+I&%DT*>f!C@**#@@&Hl!6mKzx`Tmb%|AOOUAyphtrbNy)#c0C)7u>pGg_$ zF0%-*e@N-4Bv7w0%bb!Nu8^B7Gc}fp-0oZcl~AQ$@F0`*X8CK=*ADMB3i4_^yDyF} zRKppcww*-YPqUzjGgmu(bn2l++VKo-OJC-yw)%XsFZ9e(e0%#<<4bnBZO*EyZS7lz zDM}`W7FKTdD&*zlQkuEbSC<4wce1^ig_6jyS*Lhycd`4&lW)CV-ANsM#mLAgv1#(- zS;`*QU1Bb27eo9)Ql-stdgHor(osf*jjEQr$l<<>hHGWUIazOCq^?g~kX(@dDnIp$ z0Y_cxr8dh<-tG*^kgCgA<=p3H>_&4~YGm;q>v% z=lgvm30LhVvtziKq@$EHdZ^1fPrB!6=Z*Ffddd=+?AYdgRYiGc-_A}x{bbe?&6L*6 zJ=G;W#bQjY8K!S+UlC4`)amOc9rpg>bJvYVI<0q7T!C%aELD9rKj1AV(fUzEd~4#< z4DITLy~E<0wen2uY&-1Fk&ZL2yHEE0;A`gLN_5oOC32^)uQ8$WOm1Sj{Bx0bV*JOX z%~>Wjp6tugM!Iw}8{7GKZ&IMcBCkyU9^X9IFx&~X+hdu%N55{Z9LjNfO@BHoZpGtQ zFSGJ}z8)BQAg`h4WEN|^-7b@~$+^eJY1+t?T~34M#}sc34|dZVipm-@7p9z!zZEm* zi9$N9^lE$Cm_T`>zK_Xbi_g|UB-SJ|Kkezs-m_^7411)c2G@0-{lYC-_@N{5XqH4^x zPukL!m+Ex9#009==xZ21GBq_X#icrI^-1;5_a37kz^2Ho8+y@|-4IqjFH0h|@zJh$ zMv`@QeC>vTNwT&&v#@(i_nF08&LvH8WBcm)Ik^Sl2`FB$rGD;aAUMD)or#)Ky z;&{Q)<|@I6g162>!yK%QDaFc-GTr^!)Q{e#PFJl|)y|2`8mMVY``nzn<7Mn)%eYI= zwtjf>S&rQ$S*5W-Ki-gv)igVbH*;Z3<@-jvuP3DH6iOZ+n$%O#AYB^v_DO<7tlXn# zFRhDzZqFQFuc~E$!EP~DHI1=RVQTxhOuOkbli^^pS)Mms*2iz!TCpxeJBbw65!;rS znjQGXn)gZWy6z|J9+O9=&n@SZTiiqaC;OSZmfNn;s1dQ{&gwW@Gn9MuW%Z*KF}SBQ z-?>#K^gWg|)w*ktVd!XNYSL_*>O}Q5nU?0cnk1qZC}BPLQ}@1xGsS6d#-6NuI1;P; z>S@X5uH|ABjcfXk3=SJGu#Z`7HB>dSvRF?w@>=8XHD$g-pJ|i)^^v2jRV^~rud@4-pU0`hswJNK zc%yMlFk2}@H_xEUaIR6FnFjf_hl1b3DN~$wS-2_Zj$h+2+C6Jxa+kemj%UUuJe!y0 zS3h$oLZMUpihh#8N~5V}4@rwXWc_M=bexu3(p0NO%DIO-`)jFr2VO`$3XAbcAZ5mU zVGQNUAJqxfPcW!3bTY?~-+7$!?eQLQpjaxYy%&2m_@Jw{Mk42SvVLr0ba28iS*&_l zzN7+H=c*n@zr>Jgfu(HmeCliHoxyN6->Y&&RA#WTb4|_qk6)64VxppX35&AVG+rJ) zt`wf7m6 zYes5l8)}hOOnE!IdAgoUg83!IgmJ|oTh?fUd463|@x7v38L_)R(7(owZ&VM|_tEv# zl-0b7-Ay%`I(_ESse^P)Y=La)s5x(L_rn@NrbU9--K9}22_5-My7i@&YvFb8Yo@4H zYY@#bPHFz`0e3x*+m~p0O0)~G12@`(Dt+Hxizi1tc%c5OzglNVQz=Jho3^jIgX$*T zaQr^cx6{RaQyoG~)8W4zTgi82KW+*w+?kSdzw6G1$J=reIvz=kX;f?9RKKBYuD;52 zha=0sC7{g{Z||b%C$@ku!ENulS92ya{%Px7o9NJ&Wz`BpTb0aoc51CtU8d}*qm56U ztUPnrFU2v^q(v(7S zyqR|=kK#9|kB;|q=l92V;A&j6iO&t9pT~%#Iefl4bXPG(GeM22xJHq!^OEG~mpV@+ zK+4g=h#(n0xRb5XTk&;$@u8Q+56ADuJiU_V+np<|pfycPMOh$Mr(9#U-<>}H;R5>9 zi}rZ68>0sXa#+(kW`8=7vFmB=J(C!-^s(9}BOxkgT4pNyW#`IX*M3CRnDuO_=Ug|J zQ$~go?K~UK-QI@A%KRHiH}CQ8xFxg~{>1W-jnn8?JtFHZ#a7&6xz?*7uy@f8Uo8Sr z?aTO$K{1Ybhg`MH`@lyMchzFj-~8Ngew?Y^qUEgQB`G7ZMB_ZAeU{Af!Uc7%Te15k z*n_PDuB=N-H`FF^|`pBW~D}@T#BffSiD9ot!UQxVw+hj9Rl>s#pd%@ z3@%_tH`f%}q_p1;e{ki+n$O!us#FEq@02Eq4UchU-diMl%LJ;;|LQTz;+mY+i1pA$ z?u_o)pTys{K6?IO%d_Ufbk+@dmNrHsN_u#Fo#;>6j~G%jP3MVB{b>JCjXl1J|7lRE zzq>&{_v^Ez52nVeWi4oTmI~8erIjKVjp2_~D_Y{sd<*8D_m`)a=|+o1@wNDQT)B?B zc!EsypLSRzNq>~A8#i} z-Rp>1m7&scL*{3_I^A~)Co%S8vlVvORr%E|=m|K$II3MZuFUo3e&DQZUsvjz>VE&h zJ&Bjwzq}Kp>6sd8tBi=a3i`#OjFUYt%s)TpjH|BkWpN*_^1#Xge)sXuE$ znp2d?ZBoNwHMGx5$&Pf698gIkbx&WvaNPeosY`A9=on`gXJNly>zkt3gHh6)Q^aq8H^o~(28@^nRjIo-Tx>1WlG*d!+n=~-*htT59g034?V;_+L&3=ZX|ol z;H~}^g+l0oB}!Y_h)%Owa%;{!$NRdDW4BoCy*qk0w5XOAzM2|a|FHGdjQW02N!`;1 z@0CR{ru;AwoMD%%*n(f?pPRhYazG|#u!+5#qujT@u_w3k+0uJkWA1#|*YQ}P%h(5d zRWW~L5id{F+2ohW7Z>W!k8zK{J{O5&mG#Q^uk00Ve2}~6S@XTQ@eUsYdL5KqjBO04 za@U6Gyj$W~W{Z6y7KtzT;J3sIeG$}p&t zUc$S;b`tNg{NPu+G$Js@y~;2QbA-JE`c=IHlUf_{mpr|F>+XXW8N{x$iVD~OU83YN zt}{nQN)#tM!*a#G#og}r4130RvXj`$d7S=)uW~s>@hUet5AriVbTbtr3~IFO#dEkz zxDciG6|;3$jV>yi^faSWC;t&#{?T$M42vaC$}6)|yo7IEWW=GzJdK zy{7C9%wFfSnCZk-eL1kL^F#l^;o^~+-kBws$MY}FzS^Ct)Ve{r(qg?yoZRfe8|-tE zG(2~1_FDI)?vu>5h6Z$0%HcQE{#&{+Hj}WL(sI0U-k4G?vvRsbkN`~H= zUqG5HE7Gsj!yBv{mmPdtTa&!=g8lKFTiC*M(X$pb++6KFBfK69-d62MFO{|a8_ebm z+1;1Az!L8}A!x_w@ScB4e3o)9>G-C*rRD8X3#{C1q%`&lKKDG~*BUPH&0TwAEq#WR zEnZ5M)5{JT;g5c1>6Upv-gEZcY4g~fpH1awTA#N%rE+`lYqyJ_)d)LnXNY>p=@~@p zX3?p=K7B2MN#iD*Z52O1Dm$Hgq9|rwU5IiKuFLw0QWWQM=aS){Ok-x=4C93;Om8xO zBa+w~#!44#9NpHNS1|lw>;&$V^OHTzb2LeKPm6Y0!+xc%2bgi3-J-$>kC2tV##pK0 z#!ei|dL(qLwr5$s=snzV!Ku5?9XhY-QthXi&6FDKd)0G9Vl63uN#y3Gt7<*B>rWqE z++)sm7{0~NZtwhPbg%2g;s%hITnu~ zJ9rz0E&5HWRG;rZuX$4X?yAy768kL=*`C!h8T*MnXH?oa#Xmj7ZNul8vuvBhJ9|dE z-|+5_6mnM8^rX(e%sz4IrhneM@eK1pdmFN4|9zXZ2WlF;zuNVh%f zsT)e?t9BU`EP9x5Iyut)*`~HdIxggeIA>++f!@x^qve*7b9Zd4T6fYn#fZaqZg=n6 zG}t&S-Ctgz{50^a$O+GAK~0lNFsYl^saZAX)bWyAY7jrocB9CqC3Cpeg_sW=OFGqf zvZIr^pKER=&--QW5%P_LMOM=1?4MEt4d;y>>|pjOsP6QTST7y%eDPv3S!zL-pf!m- zdxSGcYOH%*eyIT(VBQ%^$ClX|F*)Wl#Ou3QUC*UwGGDK#h>%;c)VWak5lf+Mao{rl61rUcO^f0D0h(OQ7`D-5ghDz$dS}N>(>x=B7A&a6>hm`K}TuZEA|OK zjy1U||9R252Zs}{y)Forw6-s%yO~;xs`b!W8b=zyAXC9$2X}|R4yL(8s^ZxNZ4dkKd=5{g z+4w`z&FrJ$XP3V8;AomIq`$MiAsfKT?%p8riHr}Lv*q%d7|(I-$Gq(wDt)&6h@t-0 zlB{EQs*f^GO@6+(`>@dz_?p2Ug(TLC4qwb#eAQy^mM81y`DE(e<2AHh?Y$>(97*lk zm%H|E?h(V|hELA74;!>GWQYnXB=*|&k0bHc#S1TPN!buH)!4vf2;cs?$6xSzcy(8O zZpS^jV@=2Ugfl8@+3Ys8;Q-6r`7gIj3X|}!GE5jsZ0qap7wC-d?mJ!R z@u2;P*x}a?BfrknJxmuR<*9CEd$-{Qw=HEC@U}b*d*okbl*m8T&g}^qemUmT=TkW2 zLH`lVVX22!jeE3T&_YOo>hA~KI${K~EZ)vt6|pt!%d~i7gW=Zp`0lHG{Mf@jrQ+=m zj~z=t)P0}y<*B9{Et0%PGmtmX!4RaFS%l_`>se@H&0KM=%SP`tC6k?W_yg&!vwAZ}8r*`3u zH#2{>Z&Wm6QQF#uQ!LA7FI}IwY`M!Tjc2^IT{Adc;|Z9+K2E{9Xy;Rvk@u5k_AWP` z%+R)5tFV$q>o_@j#;SjI^SYj8VXn8d3x}q5H}#(!RT~fPvoDT}rJp%_wmIo&?-Hy& zz0Nj6-iLL%gEmgG;mzK=_T-XBE_1Y=@*R6CIfqA5MwEJb3;SdDpSyfMIfdIZ)94sg z#Wq#`egEUGVhrEr$!wL?2LpFGk+uAWud#x7Yer8C-nJjf`z3bU1;*8bX-%vygG8%??47Gly zaD5=E@2<#s^Y*DJOFZWXxOf=_i6?Ob1{#Lm0mfagUzYtmhHz2q=BW=>gUd``(f#c( z%DBNVY#piBwlk&+mIlq`Gpn`IMpv^>a*PI3`vvujGT9G9FFn5L_ny^ns&~g$-b7C} zizm%nCNs^p!f$ku|C|=b5dEv7y1Z~6Q=mS?>Nr?z|MdRFpkFMXcsC5IthPUfx755k zoXmsmfH2wTtXEUQ&NKchk^kR#;v9n<;lR&&|5^IJ;P|#e_n@&yn3}1PUFVi`^grXO4N0=_HRLq6+t<#O-MO7NZG6=HDKQ-zFTS^b z(ZC&rB^2|ynB^6-aI_QZM~3&X0y)QqPV#oNspo%+J9&Y2sU^v+tzFZQxYMpg?K*!Y zYhcs~+v8QcRIqrBSF!mrDV>3BEG#c+sIqVA=R>b*Z-)G`^d7ZXLfpyb3^^a$EBX_w zX24K+E93Hl-sR6{G!VZiKOGWdDRVRUF9wb^ZTVOld-Y<}RfAWFUA{UO2&%S|RGtnw zv*(E}Hof5EyTowe&#rfjB1Eg%TK!UkRzq8Qu2#-XDZO^^8udTc#Z_KDa=qqNLicnXANyJ>uJZih zIfL`0FIjPXGUxZr$aC;B^pmU{9Og)H-}N`QpQzf9mJv1kQtbU1`P!qC%`cEgOt(nC z;EfM`P1v;>wrrqw(szOgP=ynrD%YZ#7mGO3mK z9c>*wtL$%I=r?PA;PheoC*8dwQtU&$CajG;8=FnKOl~wK^wp4MJ#2eO+f4Fg zdiiE!##&Sd>3NNdzWQ3)8OYxlSk(QRrOz$zJ5=ZLKJY>H<-U8&yx!p^6MLe&xu}Br zFi|i?K^MoFx^@A32HQd1Kvaa+`=QgRuZcC(kx*Ti?sf0!74vw0RkiFRd>`G~B3@<+ z@7T~+SDNDQeu|R|v`?l>nctIo zJ76}nK_!P)Fn426!i<~53kv#dhqgyuj|MWit2z&sk`rUD9lL6p+SGAZub<(=a5kKQ zVX*g)%+r+@8_Bf@d&j-)U50|NQ#(PuGf5lG>s)JxI3+R@$DU zcAUcT(?$uN&lXuM%yT_%xMN(Z_i%UOfI7dT|7(3n*1uL8-rso#tppu<^vhSO@;l9_12I_4i7y2>%8@T5=H(PHNS0o2Uwcm%_q~rIqc+5!Dd--|(=D*^Hgi?k zC~7U%pmq{_^x@cssKh5{N=rwA4A@pQqnEPX zqjsXLn!4lzKJ9)(E_ADfs-2>&5yKG^fdtE;Zg=*H=N}#{jj?_w(jKaC-1L=Mx5jjd z-I&Spxn?#lC;XQC%%xcvnaR!{4IDZ$DB$$B`&8X|r}?O)e{3 z8hJLNpDOu}|%+0q+y^UGQoA9Id$8^In zqXTQlbqp4`u9~Yb`;v2o(K?AVZZ12NC*XzkUaFm&VHsz6gZf}B+m|P4WJ5e=`BLFB zUvp@)>|GnvDNb_;Qx4j@sh=G?%9+IuAE5W=H_t11`Xcu}<<738vM*RUYwM$SnR=JS zt_(V38g=U(EoVl~pgNz&R!WZZ>^NU}R|a?YN!EF1uw$7wETYqMoA`moi-rh(77c{*kk%Q^ z6^qeb$r$m=_pfq#WO`M0cqoId#$okabl$Fs%AT39`fg|p{UdE)RIkVWh0PL;M3H49 zO){nCBoFx+JyULwpJ=&^zvy4gGGO26>u6?_>!!QMG)E04vOl*DE|BR`+$AuIMIZ`9<#XAgk{g>rQ`vXGxuE-pRy|_v#z znC1>jh41-*G*?&4J~?sz2F^QfE%$X#ah+L?O5&3TE8-e5&3bmIMp|9AnyIZVoh_QE zP+_Lv66mYpwV%9H-&N|hAY#yhr^TvlvMV2czw#M5&LeqBndZ=_W`xBV<4LLpQmN8> zElGPzk4T>!=LqWn4UEL(kqrIUG zu@b$>GuES&GHe{B(jk6-Y^`89`(;x{*`>@=soDvuNwqmG?Q5i;>z~E8Yxc+~$<5K= zSux$9$^F_jtQK(c@|J>-p9D1Q#@i%u=x1)XL;m zDCHWQByIJG_PjuIFuSC-Oo}^Z#lOY6+GJlz%cH+FOVW9k|7Kb3^3iqLgJyx)RqDGH z32N2GZInT`)05s1ZWyVlxJZ4(+!O@#J*u}XS(X{~GV$5+M4hY;O%~$qdhw<;`r|6? ziu*Nc%r`K$y5~D@!=E$ER92JRFoxlEb*!q{TXZU$k*=J4GwQ{al1U0Z=SiEYbUXH{)d}Nf z%`d8P+OZbTX)9dRn0Ii;3=>s+CG-VKz2*(R<&*OlWvxjcN!EK&k$0s-T;`pToaGba zXzeqasrm`l`ZTg*GeynD*KoCJl7z_6$quQ{?S%@t>RIYx}$TP&udUNU2nQQd($2QR1aMa^GY1Oq>;A z*i=f>RV7{fne|0SNsl?s<9HK&E9p|6WM@@_p!!3(Xo+L?snkhNZlxrY&K#PjZ$>b| zpEj~oZB&xbQ^B8~Waydg;AE4ov0aqS-rG)Uu=rG2E?%6J(VH~&@y68i6;>l@M!LjO z+!KQv%1mXdK?L!m>k2nI^|k3f<(=buIdYw=8-A%;TpC=!dH*JP&9lgK!DkAl$apL9 zEAFiUPi2jYlc6f9&2@+CJaUxrQN^&aWt{t+{*9DsLP>XS^&7pU%I9hyrZhT9*qf>o zezMUsa8pZDXB&G{q+R7)-Vm#?w-wSyi~F~=y{)@g!6|mn)p)~8G*1%C>S=i=t7~fP5G(>JwEWjuBor{-JR=&&UC+;tsm>w_xgOHvB5GuvLtr;2}=U+X`jqHAqC z!xm!_pu0q8s&O8Ej>9U)l_U#uq6S&)Q|+9mF=pWI8T80;hLu^)t$MY$OZoY(+1uV2z7l!AxzdkqsMKmo zu(@h>Pk*Owz0nceBc_te3VMpQr!G(0Q6SNGq>b+BdF4zwLqR^ z5^UXT=3)3nul)aU^wn`yY~S1GOq`iHXHF+AEg&KY2q-FccV4@@zjj}UTF@NsQ-m}(TYd!1PYp>nmY|5~lnLTQ{R>a2(87SS$G`A`0 ze>|_<|7B4*|3iF{_tV7|pHMPI;QMsr!E!rr4ppyRgum zOH=2X4V4Q^lZsvzCRg@s+voYhdlLVyV{>$$Fmu?qn01}=yDi9G)M-pq4d)x2;9(kf zOU^WmuewpO_S3TWXWy@{3Kb`KQ9c-dJ<-*nU-;XIcX5-_rV)+5yh)i6wXBn17gxJJ zUczh~T~%1Vtz=~3kD@@;R%v>0hUi4X(L{Rm+VI&CqPY0Zf-Iz~B&jK~GiMKIvC}j= zTG!TtRc|V`d^}!gEgDumN8S~nhZiMXP5KnVdD{fm9-eGL~w$yLw*3R;T%y2uSEs#!N zh_L3~wWq7Qe%VyIq$uXYrrNLSBk0uVyPZ-yU5F=lXpahspPhO*J-o9x{$QAg{yy;3 zbXX>9?q55%n)zjT>A9lsAF}E?X@;WJ9Y&>2PF!?P){Q9`Eu;l&PTC;j9x*$5S^TafF$N90c=ml}xle>2Qkg_}OXxJ0R z%Rn#FL#ctu zZ=i!INyck#`C3?0S@ru@gbIt2d-HM8ek5~PY&!jAiS(h;|^M9%0cz*PnnA088I~_{uA2Tw< zfVcaPn!hLvKgNDLSC?G#;8SMNucD7%DDnXGCi;8&q0GC49;d{##AGENPwtgCzr##X zG5$HQz%p66{9oa!&l6?apueIIXPnHMoMuW~7X!zLy*=wNpUq<8F|ODAEpsS@GZ|bF0N09=^gGy zhJ~c^`m>4qWVL*E6pNfrHzo8A<^A6M6^`GA257qScUj%H2nT%S{V{W1S?eNm$O+U@&ne?P3-PYwAx<;g$ds^3AGu*UPJaes_G}7A9M>*X3Bdc1g9z zJEJlpZinvXQ`m0CGU`civA@i^O7^f(^*QOI^xf8Xl+sNNUv*w|d&~vmsfUwU=CstL zUNQF~?}iT*-RGQPmD0N+7fGXBx7CHsNmXM?#}rk(J@Ni@b$`iv-vMFgPRZE@q7~o2 zsaQgIbYj%muArr>1D!bM`GgA|L#_j z-M>qG;ho7tLOpZ2BlQ}sunY<%_lpPS8VX=7pr zWklzW%6XNZns_fFPjsI57i$bN$WYKgN@{ScwMBaC`@kA!#lN3!e_H&xrD4C~T_9f6 zp0c=GukNkgXlX;^Zik-~UgHdAc4qvAFGv0%*SU0>CBI(R9sW{Nwxsmlr;@6&mXC%N zSW0wWdUfu@ya|~flGb;45+dNOVfLgi#T!r%+V1~li6ZLA*M5CfIr`JDk7vuhwYS7K zJkfk}k}JDc{)X;1GNyEtMII4eV?D#4S(HNAmZJ+QpbN7F}pWmHqT z&OsGq&^P*e{_=xtr+cs5w{&Wdmu8H^K5r{@;&b-}0+Ql$Cu9Eo-Yu zZYy_8VvUW5GWT~^cjsh9rC`z5L`&IMX?5sn$|JJcr*V=EPbJEhMfF2J5Bii*Dz13l z@I}=?c88cc{m6NiKdAfUt`9n5QI&$N3@zH9auw+9@8`K_`>CObvsw<+@+-QRmXy+} zGJhJ(rPNi?y)wRa@87dDuUq<}ga_dVx!Kea@KK=J4|;aE9$CWG*W2M{pk_i@6FsQ z%G5@)>L;m8n4Ln(*_!Xk&&}DInj5<)#E(~l3rLrIS=a(u{K{so|umMZws5TWMDvWf;Z@Fa9y&k)xMGNe;%uwP&u?r zRFU@8^Si(81yvo@r%Op*O7B;_FXbHU^x@}ROmRjQ=<9zK zA!QZi-Rr0ll5-}*5nGsfrWe}(K><5EE!hz<-PlGD7^^axey(zrG$(r2ezV-pVdowdnB`yq|NlgrnaKE&UG~F`SSZWQMHO;ci zKkwDXRU9ehR3tYPD1||)Xh`be9wq&$gI@OjU#2SYc*r*Nf^VzivAL%)&a&FdGkwu; z8rQ%4_Qr%kUYPh#canIEQHw;?Rdrns%VHST1_}2B)cG*15yw!fz z-e$a_UMEHyofWQ<4mKl*Xl*o3T%n%MPp?|XxO4m#NDTKdM=V)k0nVQ0DRi}`@1 z%rV*C&DcSGwEg9`sLF!UUZ3_>D_ScoKd^<-{W7@)2L{aRb z_F9dO(Y7Xis*2P$v3`7I>8Cm6b8450`#KKOcg5Y${$HQ{gD&(h%B@WK5`Kqz+-Gp` zZPDfwYo%RoF4nG41b!iPyDB5gPgZ{TCQ=OczTtf9cqKQypLWpqz6ZMLlH8&B807am zLu@Q_f%UrWnbD!{C13Efw6^i{mdc8%gUvw=m$X&@rNrhf>U&@SsW&h4LgFn^f278D z&UwsIZX9UIwHS4%YM1nU%bTxy;vwzdHPm054eP*uAzM4SbH#mj^;@6cCu4BJbipNf zsISQ(H1{>$G!G;yQw)k-;=h{yuCag4{_>@+zU`c)2w_Fkbza~7Vc){OjonRMhR4yk zo532-8QUepWJ7lo!!TK0Dz9!!{+{~vbJgOS)89wQpF5x6?HxXLvG;KIUC~FEOHFfj z*vX`mKe&wM>$+(D5B+Y<6GcC%=GU`^eqT$gx7Ge^DNzmgDwthkCuQXJjOa%%7??v& zt%}%!F9`N<4mI`H4b>z1ed?nMOe*`ivEj#8Tg~hGMXjYeMesQ0F^cDr0~yl+*0 z$4ns6%KrryduCbMwYi!s?Iv}avP3Qtcm5I8xUY_0zq|RF47C4^V&TV#{5+~pcAwJj z7t^a_M{`)g8C z%Z1aR4&{tHB(dY6o}<2@-m4y_oU8Cim$wdT0>9PNo^0s#yIGeOEaGe+ZaW;zZ!6f) z^Fnq-%E7R+)Fb};)aE_UYE@VjzY1%J6X?fR!iE)YAqIC4KuZu;b5vxuT*GXO|zRI7p5Bl+-@mc-TZ+D3GJ$A~m2Vr-T_GFF8 z*XQ5Nv34nr8_Dej#kn6D)~FKIvFfo(qe3sc{rgUHOT&cv;>IIw6}qARIZR7bV<#ZT zmH)E`qg!?7ksVerev|&R&(bz1yQ{mZjwmiEu1G3=KKtJH+xYt4&F`e;=5xRt-q6_G zv=zB{{*k;>nTaWI*lub@AZQt-*`N;5R4eJqN;#u_M9ZMY-Sw_-O~2-;SGY!Dy+YFx z?_?az8`6WBqfWn{VC1hyw)yhStF+15Lz>UZ9OX9IqSg;h?1nA%h2Q@ZA29g+4n}Mw zHMuSGMV>2HoheQ2)ZrU*E_lObHXPOV)xjD}g)2dc_{Yn}DfPGO_qR-xt1TlzH@7yr zCUr%(knWvxlDd9QQisT?tAfjIO}hQs4ce)y3Ce!*$ToTNqlPE-D;sbB_GtRLWtdAu zNtm5JJ9j~zK3mzPDDE~d6#2vV*j%pLqHEAlR40@YY428Y^TmehZ#gX=rOQqE!7_w8l`fw;$eG-AF4+Fw=E4#zm}?g+D-5T z{{Lbaoga1^l^2(Du4`Tr6iUXslj9v%j4KTzbQe?)lp^$CB;(VjUa>&5ae^tvBQxrSIbw3N5gbmXh%s(O(6zc_G z0OxpA|CDQ4j@-{VGqSv$wnlDcb%j5>XP8$Rrs={|#}o%-`ro3Kn8wrf_ZzmfURA$v z&PF#1Q{q;o9qHCLw>}%q_$SdUI*hY`ddFVV5W{zkOp&GdDG{{xZw@rf`S!i(l{nMj z_HCs95wX{n zsy`e*;6=Po9Xu&(Grwi;%etMuwBzZpY}Ofgl_%W#*7!m@Of^KAE_?D@(?aV#JvY#!g6NoasVg)8%x=z{-=#V}Eo2NM1)SpQZFy~o(fp(AqYy~!zdkm{H4bWA z{9}o9v2nKl6D>AmSbSC5(kx}x;;!90jqE_>4ZsotDRvF znx+{)7KoSX`g-=E`}pNimlCQw-iUu6MGsleT8-_4PLq>^Zvu=!xwpdA)0S&Es_ZFF z`*r$9VXIGe!vOofp&Z_(@bc)zF>@n}LwfQ$F!HcHluOVLU=WZ+J{L&v9(PolLN)bL z!S6qQ8Gm1wy)Z<3ry_gVsUeo||H3zmDtLQX=Vwya4_Kwvk5%ZhJ(w+q#?b z6K&sG;r6M@1C|cKNwmwnn9#Pc2cbM+1J}#^L31KY;Avns&JU$|l{a&3Yl zAWjwIa;x^Vg9q%#xx9#wxuGr*UC_kIVD_Z4ka*Y$YzIK_CRr6QdCxl=%tq}Zg+u(M zJyEtrTWoJ5-^Q=-T0**prG?fCO1Tf2PU112y6!EQn5eJl{iu^NgV!lCEjA@cG-p@-wBFdkBCrJdThv@_*? zU4rWgc#baQ2SY}N{SGM*g1lkOyVP08NO&xWfZu>mB!fT8GuZyna8mU_a;5EN`zpCo z-`7opUo-LqrJ;|)93g##x4CvkN9qwI89oXYfDWLTbldNDAGPf<=#_gVbK1VQzmTg8 zGd!y)2`sZPJp5JoxzN$VC)_9oL>-O{g#BPO*c-HvKKfs~qiwJBbCn^I_O?I79hI@B zkKVq>GS*e$h_KyZeL@NZDVzej8eK)ng>u1eU@P#R^uq6PFSCs@*pyEt-P&E^A<6+J zt5<;ZVf7T=2z7*R7G37gV~602&|>%`v=Agi>%cB#N+7}0(UxgwQjU~lwSO0jl$dFN zR}ACK&-_Or#t^+Q$kVfS(T*V7;Ok&CH~|a*MT9hZdH%DVHoR8#mQE74NiHjon|^wh z!Z_m>Pa(PxvP0O+Tf*v38-)xYWbqA{1-1hoQqRD4PrFTIbgJG;7m6QAMiMo{FWdkW zLw9lSi8hA37FP0Bv0T(>ZX30?X^+1EJP{TH|j z-@@H4T18wLAHwIdLvaw@2&aMZKoanqtPdXb7rEcr{xv?;r-a(4MSL z0+%R4v|KQN>!M%BU`jXe3i$X)gW$O@$l#b)hfThBlrm z9}c-B8Y}d1zc6yB2PosgedOijf5_KJeS;Z3lWTy@W!$3~rU2w~6;CxP%P?Oee3_2( zLPQfp3BpahGUiX}Sfm+LkfmfEaGE@a(AaTLl!IeFt-Ytnm)T{Ns&l47_i!*k+s9oi zTqNox{KRW!cBADWY={Xg2A%^4fqUeELA`gBGul$9^C|w2iDloF@y6LM6FEptbngrQm+~7HDi!?};=cS3#o$~eC9Q)p2J9>>(#1nD&(IQGkuoS-bgngptDY{81bU z{V_J1k`7G=HDCff748k~B3JspItonF)h}dyrEXbx4~wCif0w zB6T+51C-!EhynitcZ9BxyZW~~x0>FlugN}1$g)uNFLSJK3+$ykxHkkPf;ap)ZWi-3 zRfY6}6(9vFg4V&^p?BnS{w>aQbC!lIzax=J_o=>`ES^bF2%W>_304Y*@vWS_%!jlm z=mpAW=q3~jABHbNmw-uuZLW6nUz$95mIRa@Qm!*Gels61BTnRu9?J#>a-^vf+Pvy#(7|z0` zQi`FE;3ud73WKfyy@MOv-z{OfR{3hl49RxICw;ba204YwWaskZ1u6V+?jhzW+EkQ9 z83s)T%Rv{|9gF}f0~6h+Eibe*`84qWF;zZD2Rrn^3+P!UpLd+!%$v@ISR@?9B$Thv z2XHhr5xNIX2i66dp8eK~#9T$X-^HteNxEasjeBgiP_b2w$Xl5pU0$Yu|hEKq5XeBrUxEVa` z9qGt4<*5^710_qOLsaWc9`{1<8qLmb;H~EW!+XjZ%H-fzuyAA!?1KhE6~Jh6W8k9a zgZ;PBr~+h3l6)zs(i?ZW_W+NmWcGMoPyQ>OlhekWj5{zP@;^8TZGgrR-g`ulP1q&X z#8b!1PDvg}N2qd4FWvWmqtxl_N4(L3bNpuR9M%V1hovLK;Y9cp^Z+~#3??;rA32Vj zZmJt(za^M#jVjZ0-hBa(Q$Mmt^G^tJ1T@}7)_1%F+k>>h8h9)G7#ab#ktX?~oy*Oe zHUG-nq*|FzHPuw?{ujukJ!4rvGT4i~vjmCMd{p696#{*$idR;jMH zGFet3{ZFCM{cC3jCCCXTl~>I_$Dhv&=Oi)L;56zLB!RLRo<#Vz{p10GGoCs2Hbbg< zzWlcArDB<`-1fo$gp$j+%ss(R5cKC~akE%<+>FgZ_Q3a`L68-Q11P~V?>)zD(?-o$ z#Zj3=enZpO66>7`{e`dN6!G5*)(B4WcCouKUQ?;)QdkYqpibZzz#Dw&yX9;$=W53* z%A`VB4^^VEqw5%X4YiuJnJ*XE1-X2TbAXXSfy4-;^h*Z zqQnsCa+Ck24q~(T6@vbPLf%St4Sg6j1sMY`g#2I)NCh>d;rgY~^mt%#5glsu4W zHJFvc+fnxI8z!FcLjc2&694YNDhNb;f zGUHE|&q#-{f*HJ5gq@wiP7`Ycpc4ej6amMA`QHE82N>=u4H9+xI0;>4Hse_ClOyJp4U_B|qchzywn5@c_K5d^Zo}uvT?>iTg zKVY5MfANci2H{0PE_Wz%8EqI!q4*&V?1IKYLSSLwhKp%Atob3EE1o32N@OjMZ6g8$ zkxtA5#GlbbEMXWgowW$h!kQ^r@LbpcZH8_UT?i}O+14iQX!&^Yp!VI82GuKbAFmIJ zr+4Gt7Q7SI3l8#HSwC<#^-ts<_z;{1-+;KFkfid&+TZ9EieZw??RxPQrPXM1-v+~R zHD{whDV!yo$0u`!5YnHAW)S@nd4%7&33^B#AJ;j?^hq^edb*t z+k}O}J%S`|4RaDc1WQC9%0qY}%!fXa|0K9yv1zqhFKuei6I*0H?J;{%;3bmFr1S0y z(uD`8*DqCZ6c z2zGN1GyMb}8&25{`pcY$O_=&$X?c5y_`9r|?uY$+(1;#nJ?D28m5cI( zJ$MUP$MEUcSV}y66PgaK0&B@y|9`HJ=AIh8bXWR3D7&RG5O{1Wa+kT2KUS0-Vi7j+ znpjtH4%UZq6zUHxfQCRaKpI@<8DM*@BP%|MH@DxD+*Iu|Pw+;-t@LF)mGGyii)aIX z8Jka^fE}kShC&F0Dj-l?24QDj+ZFvN#TD`CwjtuHia28@cMh12_u*y;|0Xp3h_7Hr zF*;Lokpg%cH0l4%1`a20_N{izFjgw>Ncio5n5=N2w@+>#B73bpVJ{B;MsKjr^Gk)7gaQ6%0+CIm z^+YGaG^iDPNc2yAMTP^{UFXdyno8-c_7`n_30Jety3wD32Zw?q2(N#ftoQeEAGGw+K9QBR?`tDTGE_s%kk=0NrH|s)3LL_q;5s4unfNXA z7JL$P15w~@poKgrc!sFT9jU*hU`Tql8^yzvtweXo!5|yIO4Ph2i#7^dd286=gde^` z83i2!Z-Zf=m;5YP?E&p(Jx95a*dN4&AcfHI*7=tFp8AfRF8Cs>629k;lyv%#XI)f`w4?G9@ z3R=O*po**ubn@J>>U4(`UnJwibEPxYv6fcvdiXWHhKmv2`;;(@|0g?=z6?7^*#i9m z&IX;pB63||7Qrl>)0Zj|B}3bb#L>zzMz;GLaFFI@vjt+|Sm6nNG6y2^IS%rUu*)Z4 zBv=5rgBI^z#|oobrIY4}LnP-Fa|~){6WLA8;oK0+7OfJM3Z`;UYXMsDQu{or!(5m2}b9 z>_{`+Q@6_gk<5@jQ%y1X-G76F@Rgi%f=E%kXqX_9D`hlLyCNA70yYA7fWg3iQk*}` zb;C^87R%L=|0H3`NW(H`Zz2n1vrqBqqM@P*!X>uKErC1yO~GLp|yQ`tB8TZO-c{R9!*U5wMzLzDoKYpw@g1Nnp$iPniuo_Uid zR-Pm|DVd>Y)LR^bNHR2yrRU`nyRkx$%^S-y(R!ir@IPP*m_x{944E3BxDQy)YN?8Q zl5j}~!L~lNjR~Y7KNu*lqp(fL72fAHvL51J(ZAp(kOQs<)4*18LqOzl5!^FV*;Q&4 zr^{ArS}bRMXW*W6CCAQB7j74<-TAlalR8s=L=r14 zRHDXY*GKY4>@U_C-e-YTu$zB_^B-e5bq(?hItAVYUx7owQZf?Aau2e+(tcIUm-dpb zQ&99C`=TI*IvHl}zk+rm&l<)(LsT|N5d-7}I3S9bzQ^C7rlmDgbuGyi{$UjNyBx4ne^tq1L z!7=De=AXQB!9L+CK?%Wt6w+|?2Q(YZ1D}I?!C0VsaDc~Swde*b|B$Ydmzyk->R;J^p8pFRdQ2`r#Dzc z`^h%)ZwPM+Z}N*cBN%1WbR+}r3f+a|kP56PA0*JX(bQ2hTz*NCCf%*PWq9EHO&X7N zWmWO&1p|elM6Bq;yg`dVX_RIt6Mh6^a363|@R_^aBGqyfy`&YA-U_eovOP5T4WTpZ zxN8IyBKNt&eaB3~x1b#;h0s)}JKTX_di#?0c(pc?AyJhfL!=w!GqfkHY=1RjXDqIq zFA^RXEZ{}6DfD`*h%y9z4JE)~@L(vFtnj%VsOge=vb>{oq|C3XHnq6V5_vd_J)U18 zfP~%oy*MR|t<;@JG=X+nppis&u?{#D;JGo&RP9Q|4JjtOth{Y#aPA?sphuYBxPt|= z1mpQloF2^CGzAeWQiwHPfo4FPfW%;5&jssef_42V&5{<&%e8s7p8i@miaw7si@!(k zg@1wDnH7tlM!&$LAvz()ZBQ35hSbNq#-4BZS2aO)OOh{}tX7$72(BQUx|t>AKH*pL z(})P;!v|owgwDD^&!IAc>zYYM2sW#nl!bk7w_rBftR2><=Tx`=2oJqjmkVYo^Bk-St+ zDdRo$Dl!!gg=P~>k``iv{$Qe4Zi5VaRe7>MBwc06>d&U*?hv31d&4wy7V-A+-f$K$ zzY@L4e-oV0Jjev~ff1;R?C{-izBX;u)XF`QHPVjCC_|B>WAGHxk&(`c;2q`-wuhE3Zp;N@mC>Xm?q6_-Y{#-^H5D{mvc6&0w#k zr%|<(BIrLbK(K!gp>yB?(p(?x7;el|ljRkX2I+cLlxdf%H#q@oXUyOX=81UoIHw5q z?<#r|9t(kx0vZNSfam}zFwHf<{97ZIcae&u*A&O}#9g9*pK_XhneF8E;dSOdVhyBM zVpWtk1g`x7{R>m!Ens1=!h_gS^?Q{aWnU#%l1GTB%}#hr#QQ~Ozv-1BfSUp z9-@LWpxsasoD7>ml)Tti?l@q)sU9aMNo$Dgc$3lV43fT}uNccYQf?e?Hs>Dm8|@JK zKgz#Q4EzE{DFtvP@FI}r9&0&9Fgz=ygQP}9lKww?hrl?>GkhYu8@D6R%PC~_q`R;# z#0k+mXe-R3_+Sn+hjiHc-R9JfQ!SJAm+q5CXydKVy?M|P+5^^mu9i2IH=cttS+ss= z9q~{9gE^ELlrC@@P~vC0m}ZjpPem8mY1tjsIO7CGpAC1z8(BWiH*PKGGHWJ%CUG96 zg7e_za5LNrPk{=^k9|)a^N4cOBl6GEzVaTLL*_J(6qrD*V}9av;oab7a^jd?+Gs2b z`Gc~GB0#z$b1Boob-{D)pO&7weah#uNO_fNlySRrM^K19p+95a=SsNkoExkG^j*|s zw2`upQbg&B9Hw}|O{CM_R@)lGXH|qeN;XK*q&;WVd+$LDXe#Cd&I0ajj+oU-KS&*n zl88K)xc@|P!A3BaO!q%=4lpaV&C0&={_-y>+-P^44|YMm;EPx}oXec|?B`4!{tGKa zI#N{dd&*ekCS?%RLgIU;+lutHscCYh!{c}?C;nWke{H+v_5C#W&ZXigjFUrtZ9jB%4T58I2Z zqP(U&L>416DQO@UY<88JA8H3Gf5|4wY3dioYfceqFp@}5X9d`=*>$WM#t+&x>=yEi z@;7A{Qh-Jw_0R`WoyTd-)TgM36I@xevWI@Q&E>lb324Ka>)AipN7$E`a=Z-#(0ob& z-blHG%tmIxJps(mc4nKZG))RnK3Gw#IcW~MOUU6^8Y7WCmD81z%l^a|h+iPyR0u&v zBWKWc=s@HSG?+BbGuZlDH$k;q{y_d&MKca?_7B!juHZdbHufmaH8z=*OGsotIuD5^ zq)~^yLL4v+_{SIPSZYkuEL5b(gT!gpf7ZR;W-y;hWd>ME_FwGNEIwl@Z8)|Z`9T?i z$dQZ40!nZ2NFdr(V4kIQC_?310}V>m7P()xTsch-*;D<5?NcL|HLTO@+3b4eN&0uH3*C$CL(=;f*P6GQ5qXYgD*l3ck;)o&xW0jIe|)eBJC^VPnL`|h1E!`F^S4WpA*(O0+Ar8 zh#3lyguX8J#fCI>k$i`&mtunUq9x#=10`q)-h&y#qOnq$SMW#F8E8jj1LYfKA2J1b z3LgQu#2U?}1nmT6v^-5g*GNsLU2{lWWIdk1#90ej51CxX6WV#KE7}{mhxA9UqZ^RN z&^}VT2eVz*L+Td!CHZ{SL_@lx!k+*aQwK6!%y+DztOmv&9Ho|_-H-_6D$;o;l!vV=x%fm(g$LY z-nvt*S^5f9AB9IgOdW2_c9i=kLaEg5^vTQuW@ly@y^2 z>+Tq4+^adKxFk#274rHQx8>86vvfwwFHbxmLMP(^#xRzIsbaL?hpFx8VdNnt8F`BwLrm~r!1%x; z*KEsk-4+$DIHg#ojy7(0kOBqJ6>JyYhtb6NA7cgmEbTrv7#)JlM@kVdav7NeVPu)N zjHpH^)!@oP`6*?a_MqjmyPkwo-cU!;XE8FE5{93sQ{I6Ekz2?~BoF-)iKR>dRQ^q_ zfcc4TimJb2m!ejE-}u?l>`#SEsE($h$1#2~*3;?uQ0i+!?h^*893 z`YG!73Y$W$?r3bXzxCY#<{@?}nV!f{F$OcP;T+l?ED=S~08)jNQp{jJX{mRK{k*YK zb4E#5bXGaE3oQNI--7?aC$I~6BmExZ9^(`}1uvxb$F`vJ(HQhB!i8gijsD***!swD zS`(pSslqe{!(=<`vyu&zebjoK!eBBXhLGNqwhLQ;HXtUX7qXAi1Og_=@YPN3dIH8p=htB3s}bpiAHnx7q45lxdzT zyDM|ltMzd<)Egizp#an!cn$p)@o(AmWPBO*62`?oqu0P#$*nDHCIc-{r>1+Ke0+PWHLnkwZ& z>rM|&v)XP{b`T>q1ONe}Vp}9nNN$pcEA_$Vr zmSlIY;77=YnrUb0;mo7V7mOS9@wk}Ui}<%@Y!t!AtRrQe_ppl%?f--Kmie^agle+16E0k- zfm`5EG?PZAe`D-t(wTSZ>G%d}J64J5uv+W|LOe!5D)daTB^blBJQeX=lRC!uKYOdU zkbIT$9Xp6C=?#qQjDGZ0G&5#F>reqU1q&c7xRPY`HrPj)=4vBVbqc+5tZu5M*)=O@ zA|&#Nwut_cQOMXu|4LI~uhD8`1iA>dAa~((qWb_s)B@(~-9%OH9OYY0KU0xos$U3h zK*m#V;hh*241lo>drJ{4t&nOFvKmx?s#{0Orr{OQCLhJ!jOJJrEXeu%h+C!3g%k6zl7qxuVQ^g?FCf#UD7q=#`1j<1R zsb6spgUo0o>}&_tLil4b@*C-gaNvDpt8c7xkNLK4k7|Kpit@B(sLA4p@>c@+NC)Z( z+)Ka2=uQ7bTS2{r>Ir0#h>jyN=VZ_jxa=;mmK!>25|w1-0QE3~!}iE?inI}ajKGkwYbQGUYtwmoV^++dl7-}I{=R(qVuf<+%(rAyVN|ju7oPMu$yL)zU5_AUjQ#0vv z7#Ks3adT}?4C^(asvF87ngPZbyUkNh`U6I>{9vmZ_rD#kx(F0-~>36gUk@rv8tJ8W6*Z@1@01 z)#w@Y0Xi7FfnK4Ipmzjo|IXIm$k6_&(y2CUU8cK^@xB{m6FeW=MC(SZaXkG2;ftqW z>(L|V53~urNUU)c`I#@t*=#P>Jykzcm20eqzik^m&B1ZdFtmY)H9W>K`V{;Kbvm{O zor-QjnFLl|MBJ}Q4!n2uwk8^~H7`{Q5T(_1mwTyxKkL-s&t}z|`)Tv*<%{E42)xU`^;i zY&NQ-BtV}@N4>Kh6r!@aR{c@kS%;WsIa<8WNM7gxI*E3GKo-UHS$Id99+RLNv^T~l zsvL&GD3Mn_b%`xq40AQd)ho2~ja9Zqo@2pGVvP@|mAHfMraSOQv{lqA*Z?dI>qOvk zH&I)e9K<|}Z99lH?owaX?9o5AOmqF?&jA)u7GNc`<#atArTb`usN0Bnq$n3#MARSY zz*VGO-qDU*rUKneb%y$Z_MXXUALH#vS^+&k?o*H982Tt2N%#URIyggcvSadbs}fX9KlxkiZobV$=$JFl`PIDSn~}*nb$|?U4DeujaV!Pji9eqnAudgN7kqYycifuffmIc2U!@>*!{*6Ja@{kTm!ic@crM z3ak?hL$uB6=h_9vG+S?Xd>{*;QHEkuX%)BwpGLe)C!h|*yl4S7pTJuoM8#fKaI+`X z-qAEq*F%%34eI~1>~bFPy&`RbE+DnoTiROu6D@~Er}n^bV%&J_ISLbNw39aYCOEz3 zc0EsP)(G`I%w~JCw=8%Egppm?AzCs14>9f+^%k}s`-kxR>o6F-2(JLxffNE&q!|I- z6%9?-U_5R+>&_2c02<&Tw3xaCUxSy>q*N_2*I+CMi^CGo2+AOEc<_$rs{Ng*hkm&> zUl(J1X+7>r^WPx1K#fQ<_KemU55ZyDX6jzTUe;sfgk85#CP4KhrMJ~lYaXv>XnSbS z>8s7vj=o-Z&;a@8|LIv;${14g@>QC$f7D??%V5UYQ2P*;}1b=%j*}s|g>7`n)E}Q7I zH{Bf>u#)e<)6kF9Y4{xc2kjEyt7bwY2lpvBZcx>KJMl>;&S3-jLJ%i(QyC!q{6EuHzbbmIIDh zZ%*(#kVBb^9--c+&80DD!>JPNEiv0aSQfUEu(Wl=pN(_>VS8z`=z8i->$Rr!_Hwr< zAOgC>Mub6KN1IRkPK}|y$ChIcv8&iP%#EZ`9)rH1khm!&F%LAn*N)S5G48Xzbh3SA z!NuSZN;7Ju?xn@jMASd97ibOP9c8GQ81;jwlT7vZa(A^&G)>ko(Zv~#n2+1LdZ+;n znFgOj;;3}mG1^t?6U;&Qf;N@IphupDwgu*-3{ zy99T)K;Uq_;0}ibxCDZGaEG7)f^!6S3GU9_?v8u^i~Qf`dB6AlzMZG{cIURHr>m=~ ztA5qpsNn$ov{8WJZB#l>+Ukm0_#kT{gL znS!XC+tIiv$Aqh=*GsPx{#tpU4{$#Dm6_38Uoit8mikD76pJ5=`@{p{FXAPkG;cAF zP^`DzRF#ZyhjjDvxQsW^O`2ri_BK+h+2+D(+*~>+)scqaJouS-T>Kz55s&Z}*m(M= z-^<>oO^=qzc=Wkmdcnv6<+}0Q`HLv@Hck{1@k`tUN5pwzeX*k$D?S(A^DVijbVbzA z%`iGC-NRec3QA%&b905&;$HEUI9$vo&KEa`XT?OZjo@(E8JSFS zZt8>OY2h9jWx}Tz{&2~QjEh@rJ;+Khq@p_zuQ@{(HfMq-$9~NKp>)90WASanE z)h?0c8M897M)t|7Hpz0_iD(GZfnOnR1n+O6)Dk3Xk~C7vFV(`fFqzv=@9=BdUA5&= zDm*vid3c~)PG_yzZb$Nz`jOqv-xMn1M%V_aDkx#kY57M({6o_P3I_Np=#olbR zoXOzU2-n5=xES2wP^p8|Mfz4+3(;>~R!7_2kokwwBeEf5RmS+pV5Oo?+gm)EQkjGN zP*IXDOVtB)0--=r(9|n1{$3aQaNFqC{x++j)-oE+=$J7rY(;Igruo#VNjlOIwxV!K z+=F{a^QG<5R%wa!y>tUt5nJ&Cm}_LDvrFG4p9-gCtO_rS_EX;)oYTR7OwDJP@^i%H zxPWv@q5`_~b>NdUL~=#Kj|7bGvisE7sceh9&gdR)94(->HRjnB{4vy8_A$RzOv8Po zOVT|l9S%$CDow-vg+d%p-||{n=ha5hZ^C)Qvm#Nsyx!Bg;C><%nFHJ%AsM%nT&X~y zWnf4kXJDlyNeN1Iw$=hdD3g($}lN6 zej#)NufL9ez{;x?k&8smg=3V??6W2PM}2KmUI>T z$J6`{rUJwm9gK5I$7sJu$!LD%5AB`lI}^ze`aIiESRkInje!@H1I+^;rK8eu+)v!a z$Fb{BJ@+x-v?WC4d z)2OJ_i4+Zw34afLUEi2!|Ke>#U6>TEfzU!Mgnxk;t-ACE*OE-UR(uP%_;S?OEo{cB zYa(BVjf|@iNx7n>T8-S};}BD6+Vb}<6P2iKn}79UD&OV0jzHo({@MK zg~x@LMBd4NYWvK`PLQmjrm|c4sPGj|#aD1nsV!Ka>e4uf3jg3bz$~@-c2j+*oIN6i zdqgVBv$PgwA@^^;Cq0k7#7_`g;xHDZF`#3+!D6LAEDsy)F;|K0BtxJ59a$4r!-s*7 zV~xFbyq}KJnbN#3R|z7*aF_xM!)0l445y-gO= zJXcT{F80G6rDf70pj!pjF*hC~EZ|nrEV*aT*EuCQVuag9n#!tr$|&wE@dr~G%mn_l zP~>0sw1Ct?x&zq8dYE}#f{7!e9M))}bd2N>tNPQxqk zYn%>N@)d4~UkNGT;bx2hIB@?|w2dkmv+XP1TeOF{!>tFTekqKG1*Lpa2<&(%yj0lA z9icaq;m&xYf#OGogy)6rNOgtQ*H~xVyyzUAgS*3z6Ek58SCwi>gTcd^hRcX1m&(MV zZ`?b^6D1lMAJ#KQM^?*qwO!^dryeOv|H#hZFAJ~58TcH&hwtD&aaq8R=kZI~-qcZV zp*2)1Dd&&u3D1eFk?*MO&0Nk+zY5iY)w!Ku`Mco7_;4`0^tSdHPe&?Koj)f$6Mw;Vz&jA2?7>CxY+)4lj&4m79Au1BRz(uR zi^992*A>=iXrF-5Vm`BuJ1c17BkW)W^ko5fS*Ey1n9daj8UE~iG%~A2q8Gwf;pxy5 z6;ZEcU3JfrZ8Tx;@VUj-fbgVa7k|WOa3VHg4)r~zDVpZqGApb7qZ`6Y!>N%n$}0_9 z)7*_@I^B(1E6fp7{^8;cJPvw$B%UfFVLE%9lD(_eOs$k`hS!7>BNgTC>M^5~v)|80 zuVW>ll6VLkc!E?^sw{l~`T7Q*6n^FA(aGejZRkWk6S*9o8>u9h*EqARv)q40&1A3e z6Tu36!cRdzuR`nQVFsTNUU4w1g2cIx45Utl73_l|H=`ZZ^ZHSHwU-yh!7cn)5lQc) z8iD1~c_~F23w|BOS%vCsWh%kzW3AAV+(Z zE4Wg~6z7VuLmemU(cbbdrK&#G%;&!H_t6HsR1jgLm@gfG!b|6XhfN?$R+`&L7a=Lm zUSq8Kv)nOyHd;oB(_+ofc3*!RjGfPUOKbzF+NK_XY&a)@mQ%&U}~UBzyy!y6U16?gcQNRE;z8+x@+n=noh>+lc|N0l5QrKsSHE2XJ@svrvR@&HRcsxOdEBS{J2>{4jc0 zUZYksn%i%@9@KWWweXenHZVH)V{lF2sx%gF5ZlB3^LZ>5KW9Ew%IBT4Mo8*3*glYkj|vG~B0a;u>tr`qXjhdfa&ftod|!^J^i(w5+b(CyH6 zz+CQ0_rytnER;k8T*ex&msKexkK9+@Cda5Tx^MpGmO^8gGkiTfIWRu-BGepkvbw?T z(k+-DSTtGJSbH^yp z3)oY2Qz7Cj)>Rg&Bi1``qv;H!ahpmYX1K$BzSv!9~GB!OVeI_=?b$%R>_{r#(SGtgMO3 zk-3rBXe3%m)%4mn?dPRBvGKxMd>^pTgwXR~9>|5okQ{%n@guE_bK%OXEV znntrIJ2ceh814c_8f#)(CA0KsW$EeqXsbfg0_X$8Y)7ybC><&m>KkkmC?ahWukia=oBHGr zaITqWbz0MuX39~yp)y##p)aw%_7pUhJt2HA?FjA)Ee@>-CIwu`wVdNCvcJIUpx(|S zbDus-o1~sm)+!5>B3ex&hkeMaMukC=21$8?OlWU#8lY1&L|yy26y`A6;ytq`n-laV z+CsIZdR3{R&VVtPhKFhupjU9U#F)U0U+*;77S6!1l()qG2(_)!GwFApG_C@hPS>LlNp=N$u!)Q#`dYoeeo3!vTro>J)%;mhGqxvxM%V+0*#O)R z?-yqYUHKv0BW5)H0(J8nyLs%*un&G6qo7g17-SSRk)6#=B4g>l*dhFEp}aU3(6%vR zCXnA_+#t3llbb$74tQCePiC5NS|6-e)br}8`XwW$J<+{R`qA&%D*O;(3iwM;#IfQ$ z;WR&i`-u%QQ6!SpZYO)3S=ory-)j4{*V+|*gn88N>ZPD4U6I=avoN|t2Jf0!OWZE> z;v=x8E(cSaVn{jHx5}8e^_N;%t%bH$Q+3(Q<(gneX?8UKR@fw-z>{!KJX1UY^NRBF z@qqVtrF!`-T+^xuviwl{K^vmg)pfut1TPNVqVsUa_{qS@W%x(Hm#P8XdVV9_ytE=^k<~3)dUz2*lq;a=}6XGP?3lPZ7fb%UBR`5|aj+u#y`rVwD zreyTc4yZ`2sCLq1eUvrQEl6%sKGTVpAUksi&%nj-0kO7NN65xcX5UfE$rE>^UCgYa zKT)%&;}l2fu1z$u+OB(*ECAmyhrr{#ka?W~8HUYbRk5$onyt`Ru!X{mae=7 zKPq0CsovKwTa<@UAx7syqK0pRZ@vofvqZo-eiJm1me*81QpNqza`aqa`vqmY++NwG zp3sA~Jhz& z)xkAMKIRSgNa%-)N+-cb?=5w}H6S)R2dhGoXpJO$JMEv$O8RS+Q%@)r)T!#<+FGNU zy~w+RbS59aR$%ZV{02XRJ{%}E5^PwT_XD$!(#au@a!m7$eo$+p+3F;%fS%PTVcl|8 z`zqurBq3Ss34Qnkp9i$IyI5T)$(QD`u$SqQpz({{pX_z!SYwUeU8jvIMt5_ib;;@H z!+tMpJ$|RK6*7nv?g~isSKvSI;V;2jr87)3nnKsSIZhMnsgd1StMAaO7>;3?b)8Rc zZd8@t#}?)<@wJ7?Fdyfr&`fanHvA1(XFZ7x(};TNA9Z)zPeJpufEE4D*kDXE=UE4x zeO@B!L7!z_u;;lvd{&{ckSt6Ph6+)>7ypWD${v9gzpec^m$L_&-x#Cy+4?R0v(d}k zWF+8z%p*F@uZOiTQ|PH^iQmp0YIig18s+s$`UE|b zvC3F$y4D`2o<9z4qJLqJaWO(G@oR{Zhv9V)uU+Ohzzpn?uvf`0zo$FgE@iIJ+h`TF z5n5PVrS~;Ho2Tve-fmr%sU3-hQXQ)d}j| zqor%Z^eo0@BeV71?&KXHeds&PQ0@aSh_}F+7sagriSH|1d| zx6DK%O{=Rt&?f7|h_~|FX-+wh_^Z)Z^ijrveqJC{g{)%@$g&lLnPoS*U)h4pMCvMO z>(6%&+Dpyd!0mf_O|a|F%u&`1`?GVwn@UDf`{)kr-`qC>C-wqslmPR@Zty&Rl@-~a zVU2qwG}KRZ8{7AcoW=o=5NxKK6Rp-lHg zQ8tZfNBiib&wFE_uUX@fUe>s8yf#N${p?N746iB4L*1q;v!`Gzn<+-cs}TDa#iPUz z!bd)qUkp13l%p1t$snOEtv`%w`X)WQaln{j7Piu@c;{EIFiE8p`Wnmdu|j6B!_UQf zB4mdIgX_f&VQzbKie2VB>jjm=N>s$Mjv(&4FmQkt9S(fK>3;7^J zGeYbm&Jlc&(z{$dHwRWh?M6As8n3&P8T<=Xzo5hO?aYX|+X_0ryIcJp)M>gq+l_0; zcM|f#%)iOvCt)=7ctd_XH)MlNHZ@xmx(_A^^s&+W}_I}%Ub zrA=lKSAUvxz_mW$_UVXV+jxX6#-)40(vZcS%0(Hbg7{`NY#9qn7@TceOs*qCPAG}@Y$ z+1bwS-u2Fr6sj1LmtDn`=F1B?gp$HZJ~yAiP2%RUQy4^>=m=To7xHd6d#v|nF0+Ao z*6d{Mvzpl*oNaD-AEVCHV)`Xhn7hNZ=hOJT{8GLZ-+*t&#c?L{p1x1LLwCtF-*IO< zF*ddqnvYG^`o+p;zqLO(*jwh`Ba_iOYA9Wc3BZ~K16optOX4bkec8%xVxKYfnZxuc zsv@dPrg*!Yh4w2;w_aK~ogNPB9(CJ#IsGkuZo;F{Xf`#LPGK^!i`g)ni7N~HEPTuT z#rA}qKia{%`07+%7*}t&Gn{05uszlO%O2}YceXff-L-CguZ(}ge?V@d{PZLGI+KsZ zY;CqUTb9kjK4m5{ztJ?EiOK^q{JS^F9pT&p`V+Qnk8mzI3GN)Xr{4dp;1&9 znqhLnTA;~H3#KyT(rxJvRDWtMYKn%Cn|^cujJM2N?;Y~ict^ZbUO~UJzte9`Hjp4X zizZW(sS-fZpYBLcq)eUjMm& z&A0s5zUCYLJwG$K?-wHFNKcZV93&S>2KgHCC=TU9|0_{Uj5u(~sACN#@N57(h zC=Ys0?vVbZE%E(VennD^tRuI`cW5>`f=rZ^DnbRR0M(4@Ld~RBQe)s#0#yj;&!J&R zKp)8#l0+C1LrO!fBgu3klTRcGO+crRjgF!Ps1urp#-TlE1YGe4^erlj%AtZtLMB;F zhLiTBH5o;c$uY7SN;+wPet~)mLv1afp2E~!xZg#nA1VngxCZxmliVfm$Z2wb>>~@v zda|1=BOA#Xk{Nx2zC*3i7&HrN-2`Pj`V9?0ZBb#6K!=@V5x? zAP2IEi0Yy0s2!>WZ7c-WDgjrFM_&PbL7*xRr5x0i8|6R%fdch0IQEboWE%A4bZE~e zxSk1jSsi{i47ETV;h2j00zbN=8bHaT$K)P41T9_!?<>hs;J^v8pWG+UfUZ2$oP=he zkth|Vpky>1+WtMNgsMS(5^~80;L&EXl*|C?m1G-8P#W;^1@Vc5$^hru0c8@J3CC14 z3To_vnxO=^bA&8t`3fA)O!{1$SuNTQf@)>BeLrcp6N1DMEdIJ|*qc*52sse5O z3b;#%MLt3u_n^-%kgHH`!Ka7h5qSwc{RaN)5~u)P91?*~61)cy2A&IWAml*5nM8$} zqC_J)@&5Vl0v!x^g#3#ZA#l;*p#HfM_0Qk`|M{O2|KqJ?6NZ)k`95~i#-?kdDZ3MB zguWCG&X$QE{r#xc-!*9P4V>suyM6t>gOdwnfq#6tWY&@h6>5*5nk?vt?~}i0;Wu{? z!yFezgu*%6y^r}zD48z~*y1fHGCyNk<%92^ybP|cQm^N*Q3uDa@Ar58q+E?(4>~w*;nS%D zXAWPRdZD2EGGSu3XJcPat~h*KvqSk{1B^fBEn(*LU(jN=_MmGJRcqNJJTzDT_RXcZ z;c}<6qz#9D9kblr*YDv5oLXUEhvI$L_xQ7c7JETvE@`{2t&iQ2crNqb-1pwlkH6CkbC)eKCSiNYYWY?12j%38 zKkn=Ic0S7zzDgC(-Zge>;YP7LvhQJ9E5qI%efH>i*$-FrIQ~&~IbY-08F?;doS}Pd9uXL%ild`^#A}u@}-YU-p%;%JZzbFXfe<@%fhS=GF8O6 z>9NjgeTn)+yKlvkRG8+NOZrJ{$A0g(Y(miz2rdHA{UTKF58G`kun3*R2t@h6U&BTW z9nhzDa{PdiLx=pE68%HD_hz { + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Evaluate playground data and exit + vm.on('playgroundData', e => { + const threads = JSON.parse(e.threads); + t.equal(threads.length, 0); + + const stage = vm.runtime.targets[0]; + const target = vm.runtime.targets[1]; + + const stageComments = Object.values(stage.comments); + + // Stage has 1 comment, and it is minimized. + t.equal(stageComments.length, 1); + t.equal(stageComments[0].minimized, true); + t.equal(stageComments[0].text, 'A minimized stage comment.'); + // The stage comment is a workspace comment + t.equal(stageComments[0].blockId, null); + + // Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments + const targetComments = Object.values(target.comments); + t.equal(targetComments.length, 6); + const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null); + t.equal(spriteWorkspaceComments.length, 1); + t.equal(spriteWorkspaceComments[0].minimized, false); + t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.'); + + // Test the sprite block comments + const blockComments = targetComments.filter(comment => !!comment.blockId); + t.equal(blockComments.length, 5); + + t.equal(blockComments[0].minimized, true); + t.equal(blockComments[0].text, '1. Green Flag Comment.'); + const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId); + t.equal(greenFlagBlock.comment, blockComments[0].id); + t.equal(greenFlagBlock.opcode, 'event_whenflagclicked'); + + t.equal(blockComments[1].minimized, true); + t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.'); + const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId); + t.equal(turnRightBlock.comment, blockComments[1].id); + t.equal(turnRightBlock.opcode, 'motion_turnright'); + + t.equal(blockComments[2].minimized, false); + t.equal(blockComments[2].text, '3. Comment for a loop.'); + const repeatBlock = target.blocks.getBlock(blockComments[2].blockId); + t.equal(repeatBlock.comment, blockComments[2].id); + t.equal(repeatBlock.opcode, 'control_repeat'); + + t.equal(blockComments[3].minimized, false); + t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.'); + const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId); + t.equal(changeColorBlock.comment, blockComments[3].id); + t.equal(changeColorBlock.opcode, 'looks_changeeffectby'); + + t.equal(blockComments[4].minimized, false); + t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.'); + const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId); + t.equal(stopAllBlock.comment, blockComments[4].id); + t.equal(stopAllBlock.opcode, 'control_stop'); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + vm.greenFlag(); + setTimeout(() => { + vm.getPlaygroundData(); + vm.stopAll(); + }, 100); + }); + }); +}); diff --git a/test/integration/import_sb2.js b/test/integration/import_sb2.js index 905ce8fe0..fb6fe8508 100644 --- a/test/integration/import_sb2.js +++ b/test/integration/import_sb2.js @@ -30,6 +30,7 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); + t.type(targets[0].comments, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -40,6 +41,7 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); + t.type(targets[1].comments, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0); diff --git a/test/unit/engine_target.js b/test/unit/engine_target.js index df327f698..f6bb8df28 100644 --- a/test/unit/engine_target.js +++ b/test/unit/engine_target.js @@ -12,6 +12,7 @@ test('spec', t => { t.type(target.id, 'string'); t.type(target.blocks, 'object'); t.type(target.variables, 'object'); + t.type(target.comments, 'object'); t.type(target._customState, 'object'); t.type(target.createVariable, 'function'); diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js index e20834314..2901eb661 100644 --- a/test/unit/serialization_sb2.js +++ b/test/unit/serialization_sb2.js @@ -29,6 +29,7 @@ test('default', t => { t.type(targets[0].id, 'string'); t.type(targets[0].blocks, 'object'); t.type(targets[0].variables, 'object'); + t.type(targets[0].comments, 'object'); t.equal(targets[0].isOriginal, true); t.equal(targets[0].currentCostume, 0); @@ -39,6 +40,7 @@ test('default', t => { t.type(targets[1].id, 'string'); t.type(targets[1].blocks, 'object'); t.type(targets[1].variables, 'object'); + t.type(targets[1].comments, 'object'); t.equal(targets[1].isOriginal, true); t.equal(targets[1].currentCostume, 0);