Merge pull request from scratchfoundation/gonfunko-style

style: copy style changes from gonfunko/modern-blockly
This commit is contained in:
Christopher Willis-Ford 2024-10-11 13:41:28 -07:00 committed by GitHub
commit afa7acc4a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 693 additions and 284 deletions

View file

@ -45,7 +45,10 @@ class Blocks {
* @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}} * @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}}
* @private * @private
*/ */
Object.defineProperty(this, '_cache', {writable: true, enumerable: false}); Object.defineProperty(this, '_cache', {
writable: true,
enumerable: false
});
this._cache = { this._cache = {
/** /**
* Cache block inputs by block id * Cache block inputs by block id
@ -129,7 +132,7 @@ class Blocks {
*/ */
getNextBlock (id) { getNextBlock (id) {
const block = this._blocks[id]; const block = this._blocks[id];
return (typeof block === 'undefined') ? null : block.next; return typeof block === 'undefined' ? null : block.next;
} }
/** /**
@ -150,7 +153,7 @@ class Blocks {
// Empty C-block? // Empty C-block?
const input = block.inputs[inputName]; const input = block.inputs[inputName];
return (typeof input === 'undefined') ? null : input.block; return typeof input === 'undefined' ? null : input.block;
} }
/** /**
@ -159,7 +162,7 @@ class Blocks {
* @return {?string} the opcode corresponding to that block * @return {?string} the opcode corresponding to that block
*/ */
getOpcode (block) { getOpcode (block) {
return (typeof block === 'undefined') ? null : block.opcode; return typeof block === 'undefined' ? null : block.opcode;
} }
/** /**
@ -168,7 +171,7 @@ class Blocks {
* @return {?object} All fields and their values. * @return {?object} All fields and their values.
*/ */
getFields (block) { getFields (block) {
return (typeof block === 'undefined') ? null : block.fields; return typeof block === 'undefined' ? null : block.fields;
} }
/** /**
@ -186,8 +189,10 @@ class Blocks {
inputs = {}; inputs = {};
for (const input in block.inputs) { for (const input in block.inputs) {
// Ignore blocks prefixed with branch prefix. // Ignore blocks prefixed with branch prefix.
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !== if (
Blocks.BRANCH_INPUT_PREFIX) { input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
Blocks.BRANCH_INPUT_PREFIX
) {
inputs[input] = block.inputs[input]; inputs[input] = block.inputs[input];
} }
} }
@ -202,7 +207,7 @@ class Blocks {
* @return {?object} Mutation for the block. * @return {?object} Mutation for the block.
*/ */
getMutation (block) { getMutation (block) {
return (typeof block === 'undefined') ? null : block.mutation; return typeof block === 'undefined' ? null : block.mutation;
} }
/** /**
@ -231,7 +236,9 @@ class Blocks {
} }
for (const id in this._blocks) { for (const id in this._blocks) {
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue; if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) {
continue;
}
const block = this._blocks[id]; const block = this._blocks[id];
if (block.opcode === 'procedures_definition') { if (block.opcode === 'procedures_definition') {
const internal = this._getCustomBlockInternal(block); const internal = this._getCustomBlockInternal(block);
@ -267,10 +274,14 @@ class Blocks {
} }
for (const id in this._blocks) { for (const id in this._blocks) {
if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue; if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) {
continue;
}
const block = this._blocks[id]; const block = this._blocks[id];
if (block.opcode === 'procedures_prototype' && if (
block.mutation.proccode === name) { block.opcode === 'procedures_prototype' &&
block.mutation.proccode === name
) {
const names = JSON.parse(block.mutation.argumentnames); const names = JSON.parse(block.mutation.argumentnames);
const ids = JSON.parse(block.mutation.argumentids); const ids = JSON.parse(block.mutation.argumentids);
const defaults = JSON.parse(block.mutation.argumentdefaults); const defaults = JSON.parse(block.mutation.argumentdefaults);
@ -301,8 +312,11 @@ class Blocks {
blocklyListen (e) { blocklyListen (e) {
// Validate event // Validate event
if (typeof e !== 'object') return; if (typeof e !== 'object') return;
if (typeof e.blockId !== 'string' && typeof e.varId !== 'string' && if (
typeof e.commentId !== 'string') { typeof e.blockId !== 'string' &&
typeof e.varId !== 'string' &&
typeof e.commentId !== 'string'
) {
return; return;
} }
const stage = this.runtime.getTargetForStage(); const stage = this.runtime.getTargetForStage();
@ -357,8 +371,13 @@ class Blocks {
case 'delete': case 'delete':
// Don't accept delete events for missing blocks, // Don't accept delete events for missing blocks,
// or shadow blocks being obscured. // or shadow blocks being obscured.
if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId) || if (
this._blocks[e.blockId].shadow) { !Object.prototype.hasOwnProperty.call(
this._blocks,
e.blockId
) ||
this._blocks[e.blockId].shadow
) {
return; return;
} }
// Inform any runtime to forget about glows on this script. // Inform any runtime to forget about glows on this script.
@ -375,9 +394,18 @@ class Blocks {
// into a state where a local var was requested for the stage, // into a state where a local var was requested for the stage,
// create a stage (global) var after checking for name conflicts // create a stage (global) var after checking for name conflicts
// on all the sprites. // on all the sprites.
if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) { if (
e.isLocal &&
editingTarget &&
!editingTarget.isStage &&
!e.isCloud
) {
if (!editingTarget.lookupVariableById(e.varId)) { if (!editingTarget.lookupVariableById(e.varId)) {
editingTarget.createVariable(e.varId, e.varName, e.varType); editingTarget.createVariable(
e.varId,
e.varName,
e.varType
);
this.emitProjectChanged(); this.emitProjectChanged();
} }
} else { } else {
@ -386,23 +414,45 @@ class Blocks {
return; return;
} }
// Check for name conflicts in all of the targets // Check for name conflicts in all of the targets
const allTargets = this.runtime.targets.filter(t => t.isOriginal); const allTargets = this.runtime.targets.filter(
t => t.isOriginal
);
for (const target of allTargets) { for (const target of allTargets) {
if (target.lookupVariableByNameAndType(e.varName, e.varType, true)) { if (
target.lookupVariableByNameAndType(
e.varName,
e.varType,
true
)
) {
return; return;
} }
} }
stage.createVariable(e.varId, e.varName, e.varType, e.isCloud); stage.createVariable(
e.varId,
e.varName,
e.varType,
e.isCloud
);
this.emitProjectChanged(); this.emitProjectChanged();
} }
break; break;
case 'var_rename': case 'var_rename':
if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) { if (
editingTarget &&
Object.prototype.hasOwnProperty.call(
editingTarget.variables,
e.varId
)
) {
// This is a local variable, rename on the current target // This is a local variable, rename on the current target
editingTarget.renameVariable(e.varId, e.newName); editingTarget.renameVariable(e.varId, e.newName);
// Update all the blocks on the current target that use // Update all the blocks on the current target that use
// this variable // this variable
editingTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); editingTarget.blocks.updateBlocksAfterVarRename(
e.varId,
e.newName
);
} else { } else {
// This is a global variable // This is a global variable
stage.renameVariable(e.varId, e.newName); stage.renameVariable(e.varId, e.newName);
@ -410,14 +460,23 @@ class Blocks {
const targets = this.runtime.targets; const targets = this.runtime.targets;
for (let i = 0; i < targets.length; i++) { for (let i = 0; i < targets.length; i++) {
const currTarget = targets[i]; const currTarget = targets[i];
currTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); currTarget.blocks.updateBlocksAfterVarRename(
e.varId,
e.newName
);
} }
} }
this.emitProjectChanged(); this.emitProjectChanged();
break; break;
case 'var_delete': { case 'var_delete': {
const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) ? const target =
editingTarget : stage; editingTarget &&
Object.prototype.hasOwnProperty.call(
editingTarget.variables,
e.varId
) ?
editingTarget :
stage;
target.deleteVariable(e.varId); target.deleteVariable(e.varId);
this.emitProjectChanged(); this.emitProjectChanged();
break; break;
@ -425,11 +484,21 @@ class Blocks {
case 'comment_create': case 'comment_create':
if (this.runtime.getEditingTarget()) { if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget(); const currTarget = this.runtime.getEditingTarget();
currTarget.createComment(e.commentId, e.blockId, e.text, currTarget.createComment(
e.xy.x, e.xy.y, e.width, e.height, e.minimized); e.commentId,
e.blockId,
e.text,
e.xy.x,
e.xy.y,
e.width,
e.height,
e.minimized
);
if (currTarget.comments[e.commentId].x === null && if (
currTarget.comments[e.commentId].y === null) { currTarget.comments[e.commentId].x === null &&
currTarget.comments[e.commentId].y === null
) {
// Block comments imported from 2.0 projects are imported with their // Block comments imported from 2.0 projects are imported with their
// x and y coordinates set to null so that scratch-blocks can // x and y coordinates set to null so that scratch-blocks can
// auto-position them. If we are receiving a create event for these // auto-position them. If we are receiving a create event for these
@ -445,8 +514,15 @@ class Blocks {
case 'comment_change': case 'comment_change':
if (this.runtime.getEditingTarget()) { if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget(); const currTarget = this.runtime.getEditingTarget();
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) { if (
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`); !Object.prototype.hasOwnProperty.call(
currTarget.comments,
e.commentId
)
) {
log.warn(
`Cannot change comment with id ${e.commentId} because it does not exist.`
);
return; return;
} }
const comment = currTarget.comments[e.commentId]; const comment = currTarget.comments[e.commentId];
@ -468,8 +544,16 @@ class Blocks {
case 'comment_move': case 'comment_move':
if (this.runtime.getEditingTarget()) { if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget(); const currTarget = this.runtime.getEditingTarget();
if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) { if (
log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`); currTarget &&
!Object.prototype.hasOwnProperty.call(
currTarget.comments,
e.commentId
)
) {
log.warn(
`Cannot move comment with id ${e.commentId} because it does not exist.`
);
return; return;
} }
const comment = currTarget.comments[e.commentId]; const comment = currTarget.comments[e.commentId];
@ -483,7 +567,12 @@ class Blocks {
case 'comment_delete': case 'comment_delete':
if (this.runtime.getEditingTarget()) { if (this.runtime.getEditingTarget()) {
const currTarget = this.runtime.getEditingTarget(); const currTarget = this.runtime.getEditingTarget();
if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) { if (
!Object.prototype.hasOwnProperty.call(
currTarget.comments,
e.commentId
)
) {
// If we're in this state, we have probably received // If we're in this state, we have probably received
// a delete event from a workspace that we switched from // a delete event from a workspace that we switched from
// (e.g. a delete event for a comment on sprite a's workspace // (e.g. a delete event for a comment on sprite a's workspace
@ -494,7 +583,9 @@ class Blocks {
if (e.blockId) { if (e.blockId) {
const block = currTarget.blocks.getBlock(e.blockId); const block = currTarget.blocks.getBlock(e.blockId);
if (!block) { if (!block) {
log.warn(`Could not find block referenced by comment with id: ${e.commentId}`); log.warn(
`Could not find block referenced by comment with id: ${e.commentId}`
);
return; return;
} }
delete block.comment; delete block.comment;
@ -562,7 +653,9 @@ class Blocks {
*/ */
changeBlock (args) { changeBlock (args) {
// Validate // Validate
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return; if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) {
return;
}
let block = this._blocks[args.id]; let block = this._blocks[args.id];
if (typeof block === 'undefined') return; if (typeof block === 'undefined') return;
switch (args.element) { switch (args.element) {
@ -576,13 +669,17 @@ class Blocks {
// 3. the checkbox should become unchecked if we're not already // 3. the checkbox should become unchecked if we're not already
// monitoring current minute // monitoring current minute
// Update block value // Update block value
if (!block.fields[args.name]) return; if (!block.fields[args.name]) return;
if (args.name === 'VARIABLE' || args.name === 'LIST' || if (
args.name === 'BROADCAST_OPTION') { args.name === 'VARIABLE' ||
args.name === 'LIST' ||
args.name === 'BROADCAST_OPTION'
) {
// Get variable name using the id in args.value. // Get variable name using the id in args.value.
const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); const variable = this.runtime
.getEditingTarget()
.lookupVariableById(args.value);
if (variable) { if (variable) {
block.fields[args.name].value = variable.name; block.fields[args.name].value = variable.name;
block.fields[args.name].id = args.value; block.fields[args.name].id = args.value;
@ -596,19 +693,26 @@ class Blocks {
// TODO: (#1787) // TODO: (#1787)
if (block.opcode === 'sensing_of_object_menu') { if (block.opcode === 'sensing_of_object_menu') {
if (block.fields.OBJECT.value === '_stage_') { if (block.fields.OBJECT.value === '_stage_') {
this._blocks[block.parent].fields.PROPERTY.value = 'backdrop #'; this._blocks[block.parent].fields.PROPERTY.value =
'backdrop #';
} else { } else {
this._blocks[block.parent].fields.PROPERTY.value = 'x position'; this._blocks[block.parent].fields.PROPERTY.value =
'x position';
} }
this.runtime.requestBlocksUpdate(); this.runtime.requestBlocksUpdate();
} }
const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block; const flyoutBlock =
block.shadow && block.parent ?
this._blocks[block.parent] :
block;
if (flyoutBlock.isMonitored) { if (flyoutBlock.isMonitored) {
this.runtime.requestUpdateMonitor(Map({ this.runtime.requestUpdateMonitor(
Map({
id: flyoutBlock.id, id: flyoutBlock.id,
params: this._getBlockParams(flyoutBlock) params: this._getBlockParams(flyoutBlock)
})); })
);
} }
} }
break; break;
@ -619,14 +723,20 @@ class Blocks {
// A checkbox usually has a one to one correspondence with the monitor // A checkbox usually has a one to one correspondence with the monitor
// block but in the case of monitored reporters that have arguments, // block but in the case of monitored reporters that have arguments,
// map the old id to a new id, creating a new monitor block if necessary // map the old id to a new id, creating a new monitor block if necessary
if (block.fields && Object.keys(block.fields).length > 0 && if (
block.opcode !== 'data_variable' && block.opcode !== 'data_listcontents') { block.fields &&
Object.keys(block.fields).length > 0 &&
block.opcode !== 'data_variable' &&
block.opcode !== 'data_listcontents'
) {
// This block has an argument which needs to get separated out into // This block has an argument which needs to get separated out into
// multiple monitor blocks with ids based on the selected argument // multiple monitor blocks with ids based on the selected argument
const newId = getMonitorIdForBlockWithArgs(block.id, block.fields);
// Note: we're not just constantly creating a longer and longer id everytime we check // Note: we're not just constantly creating a longer and longer id everytime we check
// the checkbox because we're using the id of the block in the flyout as the base // the checkbox because we're using the id of the block in the flyout as the base
const newId = getMonitorIdForBlockWithArgs(
block.id,
block.fields
);
// check if a block with the new id already exists, otherwise create // check if a block with the new id already exists, otherwise create
let newBlock = this.runtime.monitorBlocks.getBlock(newId); let newBlock = this.runtime.monitorBlocks.getBlock(newId);
@ -645,19 +755,31 @@ class Blocks {
// Variable blocks may be sprite specific depending on the owner of the variable // Variable blocks may be sprite specific depending on the owner of the variable
let isSpriteLocalVariable = false; let isSpriteLocalVariable = false;
if (block.opcode === 'data_variable') { if (block.opcode === 'data_variable') {
isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.VARIABLE.id]); isSpriteLocalVariable =
!this.runtime.getTargetForStage().variables[
block.fields.VARIABLE.id
];
} else if (block.opcode === 'data_listcontents') { } else if (block.opcode === 'data_listcontents') {
isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.LIST.id]); isSpriteLocalVariable =
!this.runtime.getTargetForStage().variables[
block.fields.LIST.id
];
} }
const isSpriteSpecific = isSpriteLocalVariable || const isSpriteSpecific =
(Object.prototype.hasOwnProperty.call(this.runtime.monitorBlockInfo, block.opcode) && isSpriteLocalVariable ||
this.runtime.monitorBlockInfo[block.opcode].isSpriteSpecific); (Object.prototype.hasOwnProperty.call(
this.runtime.monitorBlockInfo,
block.opcode
) &&
this.runtime.monitorBlockInfo[block.opcode]
.isSpriteSpecific);
if (isSpriteSpecific) { if (isSpriteSpecific) {
// If creating a new sprite specific monitor, the only possible target is // If creating a new sprite specific monitor, the only possible target is
// the current editing one b/c you cannot dynamically create monitors. // the current editing one b/c you cannot dynamically create monitors.
// Also, do not change the targetId if it has already been assigned // Also, do not change the targetId if it has already been assigned
block.targetId = block.targetId || this.runtime.getEditingTarget().id; block.targetId =
block.targetId || this.runtime.getEditingTarget().id;
} else { } else {
block.targetId = null; block.targetId = null;
} }
@ -667,16 +789,25 @@ class Blocks {
} else if (!wasMonitored && block.isMonitored) { } else if (!wasMonitored && block.isMonitored) {
// Tries to show the monitor for specified block. If it doesn't exist, add the monitor. // Tries to show the monitor for specified block. If it doesn't exist, add the monitor.
if (!this.runtime.requestShowMonitor(block.id)) { if (!this.runtime.requestShowMonitor(block.id)) {
this.runtime.requestAddMonitor(MonitorRecord({ this.runtime.requestAddMonitor(
MonitorRecord({
id: block.id, id: block.id,
targetId: block.targetId, targetId: block.targetId,
spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null, spriteName: block.targetId ?
this.runtime
.getTargetById(block.targetId)
.getName() :
null,
opcode: block.opcode, opcode: block.opcode,
params: this._getBlockParams(block), params: this._getBlockParams(block),
// @todo(vm#565) for numerical values with decimals, some countries use comma // @todo(vm#565) for numerical values with decimals, some countries use comma
value: '', value: '',
mode: block.opcode === 'data_listcontents' ? 'list' : 'default' mode:
})); block.opcode === 'data_listcontents' ?
'list' :
'default'
})
);
} }
} }
break; break;
@ -705,8 +836,8 @@ class Blocks {
// Move coordinate changes. // Move coordinate changes.
if (e.newCoordinate) { if (e.newCoordinate) {
didChange =
didChange = (block.x !== e.newCoordinate.x) || (block.y !== e.newCoordinate.y); block.x !== e.newCoordinate.x || block.y !== e.newCoordinate.y;
block.x = e.newCoordinate.x; block.x = e.newCoordinate.x;
block.y = e.newCoordinate.y; block.y = e.newCoordinate.y;
@ -715,8 +846,10 @@ class Blocks {
// Remove from any old parent. // Remove from any old parent.
if (typeof e.oldParent !== 'undefined') { if (typeof e.oldParent !== 'undefined') {
const oldParent = this._blocks[e.oldParent]; const oldParent = this._blocks[e.oldParent];
if (typeof e.oldInput !== 'undefined' && if (
oldParent.inputs[e.oldInput].block === e.id) { typeof e.oldInput !== 'undefined' &&
oldParent.inputs[e.oldInput].block === e.id
) {
// This block was connected to the old parent's input. // This block was connected to the old parent's input.
oldParent.inputs[e.oldInput].block = null; oldParent.inputs[e.oldInput].block = null;
} else if (oldParent.next === e.id) { } else if (oldParent.next === e.id) {
@ -741,8 +874,14 @@ class Blocks {
// Moved to the new parent's input. // Moved to the new parent's input.
// Don't obscure the shadow block. // Don't obscure the shadow block.
let oldShadow = null; let oldShadow = null;
if (Object.prototype.hasOwnProperty.call(this._blocks[e.newParent].inputs, e.newInput)) { if (
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow; Object.prototype.hasOwnProperty.call(
this._blocks[e.newParent].inputs,
e.newInput
)
) {
oldShadow =
this._blocks[e.newParent].inputs[e.newInput].shadow;
} }
// If the block being attached is itself a shadow, make sure to set // If the block being attached is itself a shadow, make sure to set
@ -764,7 +903,6 @@ class Blocks {
if (didChange) this.emitProjectChanged(); if (didChange) this.emitProjectChanged();
} }
/** /**
* Block management: run all blocks. * Block management: run all blocks.
* @param {!object} runtime Runtime to run all blocks in. * @param {!object} runtime Runtime to run all blocks in.
@ -777,7 +915,9 @@ class Blocks {
const targetId = this.getBlock(blockId).targetId; const targetId = this.getBlock(blockId).targetId;
return { return {
blockId, blockId,
target: targetId ? runtime.getTargetById(targetId) : null target: targetId ?
runtime.getTargetById(targetId) :
null
}; };
}); });
} }
@ -816,8 +956,10 @@ class Blocks {
this.deleteBlock(block.inputs[input].block); this.deleteBlock(block.inputs[input].block);
} }
// Delete obscured shadow blocks. // Delete obscured shadow blocks.
if (block.inputs[input].shadow !== null && if (
block.inputs[input].shadow !== block.inputs[input].block) { block.inputs[input].shadow !== null &&
block.inputs[input].shadow !== block.inputs[input].block
) {
this.deleteBlock(block.inputs[input].shadow); this.deleteBlock(block.inputs[input].shadow);
} }
} }
@ -863,7 +1005,10 @@ class Blocks {
} else if (blocks[blockId].fields.LIST) { } else if (blocks[blockId].fields.LIST) {
varOrListField = blocks[blockId].fields.LIST; varOrListField = blocks[blockId].fields.LIST;
varType = Variable.LIST_TYPE; varType = Variable.LIST_TYPE;
} else if (optIncludeBroadcast && blocks[blockId].fields.BROADCAST_OPTION) { } else if (
optIncludeBroadcast &&
blocks[blockId].fields.BROADCAST_OPTION
) {
varOrListField = blocks[blockId].fields.BROADCAST_OPTION; varOrListField = blocks[blockId].fields.BROADCAST_OPTION;
varType = Variable.BROADCAST_MESSAGE_TYPE; varType = Variable.BROADCAST_MESSAGE_TYPE;
} }
@ -875,10 +1020,12 @@ class Blocks {
type: varType type: varType
}); });
} else { } else {
allReferences[currVarId] = [{ allReferences[currVarId] = [
{
referencingField: varOrListField, referencingField: varOrListField,
type: varType type: varType
}]; }
];
} }
} }
} }
@ -915,9 +1062,15 @@ class Blocks {
updateTargetSpecificBlocks (isStage) { updateTargetSpecificBlocks (isStage) {
const blocks = this._blocks; const blocks = this._blocks;
for (const blockId in blocks) { for (const blockId in blocks) {
if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') { if (
isStage &&
blocks[blockId].opcode === 'event_whenthisspriteclicked'
) {
blocks[blockId].opcode = 'event_whenstageclicked'; blocks[blockId].opcode = 'event_whenstageclicked';
} else if (!isStage && blocks[blockId].opcode === 'event_whenstageclicked') { } else if (
!isStage &&
blocks[blockId].opcode === 'event_whenstageclicked'
) {
blocks[blockId].opcode = 'event_whenthisspriteclicked'; blocks[blockId].opcode = 'event_whenthisspriteclicked';
} }
} }
@ -967,10 +1120,12 @@ class Blocks {
let blockUpdated = false; let blockUpdated = false;
for (const blockId in blocks) { for (const blockId in blocks) {
const block = blocks[blockId]; const block = blocks[blockId];
if (block.opcode === 'sensing_of' && if (
block.opcode === 'sensing_of' &&
block.fields.PROPERTY.value === oldName && block.fields.PROPERTY.value === oldName &&
// If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored. // If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored.
block.inputs.OBJECT.block === block.inputs.OBJECT.shadow) { block.inputs.OBJECT.block === block.inputs.OBJECT.shadow
) {
const inputBlock = this.getBlock(block.inputs.OBJECT.block); const inputBlock = this.getBlock(block.inputs.OBJECT.block);
if (inputBlock.fields.OBJECT.value === targetName) { if (inputBlock.fields.OBJECT.value === targetName) {
block.fields.PROPERTY.value = newName; block.fields.PROPERTY.value = newName;
@ -991,7 +1146,10 @@ class Blocks {
*/ */
_getCostumeField (blockId) { _getCostumeField (blockId) {
const block = this.getBlock(blockId); const block = this.getBlock(blockId);
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')) { if (
block &&
Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')
) {
return block.fields.COSTUME; return block.fields.COSTUME;
} }
return null; return null;
@ -1006,7 +1164,10 @@ class Blocks {
*/ */
_getSoundField (blockId) { _getSoundField (blockId) {
const block = this.getBlock(blockId); const block = this.getBlock(blockId);
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')) { if (
block &&
Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')
) {
return block.fields.SOUND_MENU; return block.fields.SOUND_MENU;
} }
return null; return null;
@ -1021,7 +1182,10 @@ class Blocks {
*/ */
_getBackdropField (blockId) { _getBackdropField (blockId) {
const block = this.getBlock(blockId); const block = this.getBlock(blockId);
if (block && Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')) { if (
block &&
Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')
) {
return block.fields.BACKDROP; return block.fields.BACKDROP;
} }
return null; return null;
@ -1039,8 +1203,15 @@ class Blocks {
if (!block) { if (!block) {
return null; return null;
} }
const spriteMenuNames = ['TOWARDS', 'TO', 'OBJECT', 'VIDEOONMENU2', const spriteMenuNames = [
'DISTANCETOMENU', 'TOUCHINGOBJECTMENU', 'CLONE_OPTION']; 'TOWARDS',
'TO',
'OBJECT',
'VIDEOONMENU2',
'DISTANCETOMENU',
'TOUCHINGOBJECTMENU',
'CLONE_OPTION'
];
for (let i = 0; i < spriteMenuNames.length; i++) { for (let i = 0; i < spriteMenuNames.length; i++) {
const menuName = spriteMenuNames[i]; const menuName = spriteMenuNames[i];
if (Object.prototype.hasOwnProperty.call(block.fields, menuName)) { if (Object.prototype.hasOwnProperty.call(block.fields, menuName)) {
@ -1059,7 +1230,9 @@ class Blocks {
* @return {string} String of XML representing this object's blocks. * @return {string} String of XML representing this object's blocks.
*/ */
toXML (comments) { toXML (comments) {
return this._scripts.map(script => this.blockToXML(script, comments)).join(); return this._scripts
.map(script => this.blockToXML(script, comments))
.join();
} }
/** /**
@ -1076,9 +1249,8 @@ class Blocks {
// this early exit allows the project to load. // this early exit allows the project to load.
if (!block) return; if (!block) return;
// Encode properties of this block. // Encode properties of this block.
const tagName = (block.shadow) ? 'shadow' : 'block'; const tagName = block.shadow ? 'shadow' : 'block';
let xmlString = let xmlString = `<${tagName}
`<${tagName}
id="${block.id}" id="${block.id}"
type="${block.opcode}" type="${block.opcode}"
${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''} ${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''}
@ -1089,10 +1261,14 @@ class Blocks {
if (Object.prototype.hasOwnProperty.call(comments, commentId)) { if (Object.prototype.hasOwnProperty.call(comments, commentId)) {
xmlString += comments[commentId].toXML(); xmlString += comments[commentId].toXML();
} else { } else {
log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`); log.warn(
`Could not find comment with id: ${commentId} in provided comment descriptions.`
);
} }
} else { } else {
log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`); log.warn(
`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`
);
} }
} }
// Add any mutation. Must come before inputs. // Add any mutation. Must come before inputs.
@ -1101,7 +1277,9 @@ class Blocks {
} }
// Add any inputs on this block. // Add any inputs on this block.
for (const input in block.inputs) { for (const input in block.inputs) {
if (!Object.prototype.hasOwnProperty.call(block.inputs, input)) continue; if (!Object.prototype.hasOwnProperty.call(block.inputs, input)) {
continue;
}
const blockInput = block.inputs[input]; const blockInput = block.inputs[input];
// Only encode a value tag if the value input is occupied. // Only encode a value tag if the value input is occupied.
if (blockInput.block || blockInput.shadow) { if (blockInput.block || blockInput.shadow) {
@ -1109,7 +1287,10 @@ class Blocks {
if (blockInput.block) { if (blockInput.block) {
xmlString += this.blockToXML(blockInput.block, comments); xmlString += this.blockToXML(blockInput.block, comments);
} }
if (blockInput.shadow && blockInput.shadow !== blockInput.block) { if (
blockInput.shadow &&
blockInput.shadow !== blockInput.block
) {
// Obscured shadow. // Obscured shadow.
xmlString += this.blockToXML(blockInput.shadow, comments); xmlString += this.blockToXML(blockInput.shadow, comments);
} }
@ -1118,7 +1299,9 @@ class Blocks {
} }
// Add any fields on this block. // Add any fields on this block.
for (const field in block.fields) { for (const field in block.fields) {
if (!Object.prototype.hasOwnProperty.call(block.fields, field)) continue; if (!Object.prototype.hasOwnProperty.call(block.fields, field)) {
continue;
}
const blockField = block.fields[field]; const blockField = block.fields[field];
xmlString += `<field name="${blockField.name}"`; xmlString += `<field name="${blockField.name}"`;
const fieldId = blockField.id; const fieldId = blockField.id;
@ -1137,7 +1320,10 @@ class Blocks {
} }
// Add blocks connected to the next connection. // Add blocks connected to the next connection.
if (block.next) { if (block.next) {
xmlString += `<next>${this.blockToXML(block.next, comments)}</next>`; xmlString += `<next>${this.blockToXML(
block.next,
comments
)}</next>`;
} }
xmlString += `</${tagName}>`; xmlString += `</${tagName}>`;
return xmlString; return xmlString;
@ -1152,8 +1338,10 @@ class Blocks {
let mutationString = `<${mutation.tagName}`; let mutationString = `<${mutation.tagName}`;
for (const prop in mutation) { for (const prop in mutation) {
if (prop === 'children' || prop === 'tagName') continue; if (prop === 'children' || prop === 'tagName') continue;
let mutationValue = (typeof mutation[prop] === 'string') ? let mutationValue =
xmlEscape(mutation[prop]) : mutation[prop]; typeof mutation[prop] === 'string' ?
xmlEscape(mutation[prop]) :
mutation[prop];
// Handle dynamic extension blocks // Handle dynamic extension blocks
if (prop === 'blockInfo') { if (prop === 'blockInfo') {

View file

@ -385,7 +385,8 @@ class Runtime extends EventEmitter {
* being added. * being added.
* @type {function} * @type {function}
*/ */
this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); this.addCloudVariable =
this._initializeAddCloudVariable(newCloudDataManager);
/** /**
* A function which updates the runtime's cloud variable limit * A function which updates the runtime's cloud variable limit
@ -393,7 +394,8 @@ class Runtime extends EventEmitter {
* if the last of the cloud variables is being removed. * if the last of the cloud variables is being removed.
* @type {function} * @type {function}
*/ */
this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); this.removeCloudVariable =
this._initializeRemoveCloudVariable(newCloudDataManager);
/** /**
* A string representing the origin of the current project from outside of the * A string representing the origin of the current project from outside of the
@ -737,24 +739,24 @@ class Runtime extends EventEmitter {
// Helper function for initializing the addCloudVariable function // Helper function for initializing the addCloudVariable function
_initializeAddCloudVariable (newCloudDataManager) { _initializeAddCloudVariable (newCloudDataManager) {
// The addCloudVariable function // The addCloudVariable function
return (() => { return () => {
const hadCloudVarsBefore = this.hasCloudData(); const hadCloudVarsBefore = this.hasCloudData();
newCloudDataManager.addCloudVariable(); newCloudDataManager.addCloudVariable();
if (!hadCloudVarsBefore && this.hasCloudData()) { if (!hadCloudVarsBefore && this.hasCloudData()) {
this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, true); this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, true);
} }
}); };
} }
// Helper function for initializing the removeCloudVariable function // Helper function for initializing the removeCloudVariable function
_initializeRemoveCloudVariable (newCloudDataManager) { _initializeRemoveCloudVariable (newCloudDataManager) {
return (() => { return () => {
const hadCloudVarsBefore = this.hasCloudData(); const hadCloudVarsBefore = this.hasCloudData();
newCloudDataManager.removeCloudVariable(); newCloudDataManager.removeCloudVariable();
if (hadCloudVarsBefore && !this.hasCloudData()) { if (hadCloudVarsBefore && !this.hasCloudData()) {
this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false); this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false);
} }
}); };
} }
/** /**
@ -764,14 +766,26 @@ class Runtime extends EventEmitter {
*/ */
_registerBlockPackages () { _registerBlockPackages () {
for (const packageName in defaultBlockPackages) { for (const packageName in defaultBlockPackages) {
if (Object.prototype.hasOwnProperty.call(defaultBlockPackages, packageName)) { if (
Object.prototype.hasOwnProperty.call(
defaultBlockPackages,
packageName
)
) {
// @todo pass a different runtime depending on package privilege? // @todo pass a different runtime depending on package privilege?
const packageObject = new (defaultBlockPackages[packageName])(this); const packageObject = new defaultBlockPackages[packageName](
this
);
// Collect primitives from package. // Collect primitives from package.
if (packageObject.getPrimitives) { if (packageObject.getPrimitives) {
const packagePrimitives = packageObject.getPrimitives(); const packagePrimitives = packageObject.getPrimitives();
for (const op in packagePrimitives) { for (const op in packagePrimitives) {
if (Object.prototype.hasOwnProperty.call(packagePrimitives, op)) { if (
Object.prototype.hasOwnProperty.call(
packagePrimitives,
op
)
) {
this._primitives[op] = this._primitives[op] =
packagePrimitives[op].bind(packageObject); packagePrimitives[op].bind(packageObject);
} }
@ -781,14 +795,23 @@ class Runtime extends EventEmitter {
if (packageObject.getHats) { if (packageObject.getHats) {
const packageHats = packageObject.getHats(); const packageHats = packageObject.getHats();
for (const hatName in packageHats) { for (const hatName in packageHats) {
if (Object.prototype.hasOwnProperty.call(packageHats, hatName)) { if (
Object.prototype.hasOwnProperty.call(
packageHats,
hatName
)
) {
this._hats[hatName] = packageHats[hatName]; this._hats[hatName] = packageHats[hatName];
} }
} }
} }
// Collect monitored from package. // Collect monitored from package.
if (packageObject.getMonitored) { if (packageObject.getMonitored) {
this.monitorBlockInfo = Object.assign({}, this.monitorBlockInfo, packageObject.getMonitored()); this.monitorBlockInfo = Object.assign(
{},
this.monitorBlockInfo,
packageObject.getMonitored()
);
} }
} }
} }
@ -818,7 +841,9 @@ class Runtime extends EventEmitter {
const context = {}; const context = {};
target = target || this.getEditingTarget() || this.getTargetForStage(); target = target || this.getEditingTarget() || this.getTargetForStage();
if (target) { if (target) {
context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE); context.targetType = target.isStage ?
TargetType.STAGE :
TargetType.SPRITE;
} }
} }
@ -851,8 +876,14 @@ class Runtime extends EventEmitter {
this._fillExtensionCategory(categoryInfo, extensionInfo); this._fillExtensionCategory(categoryInfo, extensionInfo);
for (const fieldTypeName in categoryInfo.customFieldTypes) { for (const fieldTypeName in categoryInfo.customFieldTypes) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) { if (
const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName]; Object.prototype.hasOwnProperty.call(
extensionInfo.customFieldTypes,
fieldTypeName
)
) {
const fieldTypeInfo =
categoryInfo.customFieldTypes[fieldTypeName];
// Emit events for custom field types from extension // Emit events for custom field types from extension
this.emit(Runtime.EXTENSION_FIELD_ADDED, { this.emit(Runtime.EXTENSION_FIELD_ADDED, {
@ -871,7 +902,9 @@ class Runtime extends EventEmitter {
* @private * @private
*/ */
_refreshExtensionPrimitives (extensionInfo) { _refreshExtensionPrimitives (extensionInfo) {
const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); const categoryInfo = this._blockInfo.find(
info => info.id === extensionInfo.id
);
if (categoryInfo) { if (categoryInfo) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name); categoryInfo.name = maybeFormatMessage(extensionInfo.name);
this._fillExtensionCategory(categoryInfo, extensionInfo); this._fillExtensionCategory(categoryInfo, extensionInfo);
@ -894,15 +927,29 @@ class Runtime extends EventEmitter {
categoryInfo.menuInfo = {}; categoryInfo.menuInfo = {};
for (const menuName in extensionInfo.menus) { for (const menuName in extensionInfo.menus) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.menus, menuName)) { if (
Object.prototype.hasOwnProperty.call(
extensionInfo.menus,
menuName
)
) {
const menuInfo = extensionInfo.menus[menuName]; const menuInfo = extensionInfo.menus[menuName];
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo); const convertedMenu = this._buildMenuForScratchBlocks(
menuName,
menuInfo,
categoryInfo
);
categoryInfo.menus.push(convertedMenu); categoryInfo.menus.push(convertedMenu);
categoryInfo.menuInfo[menuName] = menuInfo; categoryInfo.menuInfo[menuName] = menuInfo;
} }
} }
for (const fieldTypeName in extensionInfo.customFieldTypes) { for (const fieldTypeName in extensionInfo.customFieldTypes) {
if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) { if (
Object.prototype.hasOwnProperty.call(
extensionInfo.customFieldTypes,
fieldTypeName
)
) {
const fieldType = extensionInfo.customFieldTypes[fieldTypeName]; const fieldType = extensionInfo.customFieldTypes[fieldTypeName];
const fieldTypeInfo = this._buildCustomFieldInfo( const fieldTypeInfo = this._buildCustomFieldInfo(
fieldTypeName, fieldTypeName,
@ -917,22 +964,32 @@ class Runtime extends EventEmitter {
for (const blockInfo of extensionInfo.blocks) { for (const blockInfo of extensionInfo.blocks) {
try { try {
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); const convertedBlock = this._convertForScratchBlocks(
blockInfo,
categoryInfo
);
categoryInfo.blocks.push(convertedBlock); categoryInfo.blocks.push(convertedBlock);
if (convertedBlock.json) { if (convertedBlock.json) {
const opcode = convertedBlock.json.type; const opcode = convertedBlock.json.type;
if (blockInfo.blockType !== BlockType.EVENT) { if (blockInfo.blockType !== BlockType.EVENT) {
this._primitives[opcode] = convertedBlock.info.func; this._primitives[opcode] = convertedBlock.info.func;
} }
if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { if (
blockInfo.blockType === BlockType.EVENT ||
blockInfo.blockType === BlockType.HAT
) {
this._hats[opcode] = { this._hats[opcode] = {
edgeActivated: blockInfo.isEdgeActivated, edgeActivated: blockInfo.isEdgeActivated,
restartExistingThreads: blockInfo.shouldRestartExistingThreads restartExistingThreads:
blockInfo.shouldRestartExistingThreads
}; };
} }
} }
} catch (e) { } catch (e) {
log.error('Error parsing block: ', {block: blockInfo, error: e}); log.error('Error parsing block: ', {
block: blockInfo,
error: e
});
} }
} }
} }
@ -948,14 +1005,25 @@ class Runtime extends EventEmitter {
if (typeof menuItems !== 'function') { if (typeof menuItems !== 'function') {
const extensionMessageContext = this.makeMessageContextForTarget(); const extensionMessageContext = this.makeMessageContextForTarget();
return menuItems.map(item => { return menuItems.map(item => {
const formattedItem = maybeFormatMessage(item, extensionMessageContext); const formattedItem = maybeFormatMessage(
item,
extensionMessageContext
);
switch (typeof formattedItem) { switch (typeof formattedItem) {
case 'string': case 'string':
return [formattedItem, formattedItem]; return [formattedItem, formattedItem];
case 'object': case 'object':
return [maybeFormatMessage(item.text, extensionMessageContext), item.value]; return [
maybeFormatMessage(
item.text,
extensionMessageContext
),
item.value
];
default: default:
throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`); throw new Error(
`Can't interpret menu item: ${JSON.stringify(item)}`
);
} }
}); });
} }
@ -985,7 +1053,8 @@ class Runtime extends EventEmitter {
colourSecondary: categoryInfo.color2, colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3, colourTertiary: categoryInfo.color3,
outputShape: menuInfo.acceptReporters ? outputShape: menuInfo.acceptReporters ?
ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND :
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE,
args0: [ args0: [
{ {
type: 'field_dropdown', type: 'field_dropdown',
@ -1027,7 +1096,12 @@ class Runtime extends EventEmitter {
* @param {object} categoryInfo - The category the field belongs to (Used to set its colors) * @param {object} categoryInfo - The category the field belongs to (Used to set its colors)
* @returns {object} - Object to be inserted into scratch-blocks * @returns {object} - Object to be inserted into scratch-blocks
*/ */
_buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) { _buildCustomFieldTypeForScratchBlocks (
fieldName,
output,
outputShape,
categoryInfo
) {
return { return {
json: { json: {
type: fieldName, type: fieldName,
@ -1114,15 +1188,13 @@ class Runtime extends EventEmitter {
const separatorJSON = { const separatorJSON = {
type: 'field_vertical_separator' type: 'field_vertical_separator'
}; };
blockJSON.args0 = [ blockJSON.args0 = [iconJSON, separatorJSON];
iconJSON,
separatorJSON
];
} }
switch (blockInfo.blockType) { switch (blockInfo.blockType) {
case BlockType.COMMAND: case BlockType.COMMAND:
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.outputShape =
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
blockJSON.previousStatement = null; // null = available connection; undefined = hat blockJSON.previousStatement = null; // null = available connection; undefined = hat
if (!blockInfo.isTerminal) { if (!blockInfo.isTerminal) {
blockJSON.nextStatement = null; // null = available connection; undefined = terminal blockJSON.nextStatement = null; // null = available connection; undefined = terminal
@ -1130,25 +1202,34 @@ class Runtime extends EventEmitter {
break; break;
case BlockType.REPORTER: case BlockType.REPORTER:
blockJSON.output = 'String'; // TODO: distinguish number & string here? blockJSON.output = 'String'; // TODO: distinguish number & string here?
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_ROUND; blockJSON.outputShape =
ScratchBlocksConstants.OUTPUT_SHAPE_ROUND;
break; break;
case BlockType.BOOLEAN: case BlockType.BOOLEAN:
blockJSON.output = 'Boolean'; blockJSON.output = 'Boolean';
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL; blockJSON.outputShape =
ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL;
break; break;
case BlockType.HAT: case BlockType.HAT:
case BlockType.EVENT: case BlockType.EVENT:
if (!Object.prototype.hasOwnProperty.call(blockInfo, 'isEdgeActivated')) { if (
!Object.prototype.hasOwnProperty.call(
blockInfo,
'isEdgeActivated'
)
) {
// if absent, this property defaults to true // if absent, this property defaults to true
blockInfo.isEdgeActivated = true; blockInfo.isEdgeActivated = true;
} }
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.outputShape =
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
blockJSON.nextStatement = null; // null = available connection; undefined = terminal blockJSON.nextStatement = null; // null = available connection; undefined = terminal
break; break;
case BlockType.CONDITIONAL: case BlockType.CONDITIONAL:
case BlockType.LOOP: case BlockType.LOOP:
blockInfo.branchCount = blockInfo.branchCount || 1; blockInfo.branchCount = blockInfo.branchCount || 1;
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.outputShape =
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
blockJSON.previousStatement = null; // null = available connection; undefined = hat blockJSON.previousStatement = null; // null = available connection; undefined = hat
if (!blockInfo.isTerminal) { if (!blockInfo.isTerminal) {
blockJSON.nextStatement = null; // null = available connection; undefined = terminal blockJSON.nextStatement = null; // null = available connection; undefined = terminal
@ -1156,19 +1237,33 @@ class Runtime extends EventEmitter {
break; break;
} }
const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; const blockText = Array.isArray(blockInfo.text) ?
blockInfo.text :
[blockInfo.text];
let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum] let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum]
let inBranchNum = 0; // how many branches have we placed into the JSON so far? let inBranchNum = 0; // how many branches have we placed into the JSON so far?
let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}` let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}`
const convertPlaceholders = this._convertPlaceholders.bind(this, context); const convertPlaceholders = this._convertPlaceholders.bind(
this,
context
);
const extensionMessageContext = this.makeMessageContextForTarget(); const extensionMessageContext = this.makeMessageContextForTarget();
// alternate between a block "arm" with text on it and an open slot for a substack // alternate between a block "arm" with text on it and an open slot for a substack
while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) { while (
inTextNum < blockText.length ||
inBranchNum < blockInfo.branchCount
) {
if (inTextNum < blockText.length) { if (inTextNum < blockText.length) {
context.outLineNum = outLineNum; context.outLineNum = outLineNum;
const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext); const lineText = maybeFormatMessage(
const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders); blockText[inTextNum],
extensionMessageContext
);
const convertedText = lineText.replace(
/\[(.+?)]/g,
convertPlaceholders
);
if (blockJSON[`message${outLineNum}`]) { if (blockJSON[`message${outLineNum}`]) {
blockJSON[`message${outLineNum}`] += convertedText; blockJSON[`message${outLineNum}`] += convertedText;
} else { } else {
@ -1179,10 +1274,14 @@ class Runtime extends EventEmitter {
} }
if (inBranchNum < blockInfo.branchCount) { if (inBranchNum < blockInfo.branchCount) {
blockJSON[`message${outLineNum}`] = '%1'; blockJSON[`message${outLineNum}`] = '%1';
blockJSON[`args${outLineNum}`] = [{ blockJSON[`args${outLineNum}`] = [
{
type: 'input_statement', type: 'input_statement',
name: `SUBSTACK${inBranchNum > 0 ? inBranchNum + 1 : ''}` name: `SUBSTACK${
}]; inBranchNum > 0 ? inBranchNum + 1 : ''
}`
}
];
++inBranchNum; ++inBranchNum;
++outLineNum; ++outLineNum;
} }
@ -1196,18 +1295,22 @@ class Runtime extends EventEmitter {
// Add icon to the bottom right of a loop block // Add icon to the bottom right of a loop block
blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT'; blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT';
blockJSON[`message${outLineNum}`] = '%1'; blockJSON[`message${outLineNum}`] = '%1';
blockJSON[`args${outLineNum}`] = [{ blockJSON[`args${outLineNum}`] = [
{
type: 'field_image', type: 'field_image',
src: './static/blocks-media/repeat.svg', // TODO: use a constant or make this configurable? src: './static/blocks-media/repeat.svg', // TODO: use a constant or make this configurable?
width: 24, width: 24,
height: 24, height: 24,
alt: '*', // TODO remove this since we don't use collapsed blocks in scratch alt: '*', // TODO remove this since we don't use collapsed blocks in scratch
flip_rtl: true flip_rtl: true
}]; }
];
++outLineNum; ++outLineNum;
} }
const mutation = blockInfo.isDynamic ? `<mutation blockInfo="${xmlEscape(JSON.stringify(blockInfo))}"/>` : ''; const mutation = blockInfo.isDynamic ?
`<mutation blockInfo="${xmlEscape(JSON.stringify(blockInfo))}"/>` :
'';
const inputs = context.inputList.join(''); const inputs = context.inputList.join('');
const blockXML = `<block type="${extendedOpcode}">${mutation}${inputs}</block>`; const blockXML = `<block type="${extendedOpcode}">${mutation}${inputs}</block>`;
@ -1242,13 +1345,22 @@ class Runtime extends EventEmitter {
*/ */
_convertButtonForScratchBlocks (buttonInfo) { _convertButtonForScratchBlocks (buttonInfo) {
// for now we only support these pre-defined callbacks handled in scratch-blocks // for now we only support these pre-defined callbacks handled in scratch-blocks
const supportedCallbackKeys = ['MAKE_A_LIST', 'MAKE_A_PROCEDURE', 'MAKE_A_VARIABLE']; const supportedCallbackKeys = [
'MAKE_A_LIST',
'MAKE_A_PROCEDURE',
'MAKE_A_VARIABLE'
];
if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) { if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) {
log.error(`Custom button callbacks not supported yet: ${buttonInfo.func}`); log.error(
`Custom button callbacks not supported yet: ${buttonInfo.func}`
);
} }
const extensionMessageContext = this.makeMessageContextForTarget(); const extensionMessageContext = this.makeMessageContextForTarget();
const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext); const buttonText = maybeFormatMessage(
buttonInfo.text,
extensionMessageContext
);
return { return {
info: buttonInfo, info: buttonInfo,
xml: `<button text="${buttonText}" callbackKey="${buttonInfo.func}"></button>` xml: `<button text="${buttonText}" callbackKey="${buttonInfo.func}"></button>`
@ -1263,7 +1375,9 @@ class Runtime extends EventEmitter {
*/ */
_constructInlineImageJson (argInfo) { _constructInlineImageJson (argInfo) {
if (!argInfo.dataURI) { if (!argInfo.dataURI) {
log.warn('Missing data URI in extension block with argument type IMAGE'); log.warn(
'Missing data URI in extension block with argument type IMAGE'
);
} }
return { return {
type: 'field_image', type: 'field_image',
@ -1296,8 +1410,13 @@ class Runtime extends EventEmitter {
let argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; let argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
// Field type not a standard field type, see if extension has registered custom field type // Field type not a standard field type, see if extension has registered custom field type
if (!ArgumentTypeMap[argInfo.type] && context.categoryInfo.customFieldTypes[argInfo.type]) { if (
argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo; !ArgumentTypeMap[argInfo.type] &&
context.categoryInfo.customFieldTypes[argInfo.type]
) {
argTypeInfo =
context.categoryInfo.customFieldTypes[argInfo.type]
.argumentTypeInfo;
} }
// Start to construct the scratch-blocks style JSON defining how the block should be // Start to construct the scratch-blocks style JSON defining how the block should be
@ -1318,8 +1437,14 @@ class Runtime extends EventEmitter {
}; };
const defaultValue = const defaultValue =
typeof argInfo.defaultValue === 'undefined' ? '' : typeof argInfo.defaultValue === 'undefined' ?
xmlEscape(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString()); '' :
xmlEscape(
maybeFormatMessage(
argInfo.defaultValue,
this.makeMessageContextForTarget()
).toString()
);
if (argTypeInfo.check) { if (argTypeInfo.check) {
// Right now the only type of 'check' we have specifies that the // Right now the only type of 'check' we have specifies that the
@ -1335,7 +1460,10 @@ class Runtime extends EventEmitter {
const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; const menuInfo = context.categoryInfo.menuInfo[argInfo.menu];
if (menuInfo.acceptReporters) { if (menuInfo.acceptReporters) {
valueName = placeholder; valueName = placeholder;
shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); shadowType = this._makeExtensionMenuId(
argInfo.menu,
context.categoryInfo.id
);
fieldName = argInfo.menu; fieldName = argInfo.menu;
} else { } else {
argJSON.type = 'field_dropdown'; argJSON.type = 'field_dropdown';
@ -1346,8 +1474,11 @@ class Runtime extends EventEmitter {
} }
} else { } else {
valueName = placeholder; valueName = placeholder;
shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; shadowType =
fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null; (argTypeInfo.shadow && argTypeInfo.shadow.type) || null;
fieldName =
(argTypeInfo.shadow && argTypeInfo.shadow.fieldName) ||
null;
} }
// <value> is the ScratchBlocks name for a block input. // <value> is the ScratchBlocks name for a block input.
@ -1364,7 +1495,9 @@ class Runtime extends EventEmitter {
// A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc. // A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
// Leave out the field if defaultValue or fieldName are not specified // Leave out the field if defaultValue or fieldName are not specified
if (defaultValue && fieldName) { if (defaultValue && fieldName) {
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`); context.inputList.push(
`<field name="${fieldName}">${defaultValue}</field>`
);
} }
if (shadowType) { if (shadowType) {
@ -1377,7 +1510,8 @@ class Runtime extends EventEmitter {
} }
const argsName = `args${context.outLineNum}`; const argsName = `args${context.outLineNum}`;
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); const blockArgs = (context.blockJSON[argsName] =
context.blockJSON[argsName] || []);
if (argJSON) blockArgs.push(argJSON); if (argJSON) blockArgs.push(argJSON);
const argNum = blockArgs.length; const argNum = blockArgs.length;
context.argsMap[placeholder] = argNum; context.argsMap[placeholder] = argNum;
@ -1419,8 +1553,7 @@ class Runtime extends EventEmitter {
} else if (categoryInfo.blockIconURI) { } else if (categoryInfo.blockIconURI) {
menuIconURI = categoryInfo.blockIconURI; menuIconURI = categoryInfo.blockIconURI;
} }
const menuIconXML = menuIconURI ? const menuIconXML = menuIconURI ? `iconURI="${menuIconURI}"` : '';
`iconURI="${menuIconURI}"` : '';
let statusButtonXML = ''; let statusButtonXML = '';
if (categoryInfo.showStatusButton) { if (categoryInfo.showStatusButton) {
@ -1429,8 +1562,11 @@ class Runtime extends EventEmitter {
return { return {
id: categoryInfo.id, id: categoryInfo.id,
xml: `<category name="${name}" id="${categoryInfo.id}" ${statusButtonXML} ${colorXML} ${menuIconXML}>${ xml: `<category name="${name}" id="${
paletteBlocks.map(block => block.xml).join('')}</category>` categoryInfo.id
}" ${statusButtonXML} ${colorXML} ${menuIconXML}>${paletteBlocks
.map(block => block.xml)
.join('')}</category>`
}; };
}); });
} }
@ -1440,7 +1576,12 @@ class Runtime extends EventEmitter {
*/ */
getBlocksJSON () { getBlocksJSON () {
return this._blockInfo.reduce( return this._blockInfo.reduce(
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); (result, categoryInfo) =>
result.concat(
categoryInfo.blocks.map(blockInfo => blockInfo.json)
),
[]
);
} }
/** /**
@ -1449,7 +1590,8 @@ class Runtime extends EventEmitter {
_initScratchLink () { _initScratchLink () {
// Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin. // Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin.
// note that `if (self?....)` will throw if `self` is undefined, so check for that first! // note that `if (self?....)` will throw if `self` is undefined, so check for that first!
if (typeof self !== 'undefined' && if (
typeof self !== 'undefined' &&
typeof document !== 'undefined' && typeof document !== 'undefined' &&
document.getElementById && document.getElementById &&
self.origin && self.origin &&
@ -1462,7 +1604,9 @@ class Runtime extends EventEmitter {
) )
) { ) {
// Create a script tag for the Scratch Link browser extension, unless one already exists // Create a script tag for the Scratch Link browser extension, unless one already exists
const scriptElement = document.getElementById('scratch-link-extension-script'); const scriptElement = document.getElementById(
'scratch-link-extension-script'
);
if (!scriptElement) { if (!scriptElement) {
const script = document.createElement('script'); const script = document.createElement('script');
script.id = 'scratch-link-extension-script'; script.id = 'scratch-link-extension-script';
@ -1481,7 +1625,8 @@ class Runtime extends EventEmitter {
* @returns {ScratchLinkSocket} The scratch link socket. * @returns {ScratchLinkSocket} The scratch link socket.
*/ */
getScratchLinkSocket (type) { getScratchLinkSocket (type) {
const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory; const factory =
this._linkSocketFactory || this._defaultScratchLinkSocketFactory;
return factory(type); return factory(type);
} }
@ -1501,10 +1646,15 @@ class Runtime extends EventEmitter {
*/ */
_defaultScratchLinkSocketFactory (type) { _defaultScratchLinkSocketFactory (type) {
const Scratch = self.Scratch; const Scratch = self.Scratch;
const ScratchLinkSafariSocket = Scratch && Scratch.ScratchLinkSafariSocket; const ScratchLinkSafariSocket =
Scratch && Scratch.ScratchLinkSafariSocket;
// detect this every time in case the user turns on the extension after loading the page // detect this every time in case the user turns on the extension after loading the page
const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible(); const useSafariSocket =
return useSafariSocket ? new ScratchLinkSafariSocket(type) : new ScratchLinkWebSocket(type); ScratchLinkSafariSocket &&
ScratchLinkSafariSocket.isSafariHelperCompatible();
return useSafariSocket ?
new ScratchLinkSafariSocket(type) :
new ScratchLinkWebSocket(type);
} }
/** /**
@ -1593,11 +1743,12 @@ class Runtime extends EventEmitter {
* @return {boolean} True if the op is known to be a edge-activated hat. * @return {boolean} True if the op is known to be a edge-activated hat.
*/ */
getIsEdgeActivatedHat (opcode) { getIsEdgeActivatedHat (opcode) {
return Object.prototype.hasOwnProperty.call(this._hats, opcode) && return (
this._hats[opcode].edgeActivated; Object.prototype.hasOwnProperty.call(this._hats, opcode) &&
this._hats[opcode].edgeActivated
);
} }
/** /**
* Attach the audio engine * Attach the audio engine
* @param {!AudioEngine} audioEngine The audio engine to attach * @param {!AudioEngine} audioEngine The audio engine to attach
@ -1701,10 +1852,10 @@ class Runtime extends EventEmitter {
*/ */
isActiveThread (thread) { isActiveThread (thread) {
return ( return (
(
thread.stack.length > 0 && thread.stack.length > 0 &&
thread.status !== Thread.STATUS_DONE) && thread.status !== Thread.STATUS_DONE &&
this.threads.indexOf(thread) > -1); this.threads.indexOf(thread) > -1
);
} }
/** /**
@ -1729,18 +1880,29 @@ class Runtime extends EventEmitter {
* determines whether we show a visual report when turning on the script. * determines whether we show a visual report when turning on the script.
*/ */
toggleScript (topBlockId, opts) { toggleScript (topBlockId, opts) {
opts = Object.assign({ opts = Object.assign(
{
target: this._editingTarget, target: this._editingTarget,
stackClick: false stackClick: false
}, opts); },
opts
);
// Remove any existing thread. // Remove any existing thread.
for (let i = 0; i < this.threads.length; i++) { for (let i = 0; i < this.threads.length; i++) {
// Toggling a script that's already running turns it off // Toggling a script that's already running turns it off
if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) { if (
this.threads[i].topBlock === topBlockId &&
this.threads[i].status !== Thread.STATUS_DONE
) {
const blockContainer = opts.target.blocks; const blockContainer = opts.target.blocks;
const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId)); const opcode = blockContainer.getOpcode(
blockContainer.getBlock(topBlockId)
);
if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) { if (
this.getIsEdgeActivatedHat(opcode) &&
this.threads[i].stackClick !== opts.stackClick
) {
// Allow edge activated hat thread stack click to coexist with // Allow edge activated hat thread stack click to coexist with
// edge activated hat thread that runs every frame // edge activated hat thread that runs every frame
continue; continue;
@ -1762,8 +1924,11 @@ class Runtime extends EventEmitter {
if (!optTarget) optTarget = this._editingTarget; if (!optTarget) optTarget = this._editingTarget;
for (let i = 0; i < this.threads.length; i++) { for (let i = 0; i < this.threads.length; i++) {
// Don't re-add the script if it's already running // Don't re-add the script if it's already running
if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE && if (
this.threads[i].updateMonitor) { this.threads[i].topBlock === topBlockId &&
this.threads[i].status !== Thread.STATUS_DONE &&
this.threads[i].updateMonitor
) {
return; return;
} }
} }
@ -1801,7 +1966,10 @@ class Runtime extends EventEmitter {
} }
for (let t = targets.length - 1; t >= 0; t--) { for (let t = targets.length - 1; t >= 0; t--) {
const target = targets[t]; const target = targets[t];
const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode); const scripts = BlocksRuntimeCache.getScripts(
target.blocks,
opcode
);
for (let j = 0; j < scripts.length; j++) { for (let j = 0; j < scripts.length; j++) {
f(scripts[j], target); f(scripts[j], target);
} }
@ -1815,9 +1983,13 @@ class Runtime extends EventEmitter {
* @param {Target=} optTarget Optionally, a target to restrict to. * @param {Target=} optTarget Optionally, a target to restrict to.
* @return {Array.<Thread>} List of threads started by this function. * @return {Array.<Thread>} List of threads started by this function.
*/ */
startHats (requestedHatOpcode, startHats (requestedHatOpcode, optMatchFields, optTarget) {
optMatchFields, optTarget) { if (
if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) { !Object.prototype.hasOwnProperty.call(
this._hats,
requestedHatOpcode
)
) {
// No known hat with this opcode. // No known hat with this opcode.
return; return;
} }
@ -1827,16 +1999,18 @@ class Runtime extends EventEmitter {
const hatMeta = instance._hats[requestedHatOpcode]; const hatMeta = instance._hats[requestedHatOpcode];
for (const opts in optMatchFields) { for (const opts in optMatchFields) {
if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) continue; if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) {
continue;
}
optMatchFields[opts] = optMatchFields[opts].toUpperCase(); optMatchFields[opts] = optMatchFields[opts].toUpperCase();
} }
// Consider all scripts, looking for hats with opcode `requestedHatOpcode`. // Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => { this.allScriptsByOpcodeDo(
const { requestedHatOpcode,
blockId: topBlockId, (script, target) => {
fieldsOfInputs: hatFields const {blockId: topBlockId, fieldsOfInputs: hatFields} =
} = script; script;
// Match any requested fields. // Match any requested fields.
// For example: ensures that broadcasts match. // For example: ensures that broadcasts match.
@ -1844,7 +2018,10 @@ class Runtime extends EventEmitter {
// (i.e., before the predicate can be run) because "broadcast and wait" // (i.e., before the predicate can be run) because "broadcast and wait"
// needs to have a precise collection of started threads. // needs to have a precise collection of started threads.
for (const matchField in optMatchFields) { for (const matchField in optMatchFields) {
if (hatFields[matchField].value !== optMatchFields[matchField]) { if (
hatFields[matchField].value !==
optMatchFields[matchField]
) {
// Field mismatch. // Field mismatch.
return; return;
} }
@ -1854,11 +2031,15 @@ class Runtime extends EventEmitter {
// If `restartExistingThreads` is true, we should stop // If `restartExistingThreads` is true, we should stop
// any existing threads starting with the top block. // any existing threads starting with the top block.
for (let i = 0; i < this.threads.length; i++) { for (let i = 0; i < this.threads.length; i++) {
if (this.threads[i].target === target && if (
this.threads[i].target === target &&
this.threads[i].topBlock === topBlockId && this.threads[i].topBlock === topBlockId &&
// stack click threads and hat threads can coexist // stack click threads and hat threads can coexist
!this.threads[i].stackClick) { !this.threads[i].stackClick
newThreads.push(this._restartThread(this.threads[i])); ) {
newThreads.push(
this._restartThread(this.threads[i])
);
return; return;
} }
} }
@ -1866,11 +2047,13 @@ class Runtime extends EventEmitter {
// If `restartExistingThreads` is false, we should // If `restartExistingThreads` is false, we should
// give up if any threads with the top block are running. // give up if any threads with the top block are running.
for (let j = 0; j < this.threads.length; j++) { for (let j = 0; j < this.threads.length; j++) {
if (this.threads[j].target === target && if (
this.threads[j].target === target &&
this.threads[j].topBlock === topBlockId && this.threads[j].topBlock === topBlockId &&
// stack click threads and hat threads can coexist // stack click threads and hat threads can coexist
!this.threads[j].stackClick && !this.threads[j].stackClick &&
this.threads[j].status !== Thread.STATUS_DONE) { this.threads[j].status !== Thread.STATUS_DONE
) {
// Some thread is already running. // Some thread is already running.
return; return;
} }
@ -1878,7 +2061,9 @@ class Runtime extends EventEmitter {
} }
// Start the thread with this top block. // Start the thread with this top block.
newThreads.push(this._pushThread(topBlockId, target)); newThreads.push(this._pushThread(topBlockId, target));
}, optTarget); },
optTarget
);
// For compatibility with Scratch 2, edge triggered hats need to be processed before // For compatibility with Scratch 2, edge triggered hats need to be processed before
// threads are stepped. See ScratchRuntime.as for original implementation // threads are stepped. See ScratchRuntime.as for original implementation
newThreads.forEach(thread => { newThreads.forEach(thread => {
@ -1888,7 +2073,6 @@ class Runtime extends EventEmitter {
return newThreads; return newThreads;
} }
/** /**
* Dispose all targets. Return to clean state. * Dispose all targets. Return to clean state.
*/ */
@ -1920,8 +2104,10 @@ class Runtime extends EventEmitter {
const newCloudDataManager = cloudDataManager(); const newCloudDataManager = cloudDataManager();
this.hasCloudData = newCloudDataManager.hasCloudVariables; this.hasCloudData = newCloudDataManager.hasCloudVariables;
this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable;
this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); this.addCloudVariable =
this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); this._initializeAddCloudVariable(newCloudDataManager);
this.removeCloudVariable =
this._initializeRemoveCloudVariable(newCloudDataManager);
} }
/** /**
@ -1953,7 +2139,10 @@ class Runtime extends EventEmitter {
newIndex = this.executableTargets.length; newIndex = this.executableTargets.length;
} }
if (newIndex <= 0) { if (newIndex <= 0) {
if (this.executableTargets.length > 0 && this.executableTargets[0].isStage) { if (
this.executableTargets.length > 0 &&
this.executableTargets[0].isStage
) {
newIndex = 1; newIndex = 1;
} else { } else {
newIndex = 0; newIndex = 0;
@ -2033,7 +2222,10 @@ class Runtime extends EventEmitter {
} }
const newRunId = uuid.v1(); const newRunId = uuid.v1();
this.storage.scratchFetch.setMetadata(this.storage.scratchFetch.RequestMetadata.RunId, newRunId); this.storage.scratchFetch.setMetadata(
this.storage.scratchFetch.RequestMetadata.RunId,
newRunId
);
} }
/** /**
@ -2062,8 +2254,13 @@ class Runtime extends EventEmitter {
const newTargets = []; const newTargets = [];
for (let i = 0; i < this.targets.length; i++) { for (let i = 0; i < this.targets.length; i++) {
this.targets[i].onStopAll(); this.targets[i].onStopAll();
if (Object.prototype.hasOwnProperty.call(this.targets[i], 'isOriginal') && if (
!this.targets[i].isOriginal) { Object.prototype.hasOwnProperty.call(
this.targets[i],
'isOriginal'
) &&
!this.targets[i].isOriginal
) {
this.targets[i].dispose(); this.targets[i].dispose();
} else { } else {
newTargets.push(this.targets[i]); newTargets.push(this.targets[i]);
@ -2097,7 +2294,9 @@ class Runtime extends EventEmitter {
// Find all edge-activated hats, and add them to threads to be evaluated. // Find all edge-activated hats, and add them to threads to be evaluated.
for (const hatType in this._hats) { for (const hatType in this._hats) {
if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) continue; if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) {
continue;
}
const hat = this._hats[hatType]; const hat = this._hats[hatType];
if (hat.edgeActivated) { if (hat.edgeActivated) {
this.startHats(hatType); this.startHats(hatType);
@ -2107,7 +2306,9 @@ class Runtime extends EventEmitter {
this._pushMonitors(); this._pushMonitors();
if (this.profiler !== null) { if (this.profiler !== null) {
if (stepThreadsProfilerId === -1) { if (stepThreadsProfilerId === -1) {
stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads'); stepThreadsProfilerId = this.profiler.idByName(
'Sequencer.stepThreads'
);
} }
this.profiler.start(stepThreadsProfilerId); this.profiler.start(stepThreadsProfilerId);
} }
@ -2119,8 +2320,10 @@ class Runtime extends EventEmitter {
// Add done threads so that even if a thread finishes within 1 frame, the green // Add done threads so that even if a thread finishes within 1 frame, the green
// flag will still indicate that a script ran. // flag will still indicate that a script ran.
this._emitProjectRunStatus( this._emitProjectRunStatus(
this.threads.length + doneThreads.length - this.threads.length +
this._getMonitorThreadCount([...this.threads, ...doneThreads])); doneThreads.length -
this._getMonitorThreadCount([...this.threads, ...doneThreads])
);
// Store threads that completed this iteration for testing and other // Store threads that completed this iteration for testing and other
// internal purposes. // internal purposes.
this._lastStepDoneThreads = doneThreads; this._lastStepDoneThreads = doneThreads;
@ -2128,7 +2331,8 @@ class Runtime extends EventEmitter {
// @todo: Only render when this.redrawRequested or clones rendered. // @todo: Only render when this.redrawRequested or clones rendered.
if (this.profiler !== null) { if (this.profiler !== null) {
if (rendererDrawProfilerId === -1) { if (rendererDrawProfilerId === -1) {
rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); rendererDrawProfilerId =
this.profiler.idByName('RenderWebGL.draw');
} }
this.profiler.start(rendererDrawProfilerId); this.profiler.start(rendererDrawProfilerId);
} }
@ -2139,7 +2343,10 @@ class Runtime extends EventEmitter {
} }
if (this._refreshTargets) { if (this._refreshTargets) {
this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */); this.emit(
Runtime.TARGETS_UPDATE,
false /* Don't emit project changed */
);
this._refreshTargets = false; this._refreshTargets = false;
} }
@ -2226,12 +2433,12 @@ class Runtime extends EventEmitter {
if (target === this._editingTarget) { if (target === this._editingTarget) {
const blockForThread = thread.blockGlowInFrame; const blockForThread = thread.blockGlowInFrame;
if (thread.requestScriptGlowInFrame || thread.stackClick) { if (thread.requestScriptGlowInFrame || thread.stackClick) {
let script = target.blocks.getTopLevelScript(blockForThread); let script =
target.blocks.getTopLevelScript(blockForThread);
if (!script) { if (!script) {
// Attempt to find in flyout blocks. // Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript( script =
blockForThread this.flyoutBlocks.getTopLevelScript(blockForThread);
);
} }
if (script) { if (script) {
requestedGlowsThisFrame.push(script); requestedGlowsThisFrame.push(script);
@ -2349,7 +2556,8 @@ class Runtime extends EventEmitter {
*/ */
requestAddMonitor (monitor) { requestAddMonitor (monitor) {
const id = monitor.get('id'); const id = monitor.get('id');
if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state if (!this.requestUpdateMonitor(monitor)) {
// update monitor if it exists in the state
// if the monitor did not exist in the state, add it // if the monitor did not exist in the state, add it
this._monitorState = this._monitorState.set(id, monitor); this._monitorState = this._monitorState.set(id, monitor);
} }
@ -2367,12 +2575,15 @@ class Runtime extends EventEmitter {
if (this._monitorState.has(id)) { if (this._monitorState.has(id)) {
this._monitorState = this._monitorState =
// Use mergeWith here to prevent undefined values from overwriting existing ones // Use mergeWith here to prevent undefined values from overwriting existing ones
this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => { this._monitorState.set(
id,
this._monitorState.get(id).mergeWith((prev, next) => {
if (typeof next === 'undefined' || next === null) { if (typeof next === 'undefined' || next === null) {
return prev; return prev;
} }
return next; return next;
}, monitor)); }, monitor)
);
return true; return true;
} }
return false; return false;
@ -2393,10 +2604,12 @@ class Runtime extends EventEmitter {
* @return {boolean} true if monitor exists and was updated, false otherwise * @return {boolean} true if monitor exists and was updated, false otherwise
*/ */
requestHideMonitor (monitorId) { requestHideMonitor (monitorId) {
return this.requestUpdateMonitor(new Map([ return this.requestUpdateMonitor(
new Map([
['id', monitorId], ['id', monitorId],
['visible', false] ['visible', false]
])); ])
);
} }
/** /**
@ -2406,10 +2619,12 @@ class Runtime extends EventEmitter {
* @return {boolean} true if monitor exists and was updated, false otherwise * @return {boolean} true if monitor exists and was updated, false otherwise
*/ */
requestShowMonitor (monitorId) { requestShowMonitor (monitorId) {
return this.requestUpdateMonitor(new Map([ return this.requestUpdateMonitor(
new Map([
['id', monitorId], ['id', monitorId],
['visible', true] ['visible', true]
])); ])
);
} }
/** /**
@ -2418,7 +2633,9 @@ class Runtime extends EventEmitter {
* @param {!string} targetId Remove all monitors with given target ID. * @param {!string} targetId Remove all monitors with given target ID.
*/ */
requestRemoveMonitorByTargetId (targetId) { requestRemoveMonitorByTargetId (targetId) {
this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId); this._monitorState = this._monitorState.filterNot(
value => value.targetId === targetId
);
} }
/** /**
@ -2538,7 +2755,10 @@ class Runtime extends EventEmitter {
getAllVarNamesOfType (varType) { getAllVarNamesOfType (varType) {
let varNames = []; let varNames = [];
for (const target of this.targets) { for (const target of this.targets) {
const targetVarNames = target.getAllVariableNamesInScopeByType(varType, true); const targetVarNames = target.getAllVariableNamesInScopeByType(
varType,
true
);
varNames = varNames.concat(targetVarNames); varNames = varNames.concat(targetVarNames);
} }
return varNames; return varNames;
@ -2579,7 +2799,8 @@ class Runtime extends EventEmitter {
* @return {Variable} The new variable that was created. * @return {Variable} The new variable that was created.
*/ */
createNewGlobalVariable (variableName, optVarId, optVarType) { createNewGlobalVariable (variableName, optVarId, optVarType) {
const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE; const varType =
typeof optVarType === 'string' ? optVarType : Variable.SCALAR_TYPE;
const allVariableNames = this.getAllVarNamesOfType(varType); const allVariableNames = this.getAllVarNamesOfType(varType);
const newName = StringUtil.unusedName(variableName, allVariableNames); const newName = StringUtil.unusedName(variableName, allVariableNames);
const variable = new Variable(optVarId || uid(), newName, varType); const variable = new Variable(optVarId || uid(), newName, varType);