diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2b17dba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,96 @@ +# Changelog + +## 1.0.0 + +- Initial release + +## 1.0.1 + +- Styled the warning modal +- Made the BlockType enum full caps instead of lowercase + +## 1.0.2 + +- Added various new blocks such as strings and math + +## 1.0.3 + +- More blocks, such as percentage, user info, and more + +## 1.0.4 + +- Fixed some blocks with inconsistent case + +## 1.0.5 + +- Fixed extension naming bug +- Added more documentation +- Fixed the color: removed alpha + +## 1.0.6 + +- Fixed menu related issues +- Added better colors +- Fixed boolean to text block +- Changed the substring block example + +## 1.0.7 + +- Lots of new reporters, commands, and a few loop blocks +- Added more documentation +- Fixed bugs related to the for loop block + +## 1.0.8 + +- Clarified warning screen +- Removed legacy labels +- Added more blocks +- Fixed some modal styles + +## 1.0.9 + +- Fix the console blocks +- Add more blocks and a block counter +- Fix localstorage blocks +- Add developer tools + +## 1.1.0 + +- Added more blocks, and the Omni-reporter block +- Fixed some bugs +- Modularized the extension: split into multiple files +- Fix some issues related to block handling +- Reformat code +- Remove unnecessary blocks +- Add and then revert "Don't Show Again" option on modal +- Added Unicode category + +## 1.1.1 + +- Redid the VM finder +- Added BigNum category +- Cleaned up some logging from earlier versions + +## 1.1.2 + +- Labeled categories +- Fixed format number block +- Added more blocks + +## 1.1.3 + +- Added view mode for the modal +- Added more to the warning modal, for example a message to project developers +- Added a "ScratchJS Enabled" block + +## 1.1.4 + +- Updated extension tutorial +- Addon support +- Some more blocks +- Fixed a few bugs and improved stability + +## 1.1.5 + +- Added variables category +- Added VariableManager class for variable management \ No newline at end of file diff --git a/README.md b/README.md index d7cf23e..d8e29f7 100644 --- a/README.md +++ b/README.md @@ -16,570 +16,3 @@ Go to the Scratch editor and click the bookmarklet to load the extension. ## Safety Notice When ScratchJS loads, you will see a warning modal indicating that this extension has access to advanced features. Only use projects with ScratchJS from creators you trust, as projects can potentially perform dangerous operations. - -## Block Categories - -### Math & Numbers - -#### `[base] ^ [exponent]` -**Type:** Reporter -**Returns:** Base raised to the power of exponent - -**Parameters:** -- `base` (number, default: 2) - Base number -- `exponent` (number, default: 3) - Exponent - -**Example:** `2 ^ 3` returns 8 - -#### `Clamp [value] between [min] and [max]` -**Type:** Reporter -**Returns:** Value constrained within min and max range - -**Parameters:** -- `value` (number, default: 15) - Value to clamp -- `min` (number, default: 0) - Minimum value -- `max` (number, default: 10) - Maximum value - -**Example:** `Clamp 15 between 0 and 10` returns 10 - -#### `Round [number] to [decimals] decimal places` -**Type:** Reporter -**Returns:** Number rounded to specified decimal places - -**Parameters:** -- `number` (number, default: 3.14159) - Number to round -- `decimals` (number, default: 2) - Number of decimal places - -**Example:** `Round 3.14159 to 2 decimal places` returns 3.14 - -#### `[part]% of [whole]` -**Type:** Reporter -**Returns:** Percentage calculation - -**Parameters:** -- `part` (number, default: 25) - Percentage value -- `whole` (number, default: 100) - Whole value - -**Example:** `25% of 100` returns 25 - -#### `[value]++` -**Type:** Reporter -**Returns:** Value incremented by 1 - -**Parameters:** -- `value` (number, default: 5) - Value to increment - -**Example:** `5++` returns 6 - -#### `[value]--` -**Type:** Reporter -**Returns:** Value decremented by 1 - -**Parameters:** -- `value` (number, default: 5) - Value to decrement - -**Example:** `5--` returns 4 - -#### `[value1] >= [value2]` -**Type:** Boolean -**Returns:** True if value1 is greater than or equal to value2 - -**Parameters:** -- `value1` (number, default: 5) - First value -- `value2` (number, default: 5) - Second value - -#### `[value1] <= [value2]` -**Type:** Boolean -**Returns:** True if value1 is less than or equal to value2 - -**Parameters:** -- `value1` (number, default: 5) - First value -- `value2` (number, default: 5) - Second value - -#### `[value1] ≠ [value2]` -**Type:** Boolean -**Returns:** True if values are not equal - -**Parameters:** -- `value1` (number, default: 5) - First value -- `value2` (number, default: 5) - Second value - -### Boolean & Constants - -#### `True` -**Type:** Boolean -**Returns:** Always returns true - -#### `False` -**Type:** Boolean -**Returns:** Always returns false - -### String & Text Manipulation - -#### `Replace all [string] in [text] with [replace]` -**Type:** Reporter -**Returns:** Text with all occurrences of string replaced - -**Parameters:** -- `text` (string, default: "Hello World") - Original text -- `string` (string, default: "World") - String to replace -- `replace` (string, default: "Scratch") - Replacement string - -**Example:** `Replace all "World" in "Hello World" with "Scratch"` returns "Hello Scratch" - -#### `Get substring of [text] from [start] to [end]` -**Type:** Reporter -**Returns:** Portion of text between specified positions (1-based indexing) - -**Parameters:** -- `text` (string, default: "Hello World") - Original text -- `start` (number, default: 1) - Start position (1-based) -- `end` (number, default: 6) - End position (1-based) - -**Example:** `Get substring of "Hello World" from 1 to 6` returns "Hello" - -#### `Reverse string [text]` -**Type:** Reporter -**Returns:** Text reversed character by character - -**Parameters:** -- `text` (string, default: "Hello World") - Text to reverse - -**Example:** `Reverse string "Hello"` returns "olleH" - -#### `Convert [text] to case [caseType]` -**Type:** Reporter -**Returns:** Text converted to specified case - -**Parameters:** -- `text` (string, default: "Hello World") - Text to convert -- `caseType` (menu) - Case conversion type: - - `UPPERCASE` - Convert to all uppercase - - `lowercase` - Convert to all lowercase - -**Example:** `Convert "hello world" to case UPPERCASE` returns "HELLO WORLD" - -#### `Newline` -**Type:** Reporter -**Returns:** Newline character (\n) - -#### `Tab` -**Type:** Reporter -**Returns:** Tab character (\t) - -### Date & Time - -#### `current [format]` -**Type:** Reporter -**Returns:** Current date/time in specified format - -**Options:** -- `date and time` - Full date and time string -- `date only` - Current date (locale format) -- `time only` - Current time (locale format) -- `timestamp` - Unix timestamp in milliseconds - -**Example:** `current date and time` returns "3/17/2026, 8:06:00 PM" - -#### `Current project ID` -**Type:** Reporter -**Returns:** The current Scratch project ID from the URL - -**Example:** If URL is `https://scratch.mit.edu/projects/123456/`, returns "123456" - -### System Information - -#### `Get info on the [what]` -**Type:** Reporter -**Returns:** Various system and browser information - -**Options:** -- `operating system` - OS platform (e.g., "Win32") -- `browser` - Browser user agent string -- `language` - Browser language (e.g., "en-US") -- `time zone` - User's timezone (e.g., "America/New_York") -- `screen width` - Screen width in pixels -- `screen height` - Screen height in pixels -- `window width` - Browser window width in pixels -- `window height` - Browser window height in pixels -- `device pixel ratio` - Device pixel ratio - -**Example:** `Get info on the operating system` returns "Win32" - -### JavaScript Execution - -#### `JS| Run JS code [code]` -**Type:** Command -**Action:** Executes the given JavaScript code - -**Parameters:** -- `code` (string, default: "alert('Hello World!')") - JavaScript code to execute - -**Warning:** This block can execute any JavaScript code and should be used with caution. - -#### `JS| Get return value of [code]` -**Type:** Reporter -**Returns:** The return value of the executed JavaScript code - -**Parameters:** -- `code` (string, default: "6473 / 84") - JavaScript code to execute and return value from - -**Example:** `JS| Get return value of "Math.random()"` returns a random number between 0 and 1 - -#### `JS| Set variable [name] to [val]` -**Type:** Command -**Action:** Creates or updates a JavaScript variable - -**Parameters:** -- `name` (string, default: "window.example") - Variable name -- `val` (string, default: "Hello World!") - Value to assign - -**Example:** `JS| Set variable "window.myVar" to "test"` creates a global variable - -### File & Web Operations - -#### `JS| Open site [url]` -**Type:** Command -**Action:** Opens the specified URL in a new browser tab - -**Parameters:** -- `url` (string, default: "https://example.com") - URL to open - -**Example:** `JS| Open site "https://scratch.mit.edu"` opens Scratch in a new tab - -#### `JS| Open this project in Turbowarp` -**Type:** Command -**Action:** Opens the current project in Turbowarp - -#### `JS| Reload page` -**Type:** Command -**Action:** Reloads the current page - -#### `JS| Save file [name] with contents [contents]` -**Type:** Command -**Action:** Downloads a text file with the specified name and contents - -**Parameters:** -- `name` (string, default: "example.txt") - Filename -- `contents` (string, default: "Hello World!") - File contents - -**Example:** `JS| Save file "data.txt" with contents "Hello from Scratch!"` downloads a text file - -### Control Flow & Loops - -#### `when [condit] is true` -**Type:** Hat -**Triggers:** When the condition becomes true - -**Parameters:** -- `condit` (boolean) - Condition to monitor - -**Example:** `when "mouse down?" is true` triggers when mouse is pressed - -#### `For i in [value]` -**Type:** Conditional -**Action:** Loop control block that increments i and checks if i <= value - -**Parameters:** -- `value` (string, default: "10") - Maximum value for loop - -**Use:** Works with the i reporter and set i blocks for custom loops - -#### `i` -**Type:** Reporter -**Returns:** Current value of loop counter i - -#### `Set i to [value]` -**Type:** Command -**Action:** Sets the loop counter i to specified value - -**Parameters:** -- `value` (number, default: 0) - Value to set i to - -### Mouse & Input - -#### `Mouse X (works out of bounds)` -**Type:** Reporter -**Returns:** Mouse X coordinate (works even when mouse is outside stage) - -#### `Mouse Y (works out of bounds)` -**Type:** Reporter -**Returns:** Mouse Y coordinate (works even when mouse is outside stage) - -#### `Mouse down? (works out of bounds)` -**Type:** Boolean -**Returns:** True if mouse button is pressed (works even when mouse is outside stage) - -### Boolean & Type Conversion - -#### `[bool]` -**Type:** Boolean -**Returns:** Boolean value converted from text - -**Parameters:** -- `bool` (string, default: "true") - Text to convert to boolean - -**Accepts:** "true", "1", "True", or any non-empty string (except "0", "false", "False") returns true - -#### `[bool]` -**Type:** Reporter -**Returns:** Text representation of boolean value - -**Parameters:** -- `bool` (boolean) - Boolean to convert to text - -**Example:** Converts true to "true" and false to "false" - -### Utility Blocks - -#### `[arg1]` -**Type:** Reporter -**Returns:** The input argument unchanged - -**Parameters:** -- `arg1` (string, default: "Hello") - Input value - -**Use:** Useful for type conversion or passing values through blocks - -#### `if [arg1] then [arg2] else [arg3]` -**Type:** Reporter -**Returns:** arg2 if arg1 is true, otherwise arg3 - -**Parameters:** -- `arg1` (boolean) - Condition -- `arg2` (string, default: "Hello") - Value if true -- `arg3` (string, default: "World") - Value if false - -**Example:** `if "mouse down?" then "Pressed" else "Released"` returns "Pressed" when mouse is down - -### Array Operations - -#### `Blank array` -**Type:** Reporter -**Returns:** Empty array as JSON string "[]" - -#### `Append [value] to array [array]` -**Type:** Reporter -**Returns:** Array with value appended to end - -**Parameters:** -- `value` (string, default: "Hello") - Value to append -- `array` (string, default: "[]") - Array as JSON string - -**Example:** `Append "World" to array "[\"Hello\"]"` returns "[\"Hello\",\"World\"]" - -#### `Get [index] from array [array]` -**Type:** Reporter -**Returns:** Item at specified index (1-based) - -**Parameters:** -- `index` (number, default: 1) - Index (1-based) -- `array` (string, default: "[]") - Array as JSON string - -**Example:** `Get 2 from array "[\"Apple\",\"Banana\"]"` returns "Banana" - -#### `Insert [value] at [index] in array [array]` -**Type:** Reporter -**Returns:** Array with value inserted at specified position - -**Parameters:** -- `value` (string, default: "Hello") - Value to insert -- `index` (number, default: 1) - Index position (1-based) -- `array` (string, default: "[\"Apple\"]") - Array as JSON string - -#### `Replace [index] in array [array] with [value]` -**Type:** Reporter -**Returns:** Array with item at index replaced - -**Parameters:** -- `index` (number, default: 1) - Index to replace (1-based) -- `array` (string, default: "[\"Apple\"]") - Array as JSON string -- `value` (string, default: "Banana") - New value - -#### `Remove [index] from array [array]` -**Type:** Reporter -**Returns:** Array with item at specified index removed - -**Parameters:** -- `index` (number, default: 1) - Index to remove (1-based) -- `array` (string, default: "[\"Apple\"]") - Array as JSON string - -#### `Merge [array1] and [array2]` -**Type:** Reporter -**Returns:** Two arrays combined into one - -**Parameters:** -- `array1` (string, default: "[\"Hello\"]") - First array -- `array2` (string, default: "[\"World\"]") - Second array - -#### `Length of array [array]` -**Type:** Reporter -**Returns:** Number of items in array - -**Parameters:** -- `array` (string, default: "[\"Apple\", \"Banana\"]") - Array as JSON string - -#### `Array [array] contains [value]` -**Type:** Boolean -**Returns:** True if array contains the specified value - -**Parameters:** -- `array` (string, default: "[\"Apple\", \"Banana\"]") - Array as JSON string -- `value` (string, default: "Carrot") - Value to search for - -#### `Index of [value] in array [array]` -**Type:** Reporter -**Returns:** Index of first occurrence of value (1-based, returns 0 if not found) - -**Parameters:** -- `value` (string, default: "Hello") - Value to find -- `array` (string, default: "[\"Apple\"]") - Array as JSON string - -#### `Split [string] by [delimiter] into array` -**Type:** Reporter -**Returns:** String split into array by delimiter - -**Parameters:** -- `string` (string, default: "Hello, World") - String to split -- `delimiter` (string, default: ",") - Delimiter character - -**Example:** `Split "Hello, World" by "," into array` returns "[\"Hello\",\" World\"]" - -#### `Join array [array] with [delimiter]` -**Type:** Reporter -**Returns:** Array elements joined into string with delimiter - -**Parameters:** -- `array` (string, default: "[\"Hello\", \"World\"]") - Array as JSON string -- `delimiter` (string, default: ",") - Delimiter character - -**Example:** `Join array "[\"Hello\",\"World\"]" with ","` returns "Hello,World" - -### Object Operations - -#### `Blank object` -**Type:** Reporter -**Returns:** Empty object as JSON string "{}" - -#### `Set [key] in object [object] to [value]` -**Type:** Reporter -**Returns:** Object with key set to value - -**Parameters:** -- `key` (string, default: "name") - Property key -- `object` (string, default: "{}") - Object as JSON string -- `value` (string, default: "John") - Value to set - -**Example:** `Set "name" in object "{}" to "John"` returns "{\"name\":\"John\"}" - -#### `Get [key] from object [object]` -**Type:** Reporter -**Returns:** Value of specified key from object - -**Parameters:** -- `key` (string, default: "name") - Property key -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -**Example:** `Get "name" from object "{\"name\": \"John\"}"` returns "John" - -#### `Delete [key] from object [object]` -**Type:** Reporter -**Returns:** Object with specified key removed - -**Parameters:** -- `key` (string, default: "name") - Property key to delete -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -#### `Object [object] has key [key]` -**Type:** Boolean -**Returns:** True if object contains the specified key - -**Parameters:** -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string -- `key` (string, default: "name") - Key to check - -#### `Keys of object [object] (array)` -**Type:** Reporter -**Returns:** Array of object's keys as JSON string - -**Parameters:** -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -**Example:** `Keys of object "{\"name\": \"John\"}" (array)` returns "[\"name\"]" - -#### `Values of object [object] (array)` -**Type:** Reporter -**Returns:** Array of object's values as JSON string - -**Parameters:** -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -**Example:** `Values of object "{\"name\": \"John\"}" (array)` returns "[\"John\"]" - -#### `Entries of object [object] (array)` -**Type:** Reporter -**Returns:** Array of [key, value] pairs as JSON string - -**Parameters:** -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -**Example:** `Entries of object "{\"name\": \"John\"}" (array)` returns "[[\"name\",\"John\"]]" - -#### `Size of object [object]` -**Type:** Reporter -**Returns:** Number of keys in object - -**Parameters:** -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -#### `Get path (array) [path] from object [object]` -**Type:** Reporter -**Returns:** Value at nested path in object - -**Parameters:** -- `path` (string, default: "[\"name\"]") - Path as JSON array -- `object` (string, default: "{\"name\": \"John\"}") - Object as JSON string - -**Example:** `Get path "[\"name\"]" from object "{\"name\": \"John\"}"` returns "John" - -#### `Set path (array) [path] in object [object] to [value]` -**Type:** Reporter -**Returns:** Object with nested path set to value - -**Parameters:** -- `path` (string, default: "[\"name\"]") - Path as JSON array -- `object` (string, default: "{}") - Object as JSON string -- `value` (string, default: "John") - Value to set - -**Example:** `Set path "[\"name\"]" in object "{}" to "John"` returns "{\"name\":\"John\"}" - -## Technical Details - -### Block Types Used -- **REPORTER**: Returns a value, can be used in round inputs -- **COMMAND**: Performs an action, can be stacked -- **HAT**: Event-triggered block, runs code below when condition is met -- **BOOLEAN**: Returns true/false, can be used in boolean inputs - -### Argument Types Used -- **string**: Accepts any text or reporter block -- **number**: Accepts numbers or reporter blocks -- **Boolean**: Accepts only boolean blocks -- **menu**: Dropdown menu with predefined options - -### Security Considerations -ScratchJS provides powerful JavaScript execution capabilities. Users should: -- Only use projects from trusted creators -- Be cautious with blocks that execute arbitrary JavaScript -- Understand that file operations and web access are available - -## Development - -ScratchJS is built using the Scratch extension system. The extension: -1. Waits for the Scratch VM to be available -2. Creates a new ScratchJS extension instance -3. Registers all blocks with the Scratch runtime -4. Provides JavaScript execution environment - -For more information on creating Scratch extensions, see `extensiontutorial.md`. - diff --git a/dist/bundle.js b/dist/bundle.js index 681dbe9..6c15ccf 100644 --- a/dist/bundle.js +++ b/dist/bundle.js @@ -204,6 +204,151 @@ } }); + // scratchjsblocks/arrays.js + var require_arrays = __commonJS({ + "scratchjsblocks/arrays.js"(exports, module) { + window.sjs_arrays = [ + Block(BlockType.BUTTON, "arraysCategory", "Arrays"), + Block(BlockType.BOOLEAN, "isValidJson", "Is [text] valid JSON?", { + text: Argument("string", '{"key":"value"}') + }, ({ text }) => { + try { + JSON.parse(text); + return true; + } catch (e) { + return false; + } + }), + Spacer, + Block(BlockType.REPORTER, "blankArray", "ARRAY | Blank array", {}, () => "[]"), + Block(BlockType.REPORTER, "addToArray", "ARRAY | Append [value] to array [array]", { + value: Argument("string", "Hello"), + array: Argument("string", "[]") + }, ({ array: array2, value }) => JSON.stringify([...tryParse(array2), value])), + Block(BlockType.REPORTER, "getFromArray", "ARRAY | Get [index] from array [array]", { + index: Argument("number", 1), + array: Argument("string", "[]") + }, ({ array: array2, index }) => tryParse(array2)[--index]), + Block(BlockType.REPORTER, "insertIntoArray", "ARRAY | Insert [value] at [index] in array [array]", { + value: Argument("string", "Hello"), + index: Argument("number", 1), + array: Argument("string", '["Apple"]') + }, ({ array: array2, index, value }) => { + const arr2 = tryParse(array2); + arr2.splice(--index, 0, value); + return JSON.stringify(arr2); + }), + Block(BlockType.REPORTER, "replaceInArray", "ARRAY | Replace [index] in array [array] with [value]", { + index: Argument("number", 1), + array: Argument("string", '["Apple"]'), + value: Argument("string", "Banana") + }, ({ array: array2, index, value }) => { + const arr2 = tryParse(array2); + arr2[--index] = value; + return JSON.stringify(arr2); + }), + Block(BlockType.REPORTER, "removeFromArray", "ARRAY | Remove [index] from array [array]", { + index: Argument("number", 1), + array: Argument("string", '["Apple"]') + }, ({ array: array2, index }) => { + const arr2 = tryParse(array2); + arr2.splice(--index, 1); + return JSON.stringify(arr2); + }), + Block(BlockType.REPORTER, "mergeArrays", "ARRAY | Merge [array1] and [array2]", { + array1: Argument("string", '["Hello"]'), + array2: Argument("string", '["World"]') + }, ({ array1, array2 }) => JSON.stringify([...tryParse(array1), ...tryParse(array2)])), + Block(BlockType.REPORTER, "lengthOfArray", "ARRAY | Length of array [array]", { + array: Argument("string", '["Apple", "Banana"]') + }, ({ array: array2 }) => tryParse(array2).length), + Block(BlockType.BOOLEAN, "arrayHas", "ARRAY | Array [array] contains [value]", { + array: Argument("string", '["Apple", "Banana"]'), + value: Argument("string", "Carrot") + }, ({ array: array2, value }) => tryParse(array2).includes(value)), + Block(BlockType.REPORTER, "indexOf", "ARRAY | Index of [value] in array [array]", { + value: Argument("string", "Hello"), + array: Argument("string", '["Apple"]') + }, ({ array: array2, value }) => { + const index = tryParse(array2).indexOf(value); + return index === -1 ? 0 : index + 1; + }), + Block(BlockType.REPORTER, "splitString", "ARRAY | Split [string] by [delimiter] into array", { + string: Argument("string", "Hello, World"), + delimiter: Argument("string", ",") + }, ({ string, delimiter }) => JSON.stringify(string.split(delimiter))), + Block(BlockType.REPORTER, "joinArray", "ARRAY | Join array [array] with [delimiter]", { + array: Argument("string", '["Hello", "World"]'), + delimiter: Argument("string", ",") + }, ({ array: array2, delimiter }) => tryParse(array2).join(delimiter)), + Block(BlockType.REPORTER, "swapArrayItems", "ARRAY | Swap [index1] and [index2] in array [array]", { + index1: Argument("number", 1), + index2: Argument("number", 2), + array: Argument("string", '["Apple", "Banana"]') + }, ({ index1, index2, array: array2 }) => { + let res; + try { + res = JSON.parse(array2); + } catch { + return array2; + } + const temp = res[index1 - 1]; + res[index1 - 1] = res[index2 - 1]; + res[index2 - 1] = temp; + return JSON.stringify(res); + }), + Block(BlockType.REPORTER, "getItemsFrom", "ARRAY | Get items from [start] to [end] from array [array]", { + start: Argument("number", 2), + end: Argument("number", 3), + array: Argument("string", '["Apple", "Banana", "Carrot"]') + }, ({ start, end, array: array2 }) => { + let res; + try { + res = JSON.parse(array2); + } catch { + return array2; + } + return JSON.stringify(res.slice(start - 1, end - 1)); + }), + Block(BlockType.LOOP, "arrayLoop", "ARRAY | For each item in array [array] do", { + array: Argument("string", '["Apple", "Banana"]') + }, ({ array: array2 }, util) => { + let parsed = tryParse(array2); + if (!window.sjs_inArrLoop) { + window.sjs_arri = 0; + } + if (++window.sjs_arri <= parsed.length) { + window.sjs_inArrLoop = true; + window.sjs_currentArray = array2; + window.sjs_currentItem = parsed[window.sjs_arri - 1]; + util.startBranch(1, true); + } else { + window.sjs_arri = 0; + window.sjs_inArrLoop = false; + } + }), + Block(BlockType.REPORTER, "arrayLoopItem", "ARRAY | Current item", {}, () => window.sjs_currentItem), + Block(BlockType.REPORTER, "arrayLoopIndex", "ARRAY | Current index", {}, () => window.sjs_arri), + Block(BlockType.REPORTER, "rawArray", "ARRAY | Raw array [array]", { + array: Argument("string", '["Apple", "Banana"]') + }, ({ array: array2 }) => tryParse(array2)), + Block(BlockType.REPORTER, "filterArray", "ARRAY | Filter [array] by condition [condition]", { + array: Argument("string", '["Apple", "Banana"]'), + condition: Argument("string", 'item === "Apple"') + }, ({ array, condition }) => { + const arr = tryParse(array); + return JSON.stringify(arr.filter((item) => { + try { + return eval(condition); + } catch { + return false; + } + })); + }) + ]; + } + }); + // scratchjsblocks/timing.js var require_timing = __commonJS({ "scratchjsblocks/timing.js"(exports, module) { @@ -333,8 +478,10 @@ case "multiply": return val1 * val2; case "divide": + if (val2 === 0) return "Error: Division by zero"; return val1 / val2; case "modulo": + if (val2 === 0) return "Error: Division by zero"; return val1 % val2; case "power": return val1 ** val2; @@ -673,134 +820,63 @@ }) ]; - // scratchjsblocks/arrays.js - window.sjs_arrays = [ - Block(BlockType.BUTTON, "arraysCategory", "Arrays"), - Block(BlockType.BOOLEAN, "isValidJson", "Is [text] valid JSON?", { - text: Argument("string", '{"key":"value"}') - }, ({ text }) => { - try { - JSON.parse(text); - return true; - } catch (e) { - return false; - } - }), - Spacer, - Block(BlockType.REPORTER, "blankArray", "ARRAY | Blank array", {}, () => "[]"), - Block(BlockType.REPORTER, "addToArray", "ARRAY | Append [value] to array [array]", { - value: Argument("string", "Hello"), - array: Argument("string", "[]") - }, ({ array, value }) => JSON.stringify([...tryParse(array), value])), - Block(BlockType.REPORTER, "getFromArray", "ARRAY | Get [index] from array [array]", { - index: Argument("number", 1), - array: Argument("string", "[]") - }, ({ array, index }) => tryParse(array)[--index]), - Block(BlockType.REPORTER, "insertIntoArray", "ARRAY | Insert [value] at [index] in array [array]", { - value: Argument("string", "Hello"), - index: Argument("number", 1), - array: Argument("string", '["Apple"]') - }, ({ array, index, value }) => { - const arr = tryParse(array); - arr.splice(--index, 0, value); - return JSON.stringify(arr); - }), - Block(BlockType.REPORTER, "replaceInArray", "ARRAY | Replace [index] in array [array] with [value]", { - index: Argument("number", 1), - array: Argument("string", '["Apple"]'), - value: Argument("string", "Banana") - }, ({ array, index, value }) => { - const arr = tryParse(array); - arr[--index] = value; - return JSON.stringify(arr); - }), - Block(BlockType.REPORTER, "removeFromArray", "ARRAY | Remove [index] from array [array]", { - index: Argument("number", 1), - array: Argument("string", '["Apple"]') - }, ({ array, index }) => { - const arr = tryParse(array); - arr.splice(--index, 1); - return JSON.stringify(arr); - }), - Block(BlockType.REPORTER, "mergeArrays", "ARRAY | Merge [array1] and [array2]", { - array1: Argument("string", '["Hello"]'), - array2: Argument("string", '["World"]') - }, ({ array1, array2 }) => JSON.stringify([...tryParse(array1), ...tryParse(array2)])), - Block(BlockType.REPORTER, "lengthOfArray", "ARRAY | Length of array [array]", { - array: Argument("string", '["Apple", "Banana"]') - }, ({ array }) => tryParse(array).length), - Block(BlockType.BOOLEAN, "arrayHas", "ARRAY | Array [array] contains [value]", { - array: Argument("string", '["Apple", "Banana"]'), - value: Argument("string", "Carrot") - }, ({ array, value }) => tryParse(array).includes(value)), - Block(BlockType.REPORTER, "indexOf", "ARRAY | Index of [value] in array [array]", { - value: Argument("string", "Hello"), - array: Argument("string", '["Apple"]') - }, ({ array, value }) => { - const index = tryParse(array).indexOf(value); - return index === -1 ? 0 : index + 1; - }), - Block(BlockType.REPORTER, "splitString", "ARRAY | Split [string] by [delimiter] into array", { - string: Argument("string", "Hello, World"), - delimiter: Argument("string", ",") - }, ({ string, delimiter }) => JSON.stringify(string.split(delimiter))), - Block(BlockType.REPORTER, "joinArray", "ARRAY | Join array [array] with [delimiter]", { - array: Argument("string", '["Hello", "World"]'), - delimiter: Argument("string", ",") - }, ({ array, delimiter }) => tryParse(array).join(delimiter)), - Block(BlockType.REPORTER, "swapArrayItems", "ARRAY | Swap [index1] and [index2] in array [array]", { - index1: Argument("number", 1), - index2: Argument("number", 2), - array: Argument("string", '["Apple", "Banana"]') - }, ({ index1, index2, array }) => { - let res; - try { - res = JSON.parse(array); - } catch { - return array; - } - const temp = res[index1 - 1]; - res[index1 - 1] = res[index2 - 1]; - res[index2 - 1] = temp; - return JSON.stringify(res); - }), - Block(BlockType.REPORTER, "getItemsFrom", "ARRAY | Get items from [start] to [end] from array [array]", { - start: Argument("number", 2), - end: Argument("number", 3), - array: Argument("string", '["Apple", "Banana", "Carrot"]') - }, ({ start, end, array }) => { - let res; - try { - res = JSON.parse(array); - } catch { - return array; - } - return JSON.stringify(res.slice(start - 1, end - 1)); - }), - Block(BlockType.LOOP, "arrayLoop", "ARRAY | For each item in array [array] do", { - array: Argument("string", '["Apple", "Banana"]') - }, ({ array }, util) => { - let parsed = tryParse(array); - if (!window.sjs_inArrLoop) { - window.sjs_arri = 0; + // scratchjsblocks/variables.js + window.sjs_variables = [ + Block(BlockType.BUTTON, "variablesCategory", "Variables"), + Block(BlockType.COMMAND, "sjscreateVariable", "Create [type] Variable [name]", { + type: ArgumentWithMenu("string", "global", "variableTypeMenu"), + name: Argument("string", "myVariable") + }, ({ type, name: name2 }, util) => { + window.variableManager.createVariable(util, type, name2, ""); + vm.runtime.requestBlocksUpdate(); + }), + Block(BlockType.COMMAND, "sjscreateVariableWithValue", "Create [type] Variable [name] with value [value]", { + type: ArgumentWithMenu("string", "global", "variableTypeMenu"), + name: Argument("string", "myVariable"), + value: Argument("string", "0") + }, ({ type, name: name2, value }, util) => { + window.variableManager.createVariable(util, type, name2, value); + vm.runtime.requestBlocksUpdate(); + }), + Block(BlockType.REPORTER, "sjsgetVariable", "Variable [name]", { + name: Argument("string", "myVariable") + }, ({ name: name2 }, util) => { + const variable = window.variableManager.getVariable(util, name2); + return variable.value; + }), + Block(BlockType.COMMAND, "sjssetVariable", "Set Variable [name] to [value]", { + name: Argument("string", "myVariable"), + value: Argument("string", "0") + }, ({ name: name2, value }, util) => { + const variable = window.variableManager.getVariable(util, name2); + variable.value = value; + }), + Block(BlockType.COMMAND, "sjschangeVariable", "Change Variable [name] by [value]", { + name: Argument("string", "myVariable"), + value: Argument("string", "1") + }, ({ name: name2, value }, util) => { + const variable = window.variableManager.getVariable(util, name2); + variable.value = parseFloat(variable.value) + parseFloat(value); + }), + Block(BlockType.BOOLEAN, "sjsvariableExists", "Variable [name] exists?", { + name: Argument("string", "myVariable") + }, ({ name: name2 }, util) => { + const variable = window.variableManager.getVariable(util, name2); + return variable !== void 0; + }), + Block(BlockType.COMMAND, "sjsdeleteVariable", "Delete Variable [name]", { + name: Argument("string", "myVariable") + }, ({ name: name2 }, util) => { + const variable = window.variableManager.getVariable(util, name2); + if (variable) { + window.variableManager.deleteVariable(util, variable); } - if (++window.sjs_arri <= parsed.length) { - window.sjs_inArrLoop = true; - window.sjs_currentArray = array; - window.sjs_currentItem = parsed[window.sjs_arri - 1]; - util.startBranch(1, true); - } else { - window.sjs_arri = 0; - window.sjs_inArrLoop = false; - } - }), - Block(BlockType.REPORTER, "arrayLoopItem", "ARRAY | Current item", {}, () => window.sjs_currentItem), - Block(BlockType.REPORTER, "arrayLoopIndex", "ARRAY | Current index", {}, () => window.sjs_arri), - Block(BlockType.REPORTER, "rawArray", "ARRAY | Raw array [array]", { - array: Argument("string", '["Apple", "Banana"]') - }, ({ array }) => tryParse(array)) + }) ]; + // scratchjsblocks/blockimports.js + var import_arrays = __toESM(require_arrays()); + // scratchjsblocks/objects.js window.sjs_objects = [ Block(BlockType.BUTTON, "objectsCategory", "Objects"), @@ -904,9 +980,9 @@ }), Block(BlockType.REPORTER, "arrayToCsv", "Array [array] to CSV", { array: Argument("string", '[{"Name":"John","Age":25},{"Name":"Jane","Age":30}]') - }, ({ array }) => { + }, ({ array: array2 }) => { try { - const data = JSON.parse(array); + const data = JSON.parse(array2); if (!Array.isArray(data) || data.length === 0) return ""; const headers = Object.keys(data[0]); const csvLines = [headers.join(",")]; @@ -1148,9 +1224,9 @@ choices: Argument("string", '["rock","paper","scissors"]') }, ({ choices }) => { try { - const array = JSON.parse(choices); - if (Array.isArray(array) && array.length > 0) { - return array[Math.floor(Math.random() * array.length)]; + const array2 = JSON.parse(choices); + if (Array.isArray(array2) && array2.length > 0) { + return array2[Math.floor(Math.random() * array2.length)]; } return ""; } catch (e) { @@ -1159,11 +1235,11 @@ }), Block(BlockType.REPORTER, "shuffleArray", "Shuffle [array]", { array: Argument("string", '["A","B","C","D"]') - }, ({ array }) => { + }, ({ array: array2 }) => { try { - const arr = JSON.parse(array); - if (!Array.isArray(arr)) return "[]"; - const shuffled = [...arr]; + const arr2 = JSON.parse(array2); + if (!Array.isArray(arr2)) return "[]"; + const shuffled = [...arr2]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; @@ -1646,24 +1722,24 @@ Block(BlockType.REPORTER, "countOccurrences", "Count occurrences of [value] in [array]", { value: Argument("string", "apple"), array: Argument("string", '["apple","banana","apple","orange"]') - }, ({ value, array }) => { + }, ({ value, array: array2 }) => { try { - const arr = JSON.parse(array); - if (!Array.isArray(arr)) return "0"; - return arr.filter((item) => item === value).length.toString(); + const arr2 = JSON.parse(array2); + if (!Array.isArray(arr2)) return "0"; + return arr2.filter((item2) => item2 === value).length.toString(); } catch (e) { return "0"; } }), Block(BlockType.REPORTER, "getFrequency", "Frequency of all values in [array]", { array: Argument("string", '["apple","banana","apple","orange"]') - }, ({ array }) => { + }, ({ array: array2 }) => { try { - const arr = JSON.parse(array); - if (!Array.isArray(arr)) return "{}"; + const arr2 = JSON.parse(array2); + if (!Array.isArray(arr2)) return "{}"; const frequency = {}; - arr.forEach((item) => { - frequency[item] = (frequency[item] || 0) + 1; + arr2.forEach((item2) => { + frequency[item2] = (frequency[item2] || 0) + 1; }); return JSON.stringify(frequency); } catch (e) { @@ -1673,9 +1749,9 @@ Block(BlockType.BOOLEAN, "isOutlier", "Is [value] an outlier in [array]?", { value: Argument("number", 100), array: Argument("string", "[1, 2, 3, 4, 5]") - }, ({ value, array }) => { + }, ({ value, array: array2 }) => { try { - const nums = JSON.parse(array); + const nums = JSON.parse(array2); if (!Array.isArray(nums) || nums.length < 4) return false; const sorted = nums.map((num) => parseFloat(num)).sort((a, b) => a - b); const q1Index = Math.floor(sorted.length * 0.25); @@ -1929,12 +2005,14 @@ num1: Argument("string", "200000000000000000000"), num2: Argument("string", "2") }, ({ num1, num2 }) => { + if (BigInt(num2) === 0n) return "Error: Division by zero"; return BigInt(num1) / BigInt(num2); }), Block(BlockType.REPORTER, "bignumModulo", "[num1] mod [num2]", { num1: Argument("string", "100000000000000000001"), num2: Argument("string", "100000000000000000000") }, ({ num1, num2 }) => { + if (BigInt(num2) === 0n) return "Error: Division by zero"; return BigInt(num1) % BigInt(num2); }), Block(BlockType.REPORTER, "bignumPower", "[num1] ^ [num2]", { @@ -1988,6 +2066,158 @@ num: Argument("string", "100000000000000000000") }, ({ num }) => { return -BigInt(num); + }), + Block(BlockType.REPORTER, "bignumFactorial", "Factorial of [num]", { + num: Argument("string", "5") + }, ({ num }) => { + let n = BigInt(num); + if (n < 0n) return "Error: Factorial of negative number"; + if (n === 0n || n === 1n) return 1n; + let result = 1n; + for (let i = 2n; i <= n; i++) { + result *= i; + } + return result; }) ]; + + // scratchjsblocks/menus.js + window.sjs_menus = { + varMenu: "getVarMenu", + dateFormatMenu: Menu( + [ + MenuItem("date and time", "datetime"), + MenuItem("date only", "date"), + MenuItem("time only", "time"), + MenuItem("timestamp", "timestamp") + ], + "datetime" + ), + caseTypeMenu: Menu( + [ + MenuItem("UPPERCASE", "uppercase"), + MenuItem("lowercase", "lowercase") + ], + "uppercase" + ), + userInfoMenu: Menu( + [ + MenuItem("operating system", "OS"), + MenuItem("browser", "browser"), + MenuItem("language", "language"), + MenuItem("time zone", "timezone"), + MenuItem("screen width", "screenWidth"), + MenuItem("screen height", "screenHeight"), + MenuItem("window width", "windowWidth"), + MenuItem("window height", "windowHeight"), + MenuItem("device pixel ratio", "devicePixelRatio") + ], + "OS" + ), + boolOpMenu: Menu( + [ + MenuItem("AND", "and"), + MenuItem("OR", "or"), + MenuItem("XOR", "xor"), + MenuItem("NAND", "nand"), + MenuItem("NOR", "nor"), + MenuItem("XNOR", "xnor"), + MenuItem("Implies", "implies"), + MenuItem("Not-Implies", "n-implies"), + MenuItem(">", "greater"), + MenuItem("<", "less"), + MenuItem("\u2265", "greater-equal"), + MenuItem("\u2264", "less-equal"), + MenuItem("=", "equal"), + MenuItem("===", "exactly-equal"), + MenuItem("\u2260", "not-equal"), + MenuItem("+", "add"), + MenuItem("-", "subtract"), + MenuItem("\xD7", "multiply"), + MenuItem("\xF7", "divide"), + MenuItem("%", "modulo"), + MenuItem("^", "power"), + MenuItem("* 10^", "scientific"), + MenuItem("join", "join"), + MenuItem("contains", "contains"), + MenuItem("starts with", "startsWith"), + MenuItem("ends with", "endsWith"), + MenuItem("repeated", "repeated"), + MenuItem("pad start with spaces", "padstart"), + MenuItem("pad end with spaces", "padend"), + MenuItem("bitwise AND", "bitwise-and"), + MenuItem("bitwise OR", "bitwise-or"), + MenuItem("bitwise XOR", "bitwise-xor"), + MenuItem("bitwise NOT", "bitwise-not"), + MenuItem("left shift", "left-shift"), + MenuItem("right shift", "right-shift"), + MenuItem("zero-fill right shift", "zero-fill-right-shift") + ], + "and" + ), + padSideMenu: Menu( + [MenuItem("left", "left"), MenuItem("right", "right")], + "left" + ), + historyActionMenu: Menu( + [MenuItem("back", "back"), MenuItem("forward", "forward")], + "back" + ), + httpMethodMenu: Menu( + [ + MenuItem("GET", "GET"), + MenuItem("POST", "POST"), + MenuItem("PUT", "PUT"), + MenuItem("DELETE", "DELETE") + ], + "GET" + ), + timeFormatMenu: Menu( + [ + MenuItem("ISO", "ISO"), + MenuItem("local", "local"), + MenuItem("date", "date"), + MenuItem("time", "time"), + MenuItem("unix", "unix") + ], + "ISO" + ), + hashAlgorithmMenu: Menu([MenuItem("simple", "simple")], "simple"), + diceSidesMenu: Menu( + [ + MenuItem("4", "4"), + MenuItem("6", "6"), + MenuItem("8", "8"), + MenuItem("10", "10"), + MenuItem("12", "12"), + MenuItem("20", "20"), + MenuItem("100", "100") + ], + "6" + ), + passwordOptionsMenu: Menu( + [ + MenuItem("letters", "letters"), + MenuItem("numbers", "numbers"), + MenuItem("letters+numbers", "letters+numbers"), + MenuItem("all", "all") + ], + "letters+numbers" + ), + rpsMenu: Menu( + [ + MenuItem("rock", "rock"), + MenuItem("paper", "paper"), + MenuItem("scissors", "scissors") + ], + "rock" + ), + variableTypeMenu: Menu( + [ + MenuItem("Sprite", "target"), + MenuItem("Global", "global") + ], + "target" + ) + }; })(); diff --git a/extensiontutorial.md b/extensiontutorial.md index b3c68cd..a3ea39a 100644 --- a/extensiontutorial.md +++ b/extensiontutorial.md @@ -1,5 +1,9 @@ # How to make an Extension for Scratch +> [!NOTE] +> This tutorial requires an advanced understanding of [JavaScript](https://en.wikipedia.org/wiki/JavaScript). +> If you are interested in learning how ScratchJS works go to [this page](https://github.com/Ironbill25/projects/blob/main/scratchjs/docs/howitworks.md). + First, fork this repository. Name it whatever you want. Feel free to remove scratchjs.js. However, we will be looking at extensiontempl.js. First, change the YourExtensionName variable to your extension's name. @@ -7,21 +11,19 @@ Now we will add some blocks. We have added an example block to the extension, it looks something like this: ```javascript -// ... -exampleBlock({ text, number, color }) { - return text + " " + number + " " + color; -} -// ... +// ... In your blocks array: Block(BlockType.REPORTER, "exampleBlock", "Example block [text] [number] [color]", { text: Argument(ArgumentType.STRING, "Hello"), number: Argument(ArgumentType.NUMBER, 42), color: Argument(ArgumentType.COLOR, "#FF0000"), +}, ({text, number, color}) => { +return text + ", " + number.toString() + " colored " + color }), // ... ``` Notice the functions `Block` and `Argument`. We will be using these to define our blocks. -The `Block` function takes 4 arguments: the block type, the block opcode (like an ID), the block label, and the block arguments. +The `Block` function takes 5 arguments: the block type, the block opcode (like an ID), the block label, the block arguments, and the function. The `Argument` function takes 2 arguments: the argument type and the default value. Now let's look at the arguments. The block label uses square brackets to denote arguments. @@ -36,20 +38,13 @@ Please note that returning a value in a non-reporter block will cause some stran To recap that last section, a typical block looks like this: ```javascript -// ... - -// In the Your_Extension class: -yourBlockOpcode({ arg1, arg2 }) { - // do something with arg1 and arg2 - return result; -} - -// ... - -// In the blocks array (should be marked with equals signs) +// ... In the blocks array (should be marked with equals signs) Block(BlockType.REPORTER /* Change this to either COMMAND, BOOLEAN, or REPORTER */, "yourBlockOpcode", "Your block [arg1] label [arg2]", { arg1: Argument(ArgumentType.STRING /* Also change these to the appropriate types */, "default value" /* Default value */), arg2: Argument(ArgumentType.NUMBER /* Also change these to the appropriate types */, 0 /* Default value */), +}, ({ arg1, arg2 }) => { + // do something with arg1 and arg2 + return result; }) // ... @@ -95,7 +90,7 @@ Lastly, let's make a bookmarklet for the extension. This will load your extensio javascript: (async () => { try { const r = await fetch( - "https://raw.githubusercontent.com/Ironbill25/JavaScript-For-Scratch/refs/heads/main/scratchjs.js", + "https://raw.githubusercontent.com/Ironbill25/JavaScript-For-Scratch/refs/heads/main/scratchjs.js", /* Replace this with the URL of your extension code */ { cache: "no-cache" } ); if (!r.ok) throw new Error("Fetch failed: " + r.status); @@ -110,5 +105,5 @@ javascript: (async () => { })(); ``` -You should change the URL to the URL of your raw extension file. -You can find this by going to your repository on GitHub, clicking on the file, and then clicking on the "Raw" button. +You should change the URL in the fetch to the URL of your raw extension file. +You can find this by going to your repository on GitHub, going to the file, and then clicking on the "Raw" button. diff --git a/scratchjs.js b/scratchjs.js index d840511..8b1be3f 100644 --- a/scratchjs.js +++ b/scratchjs.js @@ -10,11 +10,13 @@ See more about ScratchJS at https://ironbill25.github.io/projects/scratchjs/`); let devmode = false; let vmtries = 0; let viewmode = false; + let supportedSites = ["scratch.mit.edu", "penguinmod.com", "turbowarp.org", "librekitten.org", "canary.librekitten.org", "ampmod.codeberg.page", "omniblocks.github.io", "snail-ide.js.org", "sheeptester.github.io"]; + let partialSupport = []; window.sjs_extensionBlocks = []; function chkKey(obj, key) { - if (!obj) return ""; - return Object.keys(obj).includes(key) ? obj[key] : ""; + if (!obj) return "No object provided"; + return Object.keys(obj).includes(key) ? obj[key] : "Key not found"; } window.sjs_applyViewMode = () => { @@ -28,13 +30,24 @@ See more about ScratchJS at https://ironbill25.github.io/projects/scratchjs/`); * @param {Function} callback - The function to call with the VM. */ function waitForVM(callback) { + let vm; if (window.vm) { callback(window.vm); return console.log("VM already available"); } + if (!supportedSites.includes(location.hostname)) { + console.error("Unsupported site: " + location.hostname); + if (!confirm("This is an unsupported site! This site has not been tested with ScratchJS and may not work properly. Do you want to continue?")) { + return; + } + } + if (partialSupport.includes(location.hostname)) { + console.warn("Partially supported site: " + location.hostname); + alert("This site is partially supported by ScratchJS. This means that the extension can load, but some features may be unavailable.\n\nIf you encounter any issues, please report them on the ScratchJS GitHub page ( https://github.com/IronBill25/JavaScript-For-Scratch/issues )"); + } vmtries++; if (vmtries > 15) { - console.error("VM not found after 15 tries, stopping attempts. Please report this error on the ScratchJS GitHub page (https://github.com/IronBill25/JavaScript-For-Scratch/issues)"); + console.error("VM not found after 15 tries, stopping attempts. Please report this error on the ScratchJS GitHub page ( https://github.com/IronBill25/JavaScript-For-Scratch/issues )"); return; } console.log("waiting for VM, try " + vmtries); @@ -58,7 +71,7 @@ See more about ScratchJS at https://ironbill25.github.io/projects/scratchjs/`); if (fiber?.stateNode?.props?.vm) break; } console.log("Check 3 - fiber after loop:", fiber); - let vm = + vm = fiber?.stateNode?.props?.vm || fiber?.return?.return?.return?.return?.updateQueue?.stores?.[0]?.value ?.vm; @@ -145,15 +158,15 @@ You can get the official bookmarklet here: https://scratch.mit.edu/projects/1316

- -

`; + + + `; modal.id = "scratchjs-warning-modal"; document.head.innerHTML += `