diff --git a/src/lib/ruby-to-blocks-converter/control.js b/src/lib/ruby-to-blocks-converter/control.js index 10d9e4163e7..3a37b9a767d 100644 --- a/src/lib/ruby-to-blocks-converter/control.js +++ b/src/lib/ruby-to-blocks-converter/control.js @@ -20,36 +20,6 @@ const StopOptions = [ */ const ControlConverter = { - onIf: function (cond, statement, elseStatement) { - const block = this._createBlock('control_if', 'statement'); - if (!this._isFalse(cond)) { - this._addInput(block, 'CONDITION', cond); - } - this._addSubstack(block, statement); - if (elseStatement) { - block.opcode = 'control_if_else'; - this._addSubstack(block, elseStatement, 2); - } - return block; - }, - - onUntil: function (cond, statement) { - statement = this._removeWaitBlocks(statement); - - let opcode; - if (statement === null) { - opcode = 'control_wait_until'; - } else { - opcode = 'control_repeat_until'; - } - const block = this._createBlock(opcode, 'statement'); - if (!this._isFalse(cond)) { - this._addInput(block, 'CONDITION', cond); - } - this._addSubstack(block, statement); - return block; - }, - register: function (converter) { // sleep(duration) - control_wait converter.registerCallMethod('self', 'sleep', 1, params => { @@ -145,6 +115,37 @@ const ControlConverter = { return null; }); + + // Register onXxx handlers + converter.registerOnIf((cond, statement, elseStatement) => { + const block = converter._createBlock('control_if', 'statement'); + if (!converter._isFalse(cond)) { + converter._addInput(block, 'CONDITION', cond); + } + converter._addSubstack(block, statement); + if (elseStatement) { + block.opcode = 'control_if_else'; + converter._addSubstack(block, elseStatement, 2); + } + return block; + }); + + converter.registerOnUntil((cond, statement) => { + statement = converter._removeWaitBlocks(statement); + + let opcode; + if (statement === null) { + opcode = 'control_wait_until'; + } else { + opcode = 'control_repeat_until'; + } + const block = converter._createBlock(opcode, 'statement'); + if (!converter._isFalse(cond)) { + converter._addInput(block, 'CONDITION', cond); + } + converter._addSubstack(block, statement); + return block; + }); } }; diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index ddf3b61e4bf..9e343cf83c0 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -89,27 +89,16 @@ class RubyToBlocksConverter { constructor (vm) { this.vm = vm; this._translator = message => message.defaultMessage; - this._converters = [ - MusicConverter, - PenConverter, - EV3Converter, - GdxForConverter, - SmalrubotS1Converter, - BoostConverter, - TranslateConverter, - MakeyMakeyConverter, - - MotionConverter, - LooksConverter, - SoundConverter, - ControlConverter, - SensingConverter, - OperatorsConverter, - VariablesConverter, - MyBlocksConverter - ]; this._receiverToMethods = {}; this._receiverToMyBlocks = {}; + this._onIfHandlers = []; + this._onUntilHandlers = []; + this._onOpAsgnHandlers = []; + this._onAndHandlers = []; + this._onOrHandlers = []; + this._onVarHandlers = []; + this._onVasgnHandlers = []; + this._onDefsHandlers = []; this.reset(); [ @@ -368,6 +357,38 @@ class RubyToBlocksConverter { this._receiverToMyBlocks[receiverName].push(myBlockHandler); } + registerOnIf (handler) { + this._onIfHandlers.push(handler); + } + + registerOnUntil (handler) { + this._onUntilHandlers.push(handler); + } + + registerOnOpAsgn (handler) { + this._onOpAsgnHandlers.push(handler); + } + + registerOnAnd (handler) { + this._onAndHandlers.push(handler); + } + + registerOnOr (handler) { + this._onOrHandlers.push(handler); + } + + registerOnVar (handler) { + this._onVarHandlers.push(handler); + } + + registerOnVasgn (handler) { + this._onVasgnHandlers.push(handler); + } + + registerOnDefs (handler) { + this._onDefsHandlers.push(handler); + } + callMethod (receiver, name, args, rubyBlockArgs, rubyBlock, node) { const receiverName = this._getReceiverName(receiver); if (!receiverName) return null; @@ -531,8 +552,45 @@ class RubyToBlocksConverter { } _callConvertersHandler (handlerName, ...args) { - for (let i = 0; i < this._converters.length; i++) { - const converter = this._converters[i]; + // First, check registered handlers based on handlerName + const handlersMap = { + onIf: this._onIfHandlers, + onUntil: this._onUntilHandlers, + onOpAsgn: this._onOpAsgnHandlers, + onAnd: this._onAndHandlers, + onOr: this._onOrHandlers, + onVar: this._onVarHandlers, + onVasgn: this._onVasgnHandlers, + onDefs: this._onDefsHandlers + }; + + const handlers = handlersMap[handlerName]; + if (handlers) { + for (const handler of handlers) { + const block = handler.apply(this, args); + if (block) { + return block; + } + } + } + + // Then, check legacy converter objects for remaining unmigrated handlers + const legacyConverters = [ + MusicConverter, + PenConverter, + EV3Converter, + GdxForConverter, + SmalrubotS1Converter, + BoostConverter, + TranslateConverter, + MakeyMakeyConverter, + LooksConverter, + SoundConverter, + SensingConverter + ]; + + for (let i = 0; i < legacyConverters.length; i++) { + const converter = legacyConverters[i]; if (Object.prototype.hasOwnProperty.call(converter, handlerName)) { const block = converter[handlerName].apply(this, args); if (block) { @@ -540,6 +598,7 @@ class RubyToBlocksConverter { } } } + return null; } diff --git a/src/lib/ruby-to-blocks-converter/motion.js b/src/lib/ruby-to-blocks-converter/motion.js index f80eb7d8db6..a225af876bc 100644 --- a/src/lib/ruby-to-blocks-converter/motion.js +++ b/src/lib/ruby-to-blocks-converter/motion.js @@ -139,31 +139,31 @@ const MotionConverter = { // direction getter converter.registerCallMethod('sprite', 'direction', 0, () => converter._createBlock('motion_direction', 'value')); - }, - - // Handle compound assignments like x += value, y += value - onOpAsgn: function (lh, operator, rh) { - let block; - if (this._isBlock(lh) && operator === '+' && this._isNumberOrBlock(rh)) { - let xy; - switch (lh.opcode) { - case 'motion_xposition': - case 'motion_yposition': - // All Motion blocks are sprite-only - if (this._isStage()) { - throw new RubyToBlocksConverterError(lh.node, 'Stage selected: no motion blocks'); - } - if (lh.opcode === 'motion_xposition') { - xy = 'x'; - } else { - xy = 'y'; + + // Register onXxx handlers + converter.registerOnOpAsgn((lh, operator, rh) => { + let block; + if (converter._isBlock(lh) && operator === '+' && converter._isNumberOrBlock(rh)) { + let xy; + switch (lh.opcode) { + case 'motion_xposition': + case 'motion_yposition': + // All Motion blocks are sprite-only + if (converter._isStage()) { + throw new RubyToBlocksConverterError(lh.node, 'Stage selected: no motion blocks'); + } + if (lh.opcode === 'motion_xposition') { + xy = 'x'; + } else { + xy = 'y'; + } + block = converter._changeBlock(lh, `motion_change${xy}by`, 'statement'); + converter._addNumberInput(block, `D${_.toUpper(xy)}`, 'math_number', rh, 10); + break; } - block = this._changeBlock(lh, `motion_change${xy}by`, 'statement'); - this._addNumberInput(block, `D${_.toUpper(xy)}`, 'math_number', rh, 10); - break; } - } - return block; + return block; + }); } }; diff --git a/src/lib/ruby-to-blocks-converter/my-blocks.js b/src/lib/ruby-to-blocks-converter/my-blocks.js index 1e5eef9092d..4de83535912 100644 --- a/src/lib/ruby-to-blocks-converter/my-blocks.js +++ b/src/lib/ruby-to-blocks-converter/my-blocks.js @@ -53,134 +53,133 @@ const MyBlocksConverter = { return block; }); - }, - - // eslint-disable-next-line no-unused-vars - onVar: function (scope, variable) { - let block; - if (scope === 'local') { - let opcode; - let blockType; - if (variable.isBoolean) { - opcode = 'argument_reporter_boolean'; - blockType = 'value_boolean'; - } else { - opcode = 'argument_reporter_string_number'; - blockType = 'value'; - } - // Use normalized variable name (should already be in snake_case lowercase) - const normalizedName = this._toSnakeCaseLowercase(variable.name); - block = this._createBlock(opcode, blockType, { - fields: { - VALUE: { - name: 'VALUE', - value: normalizedName + + // Register onXxx handlers + converter.registerOnVar((scope, variable) => { + let block; + if (scope === 'local') { + let opcode; + let blockType; + if (variable.isBoolean) { + opcode = 'argument_reporter_boolean'; + blockType = 'value_boolean'; + } else { + opcode = 'argument_reporter_string_number'; + blockType = 'value'; + } + // Use normalized variable name (should already be in snake_case lowercase) + const normalizedName = converter._toSnakeCaseLowercase(variable.name); + block = converter._createBlock(opcode, blockType, { + fields: { + VALUE: { + name: 'VALUE', + value: normalizedName + } } + }); + if (Object.prototype.hasOwnProperty.call(converter._context.argumentBlocks, variable.id)) { + converter._context.argumentBlocks[variable.id].push(block.id); + } else { + converter._context.argumentBlocks[variable.id] = [block.id]; } - }); - if (Object.prototype.hasOwnProperty.call(this._context.argumentBlocks, variable.id)) { - this._context.argumentBlocks[variable.id].push(block.id); - } else { - this._context.argumentBlocks[variable.id] = [block.id]; } - } - return block; - }, - - // eslint-disable-next-line no-unused-vars - onDefs: function (node, saved) { - const receiver = this._process(node.children[0]); - if (!this._isSelf(receiver)) { - return null; - } - - const procedureName = node.children[1].toString(); - const block = this._createBlock('procedures_definition', 'hat', { - topLevel: true + return block; }); - const procedure = this._createProcedure(procedureName); - const customBlock = this._createBlock('procedures_prototype', 'statement', { - shadow: true - }); - this._addInput(block, 'custom_block', customBlock); - - this._context.localVariables = {}; - this._process(node.children[2]).forEach(n => { - const originalName = n.toString(); - // Convert argument name to snake_case lowercase - const normalizedName = this._toSnakeCaseLowercase(originalName); - - procedure.argumentNames.push(normalizedName); - procedure.argumentVariables.push(this._lookupOrCreateVariable(normalizedName)); - procedure.procCode.push('%s'); - procedure.argumentDefaults.push(''); - const inputId = Blockly.utils.genUid(); - procedure.argumentIds.push(inputId); - const inputBlock = this._createBlock('argument_reporter_string_number', 'value', { - fields: { - VALUE: { - name: 'VALUE', - value: normalizedName - } - }, + converter.registerOnDefs((node, saved) => { + const receiver = converter._process(node.children[0]); + if (!converter._isSelf(receiver)) { + return null; + } + + const procedureName = node.children[1].toString(); + const block = converter._createBlock('procedures_definition', 'hat', { + topLevel: true + }); + const procedure = converter._createProcedure(procedureName); + + const customBlock = converter._createBlock('procedures_prototype', 'statement', { shadow: true }); - this._addInput(customBlock, inputId, inputBlock); - procedure.argumentBlocks.push(inputBlock); - }); + converter._addInput(block, 'custom_block', customBlock); + + converter._context.localVariables = {}; + converter._process(node.children[2]).forEach(n => { + const originalName = n.toString(); + // Convert argument name to snake_case lowercase + const normalizedName = converter._toSnakeCaseLowercase(originalName); + + procedure.argumentNames.push(normalizedName); + procedure.argumentVariables.push(converter._lookupOrCreateVariable(normalizedName)); + procedure.procCode.push('%s'); + procedure.argumentDefaults.push(''); + const inputId = Blockly.utils.genUid(); + procedure.argumentIds.push(inputId); + const inputBlock = converter._createBlock('argument_reporter_string_number', 'value', { + fields: { + VALUE: { + name: 'VALUE', + value: normalizedName + } + }, + shadow: true + }); + converter._addInput(customBlock, inputId, inputBlock); + procedure.argumentBlocks.push(inputBlock); + }); - let body = this._process(node.children[3]); - if (!_.isArray(body)) { - body = [body]; - } - if (this._isBlock(body[0])) { - block.next = body[0].id; - body[0].parent = block.id; - } - - const booleanIndexes = []; - procedure.argumentVariables.forEach((v, i) => { - if (v.isBoolean) { - booleanIndexes.push(i); - procedure.procCode[i + 1] = '%b'; - procedure.argumentDefaults[i] = 'false'; - procedure.argumentBlocks[i].opcode = 'argument_reporter_boolean'; - this._setBlockType(procedure.argumentBlocks[i], 'value_boolean'); + let body = converter._process(node.children[3]); + if (!_.isArray(body)) { + body = [body]; } - }); + if (converter._isBlock(body[0])) { + block.next = body[0].id; + body[0].parent = block.id; + } + + const booleanIndexes = []; + procedure.argumentVariables.forEach((v, i) => { + if (v.isBoolean) { + booleanIndexes.push(i); + procedure.procCode[i + 1] = '%b'; + procedure.argumentDefaults[i] = 'false'; + procedure.argumentBlocks[i].opcode = 'argument_reporter_boolean'; + converter._setBlockType(procedure.argumentBlocks[i], 'value_boolean'); + } + }); - if (booleanIndexes.length > 0 && - Object.prototype.hasOwnProperty.call(this._context.procedureCallBlocks, procedure.id)) { - this._context.procedureCallBlocks[procedure.id].forEach(id => { - const b = this._context.blocks[id]; - b.mutation.proccode = procedure.procCode.join(' '); - booleanIndexes.forEach(booleanIndex => { - const input = b.inputs[procedure.argumentIds[booleanIndex]]; - const inputBlock = this._context.blocks[input.block]; - if (inputBlock) { - if (!inputBlock.shadow && input.shadow) { - delete this._context.blocks[input.shadow]; - input.shadow = null; + if (booleanIndexes.length > 0 && + Object.prototype.hasOwnProperty.call(converter._context.procedureCallBlocks, procedure.id)) { + converter._context.procedureCallBlocks[procedure.id].forEach(id => { + const b = converter._context.blocks[id]; + b.mutation.proccode = procedure.procCode.join(' '); + booleanIndexes.forEach(booleanIndex => { + const input = b.inputs[procedure.argumentIds[booleanIndex]]; + const inputBlock = converter._context.blocks[input.block]; + if (inputBlock) { + if (!inputBlock.shadow && input.shadow) { + delete converter._context.blocks[input.shadow]; + input.shadow = null; + } } - } + }); }); - }); - } + } - customBlock.mutation = { - argumentdefaults: JSON.stringify(procedure.argumentDefaults), - argumentids: JSON.stringify(procedure.argumentIds), - argumentnames: JSON.stringify(procedure.argumentNames), - children: [], - proccode: procedure.procCode.join(' '), - tagName: 'mutation', - warp: 'false' - }; + customBlock.mutation = { + argumentdefaults: JSON.stringify(procedure.argumentDefaults), + argumentids: JSON.stringify(procedure.argumentIds), + argumentnames: JSON.stringify(procedure.argumentNames), + children: [], + proccode: procedure.procCode.join(' '), + tagName: 'mutation', + warp: 'false' + }; - this._restoreContext({localVariables: saved.localVariables}); + converter._restoreContext({localVariables: saved.localVariables}); - return block; + return block; + }); } }; diff --git a/src/lib/ruby-to-blocks-converter/operators.js b/src/lib/ruby-to-blocks-converter/operators.js index cc6fac92b4d..fda180c5809 100644 --- a/src/lib/ruby-to-blocks-converter/operators.js +++ b/src/lib/ruby-to-blocks-converter/operators.js @@ -221,40 +221,39 @@ const OperatorsConverter = { converter._addNumberInput(block, 'NUM', 'math_number', rh, ''); return block; }); - }, - - // eslint-disable-next-line no-unused-vars - onAnd: function (operands) { - const block = this._createBlock('operator_and', 'value_boolean'); - operands.forEach(o => { - if (o) { - o.parent = block.id; + + // Register onXxx handlers + converter.registerOnAnd(operands => { + const block = converter._createBlock('operator_and', 'value_boolean'); + operands.forEach(o => { + if (o) { + o.parent = block.id; + } + }); + if (!converter._isFalse(operands[0])) { + converter._addInput(block, 'OPERAND1', converter._createTextBlock(operands[0])); } + if (!converter._isFalse(operands[1])) { + converter._addInput(block, 'OPERAND2', converter._createTextBlock(operands[1])); + } + return block; }); - if (!this._isFalse(operands[0])) { - this._addInput(block, 'OPERAND1', this._createTextBlock(operands[0])); - } - if (!this._isFalse(operands[1])) { - this._addInput(block, 'OPERAND2', this._createTextBlock(operands[1])); - } - return block; - }, - - // eslint-disable-next-line no-unused-vars - onOr: function (operands) { - const block = this._createBlock('operator_or', 'value_boolean'); - operands.forEach(o => { - if (o) { - o.parent = block.id; + + converter.registerOnOr(operands => { + const block = converter._createBlock('operator_or', 'value_boolean'); + operands.forEach(o => { + if (o) { + o.parent = block.id; + } + }); + if (!converter._isFalse(operands[0])) { + converter._addInput(block, 'OPERAND1', converter._createTextBlock(operands[0])); } + if (!converter._isFalse(operands[1])) { + converter._addInput(block, 'OPERAND2', converter._createTextBlock(operands[1])); + } + return block; }); - if (!this._isFalse(operands[0])) { - this._addInput(block, 'OPERAND1', this._createTextBlock(operands[0])); - } - if (!this._isFalse(operands[1])) { - this._addInput(block, 'OPERAND2', this._createTextBlock(operands[1])); - } - return block; } }; diff --git a/src/lib/ruby-to-blocks-converter/variables.js b/src/lib/ruby-to-blocks-converter/variables.js index 71dfc40c13b..c08f7634e0c 100644 --- a/src/lib/ruby-to-blocks-converter/variables.js +++ b/src/lib/ruby-to-blocks-converter/variables.js @@ -191,52 +191,32 @@ const VariablesConverter = { ); return block; }); - }, - // eslint-disable-next-line no-unused-vars - onOpAsgn: function (lh, operator, rh) { - let block; - if (operator === '+' && this._isString(lh) && this._isNumberOrBlock(rh)) { - const variable = this._lookupOrCreateVariable(lh); - if (variable.scope !== 'local') { - block = this._createBlock('data_changevariableby', 'statement', { - fields: { - VARIABLE: { - name: 'VARIABLE', - id: variable.id, - value: variable.name, - variableType: variable.type + // Register onXxx handlers + converter.registerOnOpAsgn((lh, operator, rh) => { + let block; + if (operator === '+' && converter._isString(lh) && converter._isNumberOrBlock(rh)) { + const variable = converter._lookupOrCreateVariable(lh); + if (variable.scope !== 'local') { + block = converter._createBlock('data_changevariableby', 'statement', { + fields: { + VARIABLE: { + name: 'VARIABLE', + id: variable.id, + value: variable.name, + variableType: variable.type + } } - } - }); - this._addNumberInput(block, 'VALUE', 'math_number', rh, 1); - } - } - return block; - }, - - // eslint-disable-next-line no-unused-vars - onVar: function (scope, variable) { - if (scope === 'global' || scope === 'instance') { - return this._createBlock('data_variable', 'value_variable', { - fields: { - VARIABLE: { - name: 'VARIABLE', - id: variable.id, - value: variable.name, - variableType: variable.type - } + }); + converter._addNumberInput(block, 'VALUE', 'math_number', rh, 1); } - }); - } - return null; - }, + } + return block; + }); - // eslint-disable-next-line no-unused-vars - onVasgn: function (scope, variable, rh) { - if (scope === 'global' || scope === 'instance') { - if (this._isNumberOrBlock(rh) || this._isStringOrBlock(rh)) { - const block = this._createBlock('data_setvariableto', 'statement', { + converter.registerOnVar((scope, variable) => { + if (scope === 'global' || scope === 'instance') { + return converter._createBlock('data_variable', 'value_variable', { fields: { VARIABLE: { name: 'VARIABLE', @@ -246,11 +226,29 @@ const VariablesConverter = { } } }); - this._addTextInput(block, 'VALUE', this._isNumber(rh) ? rh.toString() : rh, '0'); - return block; } - } - return null; + return null; + }); + + converter.registerOnVasgn((scope, variable, rh) => { + if (scope === 'global' || scope === 'instance') { + if (converter._isNumberOrBlock(rh) || converter._isStringOrBlock(rh)) { + const block = converter._createBlock('data_setvariableto', 'statement', { + fields: { + VARIABLE: { + name: 'VARIABLE', + id: variable.id, + value: variable.name, + variableType: variable.type + } + } + }); + converter._addTextInput(block, 'VALUE', converter._isNumber(rh) ? rh.toString() : rh, '0'); + return block; + } + } + return null; + }); } };