From bf133c73dc0d12637800164f93d523b85c045665 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 00:59:27 +0900 Subject: [PATCH 1/7] feat: convert print/puts/p to say blocks with comments - Implemented comment support in RubyToBlocksConverter - Added handlers for print, puts, and p in LooksConverter - Added unit tests verifying conversion and comment attachment Closes #527 Co-Authored-By: Gemini --- src/lib/ruby-to-blocks-converter/index.js | 29 +++++++++++++++++ src/lib/ruby-to-blocks-converter/looks.js | 11 +++++++ .../ruby-to-blocks-converter/looks.test.js | 32 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index 0cb6023d871..5579cf254d4 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -162,6 +162,7 @@ class RubyToBlocksConverter { extensionIDs: new Set(), blocks: {}, + comments: {}, blockTypes: {}, localVariables: {}, variables: {}, @@ -283,11 +284,17 @@ class RubyToBlocksConverter { Object.keys(target.blocks._blocks).forEach(blockId => { target.blocks.deleteBlock(blockId); }); + target.comments = {}; Object.keys(this._context.blocks).forEach(blockId => { target.blocks.createBlock(this._context.blocks[blockId]); }); + Object.keys(this._context.comments).forEach(commentId => { + const comment = this._context.comments[commentId]; + target.createComment(comment.id, comment.blockId, comment.text, comment.x, comment.y, comment.width, comment.height, comment.minimized); + }); + this.vm.emitWorkspaceUpdate(); }); } @@ -878,6 +885,25 @@ class RubyToBlocksConverter { return null; } + createComment (text, blockId) { + return this._createComment(text, blockId); + } + + _createComment (text, blockId) { + const id = Blockly.utils.genUid(); + this._context.comments[id] = { + id: id, + text: text, + blockId: blockId, + x: 0, + y: 0, + width: 200, + height: 200, + minimized: false + }; + return id; + } + createBlock (opcode, type, attributes = {}) { return this._createBlock(opcode, type, attributes); } @@ -895,6 +921,9 @@ class RubyToBlocksConverter { x: void 0, y: void 0 }, attributes); + if (attributes.comment) { + block.comment = this._createComment(attributes.comment, block.id); + } this._context.blocks[block.id] = block; this._context.blockTypes[block.id] = type; return block; diff --git a/src/lib/ruby-to-blocks-converter/looks.js b/src/lib/ruby-to-blocks-converter/looks.js index e6d4e2d0731..78751a4b718 100644 --- a/src/lib/ruby-to-blocks-converter/looks.js +++ b/src/lib/ruby-to-blocks-converter/looks.js @@ -79,6 +79,17 @@ const validateBackdrop = function (backdropName, args) { */ const LooksConverter = { register: function (converter) { + ['print', 'puts', 'p'].forEach(methodName => { + converter.registerOnSend('sprite', methodName, 1, params => { + const {args} = params; + if (!converter._isNumberOrStringOrBlock(args[0])) return null; + + const block = createBlockWithMessage.call(converter, 'looks_say', args[0], 'Hello!'); + block.comment = converter.createComment(`@smalruby:${methodName}`, block.id); + return block; + }); + }); + ['say', 'think'].forEach(methodName => { converter.registerOnSend('sprite', methodName, 1, params => { const {args} = params; diff --git a/test/unit/lib/ruby-to-blocks-converter/looks.test.js b/test/unit/lib/ruby-to-blocks-converter/looks.test.js index aa8752f0e8f..cfd6bb91bfd 100644 --- a/test/unit/lib/ruby-to-blocks-converter/looks.test.js +++ b/test/unit/lib/ruby-to-blocks-converter/looks.test.js @@ -1182,4 +1182,36 @@ describe('RubyToBlocksConverter/Looks', () => { }); }); }); + + describe('print, puts, p', () => { + ['print', 'puts', 'p'].forEach(method => { + test(`${method}("Hello") should become looks_say with comment`, () => { + code = `${method}("Hello")`; + expected = [ + { + opcode: 'looks_say', + inputs: [ + { + name: 'MESSAGE', + block: expectedInfo.makeText('Hello') + } + ] + } + ]; + + // First verify blocks structure + convertAndExpectToEqualBlocks(converter, target, code, expected); + + // Then verify comment + // We need to find the block that is 'looks_say' (it should be the first/only top level block) + const blockId = Object.keys(converter.blocks).find(id => converter.blocks[id].opcode === 'looks_say'); + const block = converter.blocks[blockId]; + expect(block.comment).toBeDefined(); + + const commentId = block.comment; + expect(converter._context.comments[commentId]).toBeDefined(); + expect(converter._context.comments[commentId].text).toEqual(`@smalruby:${method}`); + }); + }); + }); }); From 3315db70f20b1f20c08793d3f6b315df06593e74 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 01:27:18 +0900 Subject: [PATCH 2/7] feat: convert looks_say back to print/puts/p using meta-comments - Updated Ruby generator to detect '@smalruby:method' comments on looks_say blocks - Implemented conversion back to print, puts, or p based on the meta-comment - Added filtering in the generator to hide @smalruby: meta-comments from output - Added unit tests for looks_say conversion and meta-comment filtering Fixes #528 --- src/lib/ruby-generator/index.js | 9 +- src/lib/ruby-generator/looks.js | 7 ++ test/unit/lib/ruby-generator/looks.test.js | 127 +++++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 test/unit/lib/ruby-generator/looks.test.js diff --git a/src/lib/ruby-generator/index.js b/src/lib/ruby-generator/index.js index 4f08122298b..0745f5a7ca1 100644 --- a/src/lib/ruby-generator/index.js +++ b/src/lib/ruby-generator/index.js @@ -356,7 +356,7 @@ RubyGenerator.scrub_ = function (block, code) { let commentCode = ''; if (!this.isConnectedValue(block)) { let comment = this.getCommentText(block); - if (comment) { + if (comment && !comment.startsWith('@smalruby:')) { commentCode += `${this.prefixLines(comment, '# ')}\n`; } const inputs = this.getInputs(block); @@ -366,7 +366,12 @@ RubyGenerator.scrub_ = function (block, code) { if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { - commentCode += this.prefixLines(comment, '# '); + const filteredComment = comment.split('\n') + .filter(line => !line.startsWith('@smalruby:')) + .join('\n'); + if (filteredComment.trim().length > 0) { + commentCode += this.prefixLines(filteredComment, '# '); + } } } } diff --git a/src/lib/ruby-generator/looks.js b/src/lib/ruby-generator/looks.js index 801de41c0c9..f65a1b8d369 100644 --- a/src/lib/ruby-generator/looks.js +++ b/src/lib/ruby-generator/looks.js @@ -12,6 +12,13 @@ export default function (Generator) { Generator.looks_say = function (block) { const message = Generator.valueToCode(block, 'MESSAGE', Generator.ORDER_NONE) || Generator.quote_(''); + const comment = Generator.getCommentText(block); + if (comment && comment.startsWith('@smalruby:')) { + const methodName = comment.substring(10); + if (['print', 'puts', 'p'].includes(methodName)) { + return `${methodName}(${message})\n`; + } + } return `say(${message})\n`; }; diff --git a/test/unit/lib/ruby-generator/looks.test.js b/test/unit/lib/ruby-generator/looks.test.js new file mode 100644 index 00000000000..c9b93eadcbc --- /dev/null +++ b/test/unit/lib/ruby-generator/looks.test.js @@ -0,0 +1,127 @@ +import RubyGenerator from '../../../../src/lib/ruby-generator'; +import LooksBlocks from '../../../../src/lib/ruby-generator/looks'; + +describe('RubyGenerator/Looks', () => { + beforeEach(() => { + RubyGenerator.cache_ = { + comments: {}, + targetCommentTexts: [] + }; + RubyGenerator.definitions_ = {}; + RubyGenerator.functionNames_ = {}; + RubyGenerator.currentTarget = null; + LooksBlocks(RubyGenerator); + }); + + describe('looks_say', () => { + test('normal say', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'say("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @smalruby:print', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:print' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'print("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @smalruby:puts', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:puts' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'puts("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with @smalruby:p', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:p' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'p("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + + test('with unknown @smalruby: tag defaults to say', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: { + MESSAGE: {} + } + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:unknown' }; + RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); + const expected = 'say("Hello!")\n'; + expect(RubyGenerator.looks_say(block)).toEqual(expected); + }); + }); + + describe('scrub_ (meta-comment filtering)', () => { + test('should filter out @smalruby: comments', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: {}, + next: null + }; + RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:print' }; + RubyGenerator.getInputs = jest.fn().mockReturnValue({}); + RubyGenerator.isConnectedValue = jest.fn().mockReturnValue(false); + RubyGenerator.getBlock = jest.fn().mockReturnValue(null); + RubyGenerator.blockToCode = jest.fn().mockReturnValue(''); + + const code = 'print("Hello!")\n'; + const result = RubyGenerator.scrub_(block, code); + + // Should NOT contain the comment since it starts with @smalruby: + expect(result).toEqual('print("Hello!")\n'); + }); + + test('should keep normal comments', () => { + const block = { + id: 'block-id', + opcode: 'looks_say', + inputs: {}, + next: null + }; + RubyGenerator.cache_.comments['block-id'] = { text: 'normal comment' }; + RubyGenerator.getInputs = jest.fn().mockReturnValue({}); + RubyGenerator.isConnectedValue = jest.fn().mockReturnValue(false); + RubyGenerator.getBlock = jest.fn().mockReturnValue(null); + RubyGenerator.blockToCode = jest.fn().mockReturnValue(''); + + const code = 'say("Hello!")\n'; + const result = RubyGenerator.scrub_(block, code); + + expect(result).toEqual('# normal comment\nsay("Hello!")\n'); + }); + }); +}); From ff10fc5b420d388ae13bf75622ee51778bd572b6 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 01:33:07 +0900 Subject: [PATCH 3/7] fix: resolve max-len lint error in applyTargetBlocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split long target.createComment() call into multiple lines to comply with 120 character max-len rule. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lib/ruby-to-blocks-converter/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index 5579cf254d4..46027f5ccf6 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -292,7 +292,10 @@ class RubyToBlocksConverter { Object.keys(this._context.comments).forEach(commentId => { const comment = this._context.comments[commentId]; - target.createComment(comment.id, comment.blockId, comment.text, comment.x, comment.y, comment.width, comment.height, comment.minimized); + target.createComment( + comment.id, comment.blockId, comment.text, + comment.x, comment.y, comment.width, comment.height, comment.minimized + ); }); this.vm.emitWorkspaceUpdate(); From 3936746275fdd60234ac8e59c5222f3abb0c1299 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 07:00:49 +0900 Subject: [PATCH 4/7] feat: improve comment positioning and minimized state - Updated _createComment to support minimized parameter (default true) and x/y coordinates - Updated Looks converter to place meta-comments at (200, 0) and minimize them - Added unit tests for comment positioning and minimization --- src/lib/ruby-to-blocks-converter/index.js | 22 ++++++++++++++----- src/lib/ruby-to-blocks-converter/looks.js | 2 +- .../ruby-to-blocks-converter/looks.test.js | 5 +++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index 46027f5ccf6..43c64d38d42 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -888,21 +888,21 @@ class RubyToBlocksConverter { return null; } - createComment (text, blockId) { - return this._createComment(text, blockId); + createComment (text, blockId, x = 0, y = 0, minimized = true) { + return this._createComment(text, blockId, x, y, minimized); } - _createComment (text, blockId) { + _createComment (text, blockId, x = 0, y = 0, minimized = true) { const id = Blockly.utils.genUid(); this._context.comments[id] = { id: id, text: text, blockId: blockId, - x: 0, - y: 0, + x: x, + y: y, width: 200, height: 200, - minimized: false + minimized: minimized }; return id; } @@ -1448,6 +1448,7 @@ class RubyToBlocksConverter { let prevBlock = null; const result = []; + let currentY = 0; blocks.forEach(block => { switch (this._getBlockType(block)) { case 'statement': @@ -1456,6 +1457,15 @@ class RubyToBlocksConverter { block.parent = prevBlock.id; } else { result.push(block); + block.x = 0; + block.y = currentY; + if (block.comment) { + const comment = this._context.comments[block.comment]; + if (comment) { + comment.y = block.y; + } + } + currentY += 48; } if (block.next) { const b = this._lastBlock(block); diff --git a/src/lib/ruby-to-blocks-converter/looks.js b/src/lib/ruby-to-blocks-converter/looks.js index 78751a4b718..f68b2e1f1b1 100644 --- a/src/lib/ruby-to-blocks-converter/looks.js +++ b/src/lib/ruby-to-blocks-converter/looks.js @@ -85,7 +85,7 @@ const LooksConverter = { if (!converter._isNumberOrStringOrBlock(args[0])) return null; const block = createBlockWithMessage.call(converter, 'looks_say', args[0], 'Hello!'); - block.comment = converter.createComment(`@smalruby:${methodName}`, block.id); + block.comment = converter.createComment(`@smalruby:${methodName}`, block.id, 200, 0); return block; }); }); diff --git a/test/unit/lib/ruby-to-blocks-converter/looks.test.js b/test/unit/lib/ruby-to-blocks-converter/looks.test.js index cfd6bb91bfd..af45dec20b9 100644 --- a/test/unit/lib/ruby-to-blocks-converter/looks.test.js +++ b/test/unit/lib/ruby-to-blocks-converter/looks.test.js @@ -1207,10 +1207,15 @@ describe('RubyToBlocksConverter/Looks', () => { const blockId = Object.keys(converter.blocks).find(id => converter.blocks[id].opcode === 'looks_say'); const block = converter.blocks[blockId]; expect(block.comment).toBeDefined(); + expect(block.x).toEqual(0); + expect(block.y).toEqual(0); const commentId = block.comment; expect(converter._context.comments[commentId]).toBeDefined(); expect(converter._context.comments[commentId].text).toEqual(`@smalruby:${method}`); + expect(converter._context.comments[commentId].x).toEqual(200); + expect(converter._context.comments[commentId].y).toEqual(0); + expect(converter._context.comments[commentId].minimized).toBe(true); }); }); }); From 318c064c12f70367dfe00cee0ea677b4c9468f43 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 07:34:22 +0900 Subject: [PATCH 5/7] fix: adjust comment Y position and minimize state - Updated _onBegin and targetCodeToBlocks to align comment Y-coordinate with block Y-coordinate - Incremented Y-coordinate for sequential statement blocks to prevent overlap - Relaxed test helpers to allow x/y coordinates in blocks --- src/lib/ruby-to-blocks-converter/index.js | 31 ++++++++++++++++++----- test/helpers/expect-to-equal-blocks.js | 8 ++++-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index 43c64d38d42..deafc8853a5 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -188,8 +188,24 @@ class RubyToBlocksConverter { if (!_.isArray(blocks)) { blocks = [blocks]; } + let currentY = 0; blocks.forEach(block => { if (this._isBlock(block)) { + if (this._isStatementBlock(block)) { + if (block.x === void 0) { + block.x = 0; + } + if (block.y === void 0) { + block.y = currentY; + } + if (block.comment) { + const comment = this._context.comments[block.comment]; + if (comment) { + comment.y = block.y; + } + } + currentY += 48; + } block.topLevel = true; } else if (block instanceof Primitive) { throw new RubyToBlocksConverterError( @@ -1452,6 +1468,12 @@ class RubyToBlocksConverter { blocks.forEach(block => { switch (this._getBlockType(block)) { case 'statement': + if (block.comment) { + const comment = this._context.comments[block.comment]; + if (comment) { + comment.y = currentY; + } + } if (prevBlock) { prevBlock.next = block.id; block.parent = prevBlock.id; @@ -1459,14 +1481,9 @@ class RubyToBlocksConverter { result.push(block); block.x = 0; block.y = currentY; - if (block.comment) { - const comment = this._context.comments[block.comment]; - if (comment) { - comment.y = block.y; - } - } - currentY += 48; } + currentY += 48; + if (block.next) { const b = this._lastBlock(block); if (this._getBlockType(b) === 'statement') { diff --git a/test/helpers/expect-to-equal-blocks.js b/test/helpers/expect-to-equal-blocks.js index 2ea8a96bb96..69f6760d15a 100644 --- a/test/helpers/expect-to-equal-blocks.js +++ b/test/helpers/expect-to-equal-blocks.js @@ -182,8 +182,12 @@ const expectToEqualBlock = function (context, parent, actualBlock, expectedBlock expect(blocks.getOpcode(block)).toEqual(expected.opcode); expect(block.parent).toEqual(parent); expect(block.shadow).toEqual(expected.shadow === true); - expect(block.x).toEqual(void 0); - expect(block.y).toEqual(void 0); + if (expected.x !== void 0) { + expect(block.x).toEqual(expected.x); + } + if (expected.y !== void 0) { + expect(block.y).toEqual(expected.y); + } expectToEqualFields(context, blocks.getFields(block), expected.fields); From 3ffda3c8358b1375327f736f2d0ba6767ca12086 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 12:01:51 +0900 Subject: [PATCH 6/7] fix: correct block comment misalignment after Ruby to blocks conversion - Remove manual coordinate calculation in RubyToBlocksConverter to trigger GUI auto-layout. - Implement post-layout comment repositioning in Blocks container using workspace.cleanUp(). - Update VM state with new block and comment coordinates after layout. - Update unit tests to omit assertions on block coordinates at the converter level. Co-Authored-By: Gemini --- src/containers/blocks.jsx | 23 ++++++++++++++++ src/lib/ruby-to-blocks-converter/index.js | 26 ------------------- .../ruby-to-blocks-converter/looks.test.js | 2 -- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 0fcd17a82f0..bcfc21fecc9 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -470,6 +470,29 @@ class Blocks extends React.Component { if (fromRuby) { this.workspace.cleanUp(); + // Re-calculate the position of the comments. + this.workspace.getTopComments(false).forEach(comment => { + if (comment.blockId) { + const block = this.workspace.getBlockById(comment.blockId); + if (block) { + const blockXY = block.getRelativeToSurfaceXY(); + const blockHW = block.getHeightWidth(); + const rtl = this.workspace.RTL; + const x = rtl ? + blockXY.x - blockHW.width - 20 - comment.getWidth() : + blockXY.x + blockHW.width + 20; + const y = blockXY.y; + comment.moveTo(x, y); + + const targetComments = this.props.vm.editingTarget.comments; + if (targetComments && targetComments[comment.id]) { + targetComments[comment.id].x = x; + targetComments[comment.id].y = y; + } + } + } + }); + this.workspace.getTopBlocks(false).forEach(wsTopBlock => { const topBlock = blocks.getBlock(wsTopBlock.id); if (topBlock) { diff --git a/src/lib/ruby-to-blocks-converter/index.js b/src/lib/ruby-to-blocks-converter/index.js index deafc8853a5..74c03ac6505 100644 --- a/src/lib/ruby-to-blocks-converter/index.js +++ b/src/lib/ruby-to-blocks-converter/index.js @@ -188,24 +188,8 @@ class RubyToBlocksConverter { if (!_.isArray(blocks)) { blocks = [blocks]; } - let currentY = 0; blocks.forEach(block => { if (this._isBlock(block)) { - if (this._isStatementBlock(block)) { - if (block.x === void 0) { - block.x = 0; - } - if (block.y === void 0) { - block.y = currentY; - } - if (block.comment) { - const comment = this._context.comments[block.comment]; - if (comment) { - comment.y = block.y; - } - } - currentY += 48; - } block.topLevel = true; } else if (block instanceof Primitive) { throw new RubyToBlocksConverterError( @@ -1464,25 +1448,15 @@ class RubyToBlocksConverter { let prevBlock = null; const result = []; - let currentY = 0; blocks.forEach(block => { switch (this._getBlockType(block)) { case 'statement': - if (block.comment) { - const comment = this._context.comments[block.comment]; - if (comment) { - comment.y = currentY; - } - } if (prevBlock) { prevBlock.next = block.id; block.parent = prevBlock.id; } else { result.push(block); - block.x = 0; - block.y = currentY; } - currentY += 48; if (block.next) { const b = this._lastBlock(block); diff --git a/test/unit/lib/ruby-to-blocks-converter/looks.test.js b/test/unit/lib/ruby-to-blocks-converter/looks.test.js index af45dec20b9..94151b8f85a 100644 --- a/test/unit/lib/ruby-to-blocks-converter/looks.test.js +++ b/test/unit/lib/ruby-to-blocks-converter/looks.test.js @@ -1207,8 +1207,6 @@ describe('RubyToBlocksConverter/Looks', () => { const blockId = Object.keys(converter.blocks).find(id => converter.blocks[id].opcode === 'looks_say'); const block = converter.blocks[blockId]; expect(block.comment).toBeDefined(); - expect(block.x).toEqual(0); - expect(block.y).toEqual(0); const commentId = block.comment; expect(converter._context.comments[commentId]).toBeDefined(); From c76d86075c5f931bfae94ba9773f01fac29d8d5a Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 18 Jan 2026 13:08:25 +0900 Subject: [PATCH 7/7] feat: use new meta-comment format @ruby:method: for print/puts/p MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update RubyToBlocksConverter to use @ruby:method: directive - Update RubyGenerator to recognize @ruby:method: and filter @ruby: prefix - Remove legacy @smalruby: prefix support and tests as compatibility is not required - Update unit tests to reflect format changes 🤖 Generated with [Gemini Code](https://gemini.google.com/code) Co-Authored-By: Gemini --- src/lib/ruby-generator/index.js | 4 ++-- src/lib/ruby-generator/looks.js | 10 +++++---- src/lib/ruby-to-blocks-converter/looks.js | 2 +- test/unit/lib/ruby-generator/looks.test.js | 22 +++++++++---------- .../ruby-to-blocks-converter/looks.test.js | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lib/ruby-generator/index.js b/src/lib/ruby-generator/index.js index 0745f5a7ca1..36487ac5fe6 100644 --- a/src/lib/ruby-generator/index.js +++ b/src/lib/ruby-generator/index.js @@ -356,7 +356,7 @@ RubyGenerator.scrub_ = function (block, code) { let commentCode = ''; if (!this.isConnectedValue(block)) { let comment = this.getCommentText(block); - if (comment && !comment.startsWith('@smalruby:')) { + if (comment && !comment.startsWith('@ruby:')) { commentCode += `${this.prefixLines(comment, '# ')}\n`; } const inputs = this.getInputs(block); @@ -367,7 +367,7 @@ RubyGenerator.scrub_ = function (block, code) { comment = this.allNestedComments(childBlock); if (comment) { const filteredComment = comment.split('\n') - .filter(line => !line.startsWith('@smalruby:')) + .filter(line => !line.startsWith('@ruby:')) .join('\n'); if (filteredComment.trim().length > 0) { commentCode += this.prefixLines(filteredComment, '# '); diff --git a/src/lib/ruby-generator/looks.js b/src/lib/ruby-generator/looks.js index f65a1b8d369..83ca0ba333b 100644 --- a/src/lib/ruby-generator/looks.js +++ b/src/lib/ruby-generator/looks.js @@ -13,10 +13,12 @@ export default function (Generator) { Generator.looks_say = function (block) { const message = Generator.valueToCode(block, 'MESSAGE', Generator.ORDER_NONE) || Generator.quote_(''); const comment = Generator.getCommentText(block); - if (comment && comment.startsWith('@smalruby:')) { - const methodName = comment.substring(10); - if (['print', 'puts', 'p'].includes(methodName)) { - return `${methodName}(${message})\n`; + if (comment) { + if (comment.startsWith('@ruby:method:')) { + const methodName = comment.substring(13); + if (['print', 'puts', 'p'].includes(methodName)) { + return `${methodName}(${message})\n`; + } } } return `say(${message})\n`; diff --git a/src/lib/ruby-to-blocks-converter/looks.js b/src/lib/ruby-to-blocks-converter/looks.js index f68b2e1f1b1..2371c2899bb 100644 --- a/src/lib/ruby-to-blocks-converter/looks.js +++ b/src/lib/ruby-to-blocks-converter/looks.js @@ -85,7 +85,7 @@ const LooksConverter = { if (!converter._isNumberOrStringOrBlock(args[0])) return null; const block = createBlockWithMessage.call(converter, 'looks_say', args[0], 'Hello!'); - block.comment = converter.createComment(`@smalruby:${methodName}`, block.id, 200, 0); + block.comment = converter.createComment(`@ruby:method:${methodName}`, block.id, 200, 0); return block; }); }); diff --git a/test/unit/lib/ruby-generator/looks.test.js b/test/unit/lib/ruby-generator/looks.test.js index c9b93eadcbc..b6b239f1c4e 100644 --- a/test/unit/lib/ruby-generator/looks.test.js +++ b/test/unit/lib/ruby-generator/looks.test.js @@ -27,7 +27,7 @@ describe('RubyGenerator/Looks', () => { expect(RubyGenerator.looks_say(block)).toEqual(expected); }); - test('with @smalruby:print', () => { + test('with @ruby:method:print', () => { const block = { id: 'block-id', opcode: 'looks_say', @@ -35,13 +35,13 @@ describe('RubyGenerator/Looks', () => { MESSAGE: {} } }; - RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:print' }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:print' }; RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); const expected = 'print("Hello!")\n'; expect(RubyGenerator.looks_say(block)).toEqual(expected); }); - test('with @smalruby:puts', () => { + test('with @ruby:method:puts', () => { const block = { id: 'block-id', opcode: 'looks_say', @@ -49,13 +49,13 @@ describe('RubyGenerator/Looks', () => { MESSAGE: {} } }; - RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:puts' }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:puts' }; RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); const expected = 'puts("Hello!")\n'; expect(RubyGenerator.looks_say(block)).toEqual(expected); }); - test('with @smalruby:p', () => { + test('with @ruby:method:p', () => { const block = { id: 'block-id', opcode: 'looks_say', @@ -63,13 +63,13 @@ describe('RubyGenerator/Looks', () => { MESSAGE: {} } }; - RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:p' }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:p' }; RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); const expected = 'p("Hello!")\n'; expect(RubyGenerator.looks_say(block)).toEqual(expected); }); - test('with unknown @smalruby: tag defaults to say', () => { + test('with unknown @ruby: tag defaults to say', () => { const block = { id: 'block-id', opcode: 'looks_say', @@ -77,7 +77,7 @@ describe('RubyGenerator/Looks', () => { MESSAGE: {} } }; - RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:unknown' }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:unknown' }; RubyGenerator.valueToCode = jest.fn().mockReturnValue('"Hello!"'); const expected = 'say("Hello!")\n'; expect(RubyGenerator.looks_say(block)).toEqual(expected); @@ -85,14 +85,14 @@ describe('RubyGenerator/Looks', () => { }); describe('scrub_ (meta-comment filtering)', () => { - test('should filter out @smalruby: comments', () => { + test('should filter out @ruby: comments', () => { const block = { id: 'block-id', opcode: 'looks_say', inputs: {}, next: null }; - RubyGenerator.cache_.comments['block-id'] = { text: '@smalruby:print' }; + RubyGenerator.cache_.comments['block-id'] = { text: '@ruby:method:print' }; RubyGenerator.getInputs = jest.fn().mockReturnValue({}); RubyGenerator.isConnectedValue = jest.fn().mockReturnValue(false); RubyGenerator.getBlock = jest.fn().mockReturnValue(null); @@ -101,7 +101,7 @@ describe('RubyGenerator/Looks', () => { const code = 'print("Hello!")\n'; const result = RubyGenerator.scrub_(block, code); - // Should NOT contain the comment since it starts with @smalruby: + // Should NOT contain the comment since it starts with @ruby: expect(result).toEqual('print("Hello!")\n'); }); diff --git a/test/unit/lib/ruby-to-blocks-converter/looks.test.js b/test/unit/lib/ruby-to-blocks-converter/looks.test.js index 94151b8f85a..aa822106c79 100644 --- a/test/unit/lib/ruby-to-blocks-converter/looks.test.js +++ b/test/unit/lib/ruby-to-blocks-converter/looks.test.js @@ -1210,7 +1210,7 @@ describe('RubyToBlocksConverter/Looks', () => { const commentId = block.comment; expect(converter._context.comments[commentId]).toBeDefined(); - expect(converter._context.comments[commentId].text).toEqual(`@smalruby:${method}`); + expect(converter._context.comments[commentId].text).toEqual(`@ruby:method:${method}`); expect(converter._context.comments[commentId].x).toEqual(200); expect(converter._context.comments[commentId].y).toEqual(0); expect(converter._context.comments[commentId].minimized).toBe(true);