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
178 changes: 178 additions & 0 deletions packages/scratch-gui/src/lib/ruby-generator/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,44 @@ export default function (Generator) {
return `${index} - 1`;
};

/**
* Get the raw text value from a block's text input.
* @param {object} block - The block containing the input.
* @param {string} inputName - The name of the input (e.g. 'ITEM').
* @returns {string} The raw text value.
*/
const getTextInputValue = function (block, inputName) {
const input = block.inputs && block.inputs[inputName];
if (!input) return '';
const textBlock = Generator.getBlock(input.block);
if (!textBlock || !textBlock.fields || !textBlock.fields.TEXT) return '';
return textBlock.fields.TEXT.value;
};

/**
* Derive the Ruby hash variable name from a keys list name.
* E.g. '$_hash_a_keys_' → '$a', '@_hash_a_keys_' → '@a', '_hash_a_keys_' → 'a'
* @param {string} keysListName - The keys list name.
* @returns {string} The Ruby variable name.
*/
const getHashVarName = function (keysListName) {
let prefix = '';
let name = keysListName;
if (name[0] === '$') {
prefix = '$';
name = name.slice(1);
} else if (name[0] === '@') {
prefix = '@';
name = name.slice(1);
}
// Remove _hash_ prefix and _keys_ suffix
const match = name.match(/^_hash_(.+)_keys_$/);
if (match) {
return `${prefix}${match[1]}`;
}
return keysListName;
};

Generator.data_listcontents = function (block) {
const list = getListName(block);
return [list, Generator.ORDER_COLLECTION];
Expand All @@ -186,13 +224,75 @@ export default function (Generator) {
// Suppressed: handled by data_deletealloflist array literal pattern
return '';
}
if (comment && (comment.includes('@ruby:hash:literal:key:') ||
comment.includes('@ruby:hash:literal:value'))) {
// Suppressed: handled by data_deletealloflist hash literal pattern
return '';
}
if (comment && (comment === '@ruby:hash:set:push:key' ||
comment === '@ruby:hash:set:push:value')) {
// Suppressed: handled by data_deleteoflist hash set pattern
return '';
}

const item = Generator.valueToCode(block, 'ITEM', Generator.ORDER_NONE) || '0';
const list = getListName(block);
return `${list}.push(${Generator.nosToCode(item)})\n`;
};

Generator.data_deleteoflist = function (block) {
const comment = Generator.getCommentText(block);

// Hash set: delete+push pattern
if (comment && comment.includes('@ruby:hash:set:')) {
if (comment.includes('@ruby:hash:set:delete:key')) {
// Suppressed: handled by the first delete block
return '';
}

// This is the first block of the delete+push pattern
// Extract variable name: use @ruby:lvar if present, else derive from list name
const lvarMatch = comment.match(/@ruby:lvar:([^:,\s]+)/);
let hashVarName;
if (lvarMatch) {
hashVarName = lvarMatch[1];
} else {
const valuesListName = getListName(block);
hashVarName = getHashVarName(
valuesListName.replace(/_values_$/, '_keys_')
);
}

// Get the key from the nested data_itemnumoflist
const indexBlockId = block.inputs && block.inputs.INDEX && block.inputs.INDEX.block;
let rawKey = '';
if (indexBlockId) {
const numBlock = Generator.getBlock(indexBlockId);
if (numBlock && numBlock.opcode === 'data_itemnumoflist') {
rawKey = getTextInputValue(numBlock, 'ITEM');
}
}

// Skip to the push:value block (3 blocks ahead: delete:key, push:key, push:value)
let nextId = block.next;
// delete:key
const deleteKeyBlock = Generator.getBlock(nextId);
nextId = deleteKeyBlock.next;
// push:key
const pushKeyBlock = Generator.getBlock(nextId);
nextId = pushKeyBlock.next;
// push:value
const pushValueBlock = Generator.getBlock(nextId);
const value = Generator.valueToCode(pushValueBlock, 'ITEM', Generator.ORDER_NONE) || '0';

if (comment.includes('@ruby:hash:set:sym')) {
const symName = rawKey.slice(1); // remove leading ":"
return `${hashVarName}[:${symName}] = ${Generator.nosToCode(value)}\n`;
}
// @ruby:hash:set:str
return `${hashVarName}["${rawKey}"] = ${Generator.nosToCode(value)}\n`;
}

const index = getListIndex(block);
const list = getListName(block);
return `${list}.delete_at(${Generator.nosToCode(index)})\n`;
Expand All @@ -216,6 +316,51 @@ export default function (Generator) {
return `${list} = [${values.join(', ')}]\n`;
}

if (comment && comment === '@ruby:hash:literal:values') {
// Suppressed: handled by the keys clear block above
return '';
}

const hashLiteralMatch = comment ? comment.match(/@ruby:hash:literal:(\d+)/) : null;
if (hashLiteralMatch) {
const count = parseInt(hashLiteralMatch[1], 10);
// Skip the next block (clear values list)
let nextId = block.next;
const clearValuesBlock = Generator.getBlock(nextId);
nextId = clearValuesBlock.next;

// Derive the variable name from the keys list name
const hashVarName = getHashVarName(list);

const entries = [];
for (let i = 0; i < count; i++) {
// Read key block - get raw text value from ITEM input
const keyBlock = Generator.getBlock(nextId);
const keyComment = Generator.getCommentText(keyBlock);
const rawKey = getTextInputValue(keyBlock, 'ITEM');
nextId = keyBlock.next;

// Read value block
const valueBlock = Generator.getBlock(nextId);
const value = Generator.valueToCode(valueBlock, 'ITEM', Generator.ORDER_NONE) || '0';
nextId = valueBlock.next;

if (keyComment && keyComment.includes('@ruby:hash:literal:key:sym')) {
// Symbol key: ":name" → generate {name: value} syntax
const symName = rawKey.slice(1); // remove leading ":"
entries.push(`${symName}: ${Generator.nosToCode(value)}`);
} else {
// String key: generate {"name" => value} syntax
entries.push(`"${rawKey}" => ${Generator.nosToCode(value)}`);
}
}

if (entries.length === 0) {
return `${hashVarName} = {}\n`;
}
return `${hashVarName} = {${entries.join(', ')}}\n`;
}

return `${list}.clear\n`;
};

Expand All @@ -239,6 +384,39 @@ export default function (Generator) {
const index = Generator.valueToCode(block, 'INDEX', Generator.ORDER_NONE);
return [index, Generator.ORDER_ATOMIC];
}

// Hash get: data_itemoflist with @ruby:hash:get:sym or @ruby:hash:get:str comment
if (comment && comment.includes('@ruby:hash:get:')) {
// Extract variable name: use @ruby:lvar if present, else derive from list name
const lvarMatch = comment.match(/@ruby:lvar:([^:,\s]+)/);
let hashVarName;
if (lvarMatch) {
hashVarName = lvarMatch[1];
} else {
const valuesListName = getListName(block);
hashVarName = getHashVarName(
valuesListName.replace(/_values_$/, '_keys_')
);
}

// Get the key from the nested data_itemnumoflist block
const indexBlockId = block.inputs && block.inputs.INDEX && block.inputs.INDEX.block;
if (indexBlockId) {
const numBlock = Generator.getBlock(indexBlockId);
if (numBlock && numBlock.opcode === 'data_itemnumoflist') {
const rawKey = getTextInputValue(numBlock, 'ITEM');
if (comment.includes('@ruby:hash:get:sym')) {
// Symbol key: ":name" → $a[:name]
const symName = rawKey.slice(1); // remove leading ":"
return [`${hashVarName}[:${symName}]`, Generator.ORDER_FUNCTION_CALL];
}
// String key: "foo" → $a["foo"]
return [`${hashVarName}["${rawKey}"]`, Generator.ORDER_FUNCTION_CALL];

}
}
}

const index = getListIndex(block);
const list = getListName(block);
return [`${list}[${index}]`, Generator.ORDER_FUNCTION_CALL];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ const ExpressionHandlers = {

visitHashNode (node) {
// Prism HashNode has elements which are AssocNode or AssocSplatNode
// Use toJSON().type instead of constructor.name for production build compatibility
const elements = new Map();
node.elements.forEach(element => {
if (element.constructor.name === 'AssocNode') {
if (element.toJSON().type === 'AssocNode') {
elements.set(this.visit(element.key), this.visit(element.value));
}
});
Expand All @@ -188,9 +189,10 @@ const ExpressionHandlers = {
visitKeywordHashNode (node) {
// Prism KeywordHashNode is used for keyword arguments without braces, e.g. foo(secs: 5)
// Elements are AssocNode with SymbolNode keys
// Use toJSON().type instead of constructor.name for production build compatibility
const elements = new Map();
node.elements.forEach(element => {
if (element.constructor.name === 'AssocNode') {
if (element.toJSON().type === 'AssocNode') {
elements.set(this.visit(element.key), this.visit(element.value));
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,42 @@ const VariableUtils = {
return block;
},

/**
* Get the keys list name for a hash variable.
* @param {string} prefixedName - The prefixed variable name (e.g. '$a', '@a', 'a').
* @returns {string} The keys list name (e.g. '$_hash_a_keys_').
*/
_hashKeysListName (prefixedName) {
let prefix = '';
let varName = prefixedName;
if (prefixedName[0] === '$') {
prefix = '$';
varName = prefixedName.slice(1);
} else if (prefixedName[0] === '@') {
prefix = '@';
varName = prefixedName.slice(1);
}
return `${prefix}_hash_${varName}_keys_`;
},

/**
* Get the values list name for a hash variable.
* @param {string} prefixedName - The prefixed variable name (e.g. '$a', '@a', 'a').
* @returns {string} The values list name (e.g. '$_hash_a_values_').
*/
_hashValuesListName (prefixedName) {
let prefix = '';
let varName = prefixedName;
if (prefixedName[0] === '$') {
prefix = '$';
varName = prefixedName.slice(1);
} else if (prefixedName[0] === '@') {
prefix = '@';
varName = prefixedName.slice(1);
}
return `${prefix}_hash_${varName}_values_`;
},

_changeToBooleanArgument (varName) {
varName = varName.toString();
const variable = this._context.localVariables[varName];
Expand Down
Loading
Loading