Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/scratch-gui/src/lib/ruby-generator/smalruby-ruby.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export default function (Generator) {
Generator.quote_('');
const arg2 = Generator.valueToCode(block, 'ARG2', order);

// Operator methods: generate infix notation (e.g. receiver * arg)
if (method === '*') {
return `${receiver} ${method} ${arg1}`;
}

const args = [arg1];
if (arg2) args.push(arg2);

Expand Down Expand Up @@ -73,7 +78,7 @@ export default function (Generator) {
// Pattern: a line ending with .method_call (optionally with args),
// followed by a line that references _rv_ or _rv_truthy_
const methodPattern =
'\\S[^\\n]*\\.(?:reverse|upcase|downcase|empty\\?|lines|delete|gsub|max|min|sort|join|first|last|keys|values|reverse!|delete!|gsub!|sort!)(?:\\([^)]*\\))?';
'\\S[^\\n]*(?:\\.(?:reverse|upcase|downcase|empty\\?|lines|delete|gsub|max|min|sort|join|first|last|keys|values|reverse!|delete!|gsub!|sort!)(?:\\([^)]*\\))?| \\* \\S+)';
// Process _rv_truthy_ BEFORE _rv_ to avoid partial match
code = code.replace(
new RegExp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ const stringMethodArgs = {
ARG2: { type: 'string', defaultValue: 'r' },
},
},
'*': {
text: 'String [RECEIVER] . [METHOD] ( [ARG1] )',
arguments: {
RECEIVER: { type: 'string', defaultValue: 'hello' },
METHOD: {
type: 'string',
menu: 'stringMethodMenu',
defaultValue: '*',
},
ARG1: { type: 'string', defaultValue: '3' },
},
},
};

const stringMethodMenuItems = {
Expand All @@ -172,6 +184,7 @@ const stringMethodMenuItems = {
['reverse!', 'reverse!'],
['delete!', 'delete!'],
['gsub!', 'gsub!'],
['*', '*'],
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ const SmalrubyRubyConverter = {
});
converter._addTextInput(block, 'RECEIVER', receiver, 'string');
converter._addField(block, 'METHOD', method);
const addArg = (name, value, defaultVal) => {
if (converter._isNumber(value)) {
converter._addNumberInput(block, name, 'math_number', value, 0);
} else {
converter._addTextInput(block, name, value, defaultVal);
}
};
if (args.length > 0) {
converter._addTextInput(block, 'ARG1', args[0], '');
addArg('ARG1', args[0], '');
}
if (args.length > 1) {
converter._addTextInput(block, 'ARG2', args[1], '');
addArg('ARG2', args[1], '');
}
return block;
};
Expand Down Expand Up @@ -110,6 +117,18 @@ const SmalrubyRubyConverter = {
);
// 1-arg string methods
registerStringMethod('delete', 1);
// String#* — only for string literal receivers to avoid conflict with numeric *
converter.registerOnSend(['string'], '*', 1, (params) => {
const { receiver, args } = params;
return createMethodBlock(
'smalrubyRuby_stringMethod',
'*',
receiver,
args,
stringMethodArgs,
stringMethodMenuItems,
);
});
// 2-arg string methods
registerStringMethod('gsub', 2);
// bang methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ describe('RubyGenerator/SmalrubyRuby', () => {
RubyGenerator.smalrubyRuby_stringMethod(block);
expect(result).toEqual('name.reverse!\n');
});

test('should generate * operator as infix', () => {
RubyGenerator.valueToCode = (block, name, _order) => {
const map = { RECEIVER: '"Jimmy"', ARG1: '5' };
return map[name] || '';
};
const block = {
opcode: 'smalrubyRuby_stringMethod',
fields: { METHOD: { value: '*' } },
inputs: { ARG1: {} },
next: null,
};
const result =
RubyGenerator.smalrubyRuby_stringMethod(block);
expect(result).toEqual('"Jimmy" * 5\n');
});
});

describe('returnValue', () => {
Expand Down Expand Up @@ -151,5 +167,12 @@ describe('RubyGenerator/SmalrubyRuby', () => {
const result = RubyGenerator.finishTargets(input, {});
expect(result).toEqual(' say(books.keys)\n');
});

test('should inline * operator', () => {
const input =
' "Jimmy" * 5\n say(_rv_, 2)\n';
const result = RubyGenerator.finishTargets(input, {});
expect(result).toEqual(' say("Jimmy" * 5, 2)\n');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ describe('Ruby Roundtrip: smalrubyRuby extension', () => {
);
});

test('String#* with say', async () => {
await expectRoundTrip(
converter,
target,
dedent`
when_flag_clicked do
say("Jimmy" * 5, 2)
end
`,
null,
opts,
);
});

test('Hash#keys with say', async () => {
await expectRoundTrip(
converter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ describe('RubyToBlocksConverter/Operators', () => {
];
await convertAndExpectToEqualBlocks(converter, target, code, expected);

// "1" * 2 is now valid (String#*), "1" * "2" is also handled by String#*
// Only numeric receiver with string arg remains an error
{ for (const s of [
'"1" * "2"',
'1 * "2"',
'"1" * 2'
'1 * "2"'
]) {
await convertAndExpectRubyBlockError(converter, target, s);
} }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ describe('RubyToBlocksConverter/SmalrubyRuby', () => {
const result = await converter.targetCodeToBlocks(target, code);
expect(result).toBe(true);
});
test('should convert "Jimmy" * 5', async () => {
const code = '"Jimmy" * 5';
const result = await converter.targetCodeToBlocks(target, code);
expect(result).toBe(true);
const blocks = Object.values(converter._context.blocks);
const methodBlock = blocks.find(
(b) => b.opcode === 'smalrubyRuby_stringMethod',
);
expect(methodBlock).toBeTruthy();
expect(methodBlock.fields.METHOD.value).toBe('*');
});
});

describe('arrayMethod', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ const stringMethodArgumentsByMethod = {
ARG2: { type: ArgumentType.STRING, defaultValue: 'r' },
},
},
'*': {
text: 'String [RECEIVER] . [METHOD] ( [ARG1] )',
arguments: {
RECEIVER: {
type: ArgumentType.STRING,
defaultValue: 'hello',
},
METHOD: {
type: ArgumentType.STRING,
menu: 'stringMethodMenu',
defaultValue: '*',
},
ARG1: { type: ArgumentType.STRING, defaultValue: '3' },
},
},
};

const stringMethodMenuItems = [
Expand All @@ -165,6 +180,7 @@ const stringMethodMenuItems = [
['reverse!', 'reverse!'],
['delete!', 'delete!'],
['gsub!', 'gsub!'],
['*', '*'],
];

const arrayMethodArgumentsByMethod = {
Expand Down Expand Up @@ -391,6 +407,7 @@ const menus = {
{ text: 'reverse!', value: 'reverse!' },
{ text: 'delete!', value: 'delete!' },
{ text: 'gsub!', value: 'gsub!' },
{ text: '*', value: '*' },
],
},
arrayMethodMenu: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const executeStringMethod = (args, util, setReturnValue) => {
? string
: string.replaceAll(String(arg1 || ''), String(arg2));
break;
case '*':
result = string.repeat(Math.max(0, Math.floor(Number(arg1) || 0)));
break;
default:
result = string;
}
Expand Down
Loading