From 2f772ec950fca9c25fefc966f67ddd117010991f Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Sun, 24 May 2026 19:57:33 -0600
Subject: [PATCH 01/18] Update extension tutorial with corrected examples
---
extensiontutorial.md | 31 +++++++++++--------------------
1 file changed, 11 insertions(+), 20 deletions(-)
diff --git a/extensiontutorial.md b/extensiontutorial.md
index b3c68cd..97a0113 100644
--- a/extensiontutorial.md
+++ b/extensiontutorial.md
@@ -7,21 +7,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 +34,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 +86,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 +101,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.
From dfd0818598d88a7fdc8b9fb9ceee5b81e7674ace Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Sun, 24 May 2026 21:16:43 -0600
Subject: [PATCH 02/18] Add array filter and bignum factorial
---
dist/bundle.js | 326 +++++++++++++++++++++-----------------
scratchjsblocks/arrays.js | 13 ++
scratchjsblocks/bignum.js | 13 ++
3 files changed, 205 insertions(+), 147 deletions(-)
diff --git a/dist/bundle.js b/dist/bundle.js
index 681dbe9..044b3cc 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) {
@@ -673,133 +818,8 @@
})
];
- // 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;
- }
- 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 = [
@@ -904,9 +924,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 +1168,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 +1179,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 +1666,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 +1693,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);
@@ -1988,6 +2008,18 @@
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;
})
];
})();
diff --git a/scratchjsblocks/arrays.js b/scratchjsblocks/arrays.js
index 84a1708..414931e 100644
--- a/scratchjsblocks/arrays.js
+++ b/scratchjsblocks/arrays.js
@@ -123,4 +123,17 @@ window.sjs_arrays = [
Block(BlockType.REPORTER, "rawArray", "ARRAY | Raw array [array]", {
array: Argument("string", "[\"Apple\", \"Banana\"]"),
}, ({ array }) => tryParse(array)),
+ 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;
+ }
+ }));
+ })
];
diff --git a/scratchjsblocks/bignum.js b/scratchjsblocks/bignum.js
index c91d91d..e2e42f2 100644
--- a/scratchjsblocks/bignum.js
+++ b/scratchjsblocks/bignum.js
@@ -119,4 +119,17 @@ window.sjs_bignum = [
}, ({ 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;
+ })
]
\ No newline at end of file
From 09d050dca5ba7e90c203b30f8cb20293f0e1f215 Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Fri, 29 May 2026 20:03:24 -0600
Subject: [PATCH 03/18] Support for CodeTorch
---
scratchjs.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scratchjs.js b/scratchjs.js
index d840511..6c0573f 100644
--- a/scratchjs.js
+++ b/scratchjs.js
@@ -39,7 +39,7 @@ See more about ScratchJS at https://ironbill25.github.io/projects/scratchjs/`);
}
console.log("waiting for VM, try " + vmtries);
const el = document.querySelector(
- 'div[class*="stage-header_stage-header-wrapper"]',
+ 'div[class*="stage-header_stage-header-wrapper"], div[class*="stage-wrapper_stage-canvas-wrapper"]', /* supports both Scratch and CodeTorch */
);
if (!el) return console.log("No stage header found");
From 7558847bcf3bea6d09ce5dd0b686a2b58eb12b88 Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Fri, 29 May 2026 20:10:18 -0600
Subject: [PATCH 04/18] General bug fixing & make code more robust
---
dist/bundle.js | 4 ++
scratchjs.js | 75 +++++++++++++++++--------------------
scratchjsblocks/bignum.js | 2 +
scratchjsblocks/booleans.js | 2 +
4 files changed, 43 insertions(+), 40 deletions(-)
diff --git a/dist/bundle.js b/dist/bundle.js
index 044b3cc..2a850d7 100644
--- a/dist/bundle.js
+++ b/dist/bundle.js
@@ -478,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;
@@ -1949,12 +1951,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]", {
diff --git a/scratchjs.js b/scratchjs.js
index 6c0573f..bfdbe8c 100644
--- a/scratchjs.js
+++ b/scratchjs.js
@@ -13,8 +13,8 @@ See more about ScratchJS at https://ironbill25.github.io/projects/scratchjs/`);
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 = () => {
@@ -350,7 +350,7 @@ You can get the official bookmarklet here: https://scratch.mit.edu/projects/1316
const timestamp = Date.now();
const response = await fetch(
"https://raw.githubusercontent.com/Ironbill25/JavaScript-For-Scratch/main/dist/bundle.js?t=" +
- timestamp, { cache: "no-store" }
+ timestamp, { cache: "no-store" }
);
if (response.ok) {
const code = await response.text();
@@ -449,9 +449,9 @@ You can get the official bookmarklet here: https://scratch.mit.edu/projects/1316
* @param {Function} fun - The function to execute when the block is run.
* @returns {Object} - The block object.
*/
- window.Block = (blockType, opcode, text, args = {}, fun = () => {}, othersettings = {}) => {
+ window.Block = (blockType, opcode, text, args = {}, fun = () => { }, othersettings = {}) => {
-const wrappedFunction = function (...args) {
+ const wrappedFunction = function (...args) {
try {
return fun.apply(this, args);
} catch (error) {
@@ -579,13 +579,13 @@ const wrappedFunction = function (...args) {
STAGE: "stage",
};
- window.addExtensionBlocks = function(blocks) {
+ window.addExtensionBlocks = function (blocks) {
window.allBlocks = [...window.allBlocks, Spacer, ...blocks];
};
await loadBlockFiles();
-
+
window.ScratchJS = class {
constructor(runtime) {
@@ -779,46 +779,41 @@ const wrappedFunction = function (...args) {
localStorage.setItem("scratchjs_devMode", "true");
}
- let retryCount = 0;
- const maxRetries = 3;
- while (retryCount < maxRetries) {
- if (retryCount === maxRetries - 1) {
- const allBlocks = categories.flatMap(
- (category) => window[`sjs_${category}`] || [],
- );
- allBlocks.push(...(window.sjs_extensionBlocks || []));
+ const allBlocks = categories.flatMap(
+ (category) => window[`sjs_${category}`] || [],
+ );
+ allBlocks.push(...(window.sjs_extensionBlocks || []));
- console.log(allBlocks);
+ console.log(allBlocks);
- const missingFunctions = allBlocks.filter(
- (block) =>
- block.opcode && !(block.opcode in (window.allFunctions || {})),
- );
- if (missingFunctions.length > 0) console.warn(
- "Missing functions for blocks:",
- missingFunctions.map((b) => b.opcode),
- );
- break;
- }
+ const missingFunctions = allBlocks.filter(
+ (block) =>
+ block.opcode && !(block.opcode in (window.allFunctions || {})),
+ );
+ if (missingFunctions.length > 0) console.warn(
+ "Missing functions for blocks:",
+ missingFunctions.map((b) => b.opcode),
+ );
- await new Promise((resolve) => setTimeout(resolve, 500));
- retryCount++;
- }
var extensionInstance = new ScratchJS(vm.extensionManager.runtime);
- if (viewmode) { for (const [opcode, func] of Object.entries(
- window.allFunctions || {},
- )) {
- extensionInstance[opcode] = () => "This block is disabled in view mode";
- }};
-
- if (!viewmode) {for (const [opcode, func] of Object.entries(
- window.allFunctions || {},
- )) {
- extensionInstance[opcode] = func;
- }};
+ if (viewmode) {
+ for (const [opcode, func] of Object.entries(
+ window.allFunctions || {},
+ )) {
+ extensionInstance[opcode] = () => "This block is disabled in view mode";
+ }
+ };
+
+ if (!viewmode) {
+ for (const [opcode, func] of Object.entries(
+ window.allFunctions || {},
+ )) {
+ extensionInstance[opcode] = func;
+ }
+ };
var serviceName =
vm.extensionManager._registerInternalExtension(extensionInstance);
vm.extensionManager._loadedExtensions.set(
diff --git a/scratchjsblocks/bignum.js b/scratchjsblocks/bignum.js
index e2e42f2..a607952 100644
--- a/scratchjsblocks/bignum.js
+++ b/scratchjsblocks/bignum.js
@@ -49,6 +49,7 @@ window.sjs_bignum = [
num1: Argument("string", "200000000000000000000"),
num2: Argument("string", "2")
}, ({ num1, num2 }) => {
+ if (BigInt(num2) === 0n) return "Error: Division by zero";
return (BigInt(num1) / BigInt(num2));
}),
@@ -56,6 +57,7 @@ window.sjs_bignum = [
num1: Argument("string", "100000000000000000001"),
num2: Argument("string", "100000000000000000000")
}, ({ num1, num2 }) => {
+ if (BigInt(num2) === 0n) return "Error: Division by zero";
return (BigInt(num1) % BigInt(num2));
}),
diff --git a/scratchjsblocks/booleans.js b/scratchjsblocks/booleans.js
index 81d2f3e..2b4b48d 100644
--- a/scratchjsblocks/booleans.js
+++ b/scratchjsblocks/booleans.js
@@ -58,8 +58,10 @@ window.sjs_booleans = [
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;
From 8b91bcc6ae9c28bf9c7400eafc570859085801e4 Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Fri, 29 May 2026 20:25:51 -0600
Subject: [PATCH 05/18] Add changelog
---
CHANGELOG.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e9c5fc3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,91 @@
+# 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
From 57a2edbcb1872f0c20c301b4d4ba4f77ce2bf063 Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Sat, 30 May 2026 17:15:46 -0600
Subject: [PATCH 06/18] add information box at top
added note about javascript and a link to the documentation on how scratchjs works
---
extensiontutorial.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/extensiontutorial.md b/extensiontutorial.md
index 97a0113..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.
From 0f4200586be28513af34c1db5dab9454022e8f1a Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Sat, 30 May 2026 17:26:29 -0600
Subject: [PATCH 07/18] add supported sites notice
---
scratchjs.js | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/scratchjs.js b/scratchjs.js
index bfdbe8c..881d151 100644
--- a/scratchjs.js
+++ b/scratchjs.js
@@ -10,6 +10,8 @@ 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) {
@@ -28,18 +30,29 @@ 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);
const el = document.querySelector(
- 'div[class*="stage-header_stage-header-wrapper"], div[class*="stage-wrapper_stage-canvas-wrapper"]', /* supports both Scratch and CodeTorch */
+ 'div[class*="stage-header_stage-header-wrapper"]',
);
if (!el) return console.log("No stage header found");
@@ -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;
From fb6c8cc7f9c1e7c84e72516d06ec5e894c244c79 Mon Sep 17 00:00:00 2001
From: IronBill25 <215311957+Ironbill25@users.noreply.github.com>
Date: Fri, 5 Jun 2026 18:00:07 -0600
Subject: [PATCH 08/18] Clean up the warning modal a bit
---
scratchjs.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/scratchjs.js b/scratchjs.js
index 881d151..00eb2c9 100644
--- a/scratchjs.js
+++ b/scratchjs.js
@@ -158,15 +158,15 @@ You can get the official bookmarklet here: https://scratch.mit.edu/projects/1316
-
-