From 1bbcab91ae6a85d0af41533cd751bb4a116725d8 Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:03:23 +0100 Subject: [PATCH 01/10] create prettyblocks.js, prettyblocks.md --- docs/Hammouda101010/prettyblocks.md | 14 + extensions/Hammouda101010/prettyblocks.js | 755 ++++++++++++++++++++++ extensions/extensions.json | 1 + 3 files changed, 770 insertions(+) create mode 100644 docs/Hammouda101010/prettyblocks.md create mode 100644 extensions/Hammouda101010/prettyblocks.js diff --git a/docs/Hammouda101010/prettyblocks.md b/docs/Hammouda101010/prettyblocks.md new file mode 100644 index 0000000000..d12c21a4aa --- /dev/null +++ b/docs/Hammouda101010/prettyblocks.md @@ -0,0 +1,14 @@ +# Pretty Blocks +An extension to add strict formatting rules to your project. +## Rules +- Camel Case Only: + - Affects all variables and sprites. + - Forces the "CamelCase" naming convention. +- Griffpatch Style: + - Affects variables only. + - Makes sure that global variables are in "uppercase" and local variables in "lowercase". much like how [griffpatch](https://www.youtube.com/@griffpatch) writes his variables. +- No Capitalized Custom Blocks: + - Affects custom blocks only. + - Makes sure that custom block names aren't capitalized. +## Creating Custom Rules +if you want to create your own custom rules \ No newline at end of file diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js new file mode 100644 index 0000000000..bdf30eeec4 --- /dev/null +++ b/extensions/Hammouda101010/prettyblocks.js @@ -0,0 +1,755 @@ +// Name: Pretty Blocks +// ID: HamPrettyBlocks +// Description: Add formating to your projects for more readability. Based of Prettier. +// By: hammouda101010 +// License: MPL-2.0 + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Pretty Blocks extension must run unsandboxed"); + } + + const vm = Scratch.vm; + const runtime = vm.runtime; + const Cast = Scratch.Cast; + /**Checks if the extension is in "penguinmod.com".*/ + // @ts-ignore + const _isPM = Scratch.extensions.isPenguinMod; + /**Checks if the extension is in "unsandboxed.org".*/ + const isUnSandBoxed = + JSON.parse(vm.toJSON()).meta.platform.url === "https://unsandboxed.org/"; + /**Checks if the extension is inside the editor.*/ + const isEditor = typeof scaffolding === "undefined"; + + function formatError(errors, logToConsole = false) { + let msg = []; + for (const error of errors) { + switch (error.level) { + case "warn": + if (logToConsole) console.warn(error.msg); + msg.push( + Scratch.translate( + `(⚠️ warning) "${error.type}" [${error.subject}]: ${error.msg}` + ) + ); + break; + case "error": + if (logToConsole) console.error(error.msg); + msg.push( + Scratch.translate( + `(❌ error) "${error.type}" [${error.subject}]: ${error.msg}` + ) + ); + break; + } + } + + return msg.join("\n").trim(); + } + + /**! + * altered modal code from + * @link https://gist.github.com/yuri-kiss/345ab1e729bd5d0a87506156635d0c83 + * @license MIT + * + */ + const errorModal = (titleName = "Alert", error = []) => { + //@ts-ignore + // run prompt to get a GUI to modify + ScratchBlocks.prompt(); + + // get the portal/modal and its header + const portal = document.querySelector("div.ReactModalPortal"); + const header = portal.querySelector( + 'div[class*="modal_header-item-title_"]' + ); + + // add our own custom title + header.textContent = Cast.toString(titleName); + // get the portal/modal body + const portalBody = portal.querySelector('div[class^="prompt_body_"]'); + const portalHolder = portalBody.parentElement.parentElement; + + // set a custom modal height + portalHolder.style.width = "650px"; + + const errorString = formatError(error, true); + + const errorHTML = ` + `; + const contentHTML = `

${Scratch.translate("Errors found in project:")}

${errorHTML}

${Scratch.translate('Make sure to fix these manually or with the "Format project" button.')}

`; + const promptButtonPos = `
${portalBody.querySelector("div[class^=prompt_button-row_]").outerHTML}
`; + + portalBody.innerHTML = `${contentHTML}${promptButtonPos}`; + const textarea = portalBody.querySelector( + 'textarea[class^="data-url_code_1o8oS"]' + ); + + //@ts-expect-error + textarea.value = textarea.value.trim(); + + // creating our OK button + const okButton = portalBody.querySelector( + `button[class^="prompt_ok-button_"]` + ); + okButton.previousElementSibling.remove(); + okButton.parentElement.style.display = "block"; + okButton.parentElement.style.verticalAlign = "bottom"; + + okButton.addEventListener("click", () => { + //@ts-expect-error - included in modal + portal.querySelector("div[class^=close-button_close-button_]").click(); + }); + }; + + /**! + * altered modal code from @yuri-kiss + * @link https://gist.github.com/yuri-kiss/345ab1e729bd5d0a87506156635d0c83 + * @license MIT + * + * some code was also borrowed from SharkPool's Rigidbodies extension + * @link https://github.com/SharkPool-SP/SharkPools-Extensions/blob/main/extension-code/Rigidbodies.js + * @license MIT + */ + const newRuleModal = ( + titleName = "Alert", + vals = [], + deleteRule, + func = (name, regex, func, scope) => {} + ) => { + let name; + let regex; + let funcType = vals[0]; + let scope = ["all"]; + // in a Button Context, ScratchBlocks always exists + // @ts-ignore + ScratchBlocks.prompt( + !deleteRule ? titleName : "test", + "", + !deleteRule ? () => func(name, regex, funcType, scope) : () => func(name), + "Format Rules Manager", + "broadcast_msg" + ); + + if (deleteRule) { + const input = document.querySelector( + `div[class="ReactModalPortal"] input` + ); + + const delLabel = input.parentNode.previousSibling.cloneNode(true); + delLabel.textContent = titleName; + const selector = document.createElement("select"); + selector.setAttribute("class", input.getAttribute("class")); + selector.addEventListener("input", (e) => { + // @ts-ignore + name = e.target.value; + }); + vals.forEach((option) => { + let opt = document.createElement("option"); + opt.value = option; + opt.text = option; + selector.appendChild(opt); + }); + + input.parentNode.append(delLabel, selector); + input.parentNode.previousSibling.remove(); + input.remove(); + } else { + const portal = document.querySelector("div.ReactModalPortal"); + const portalBody = portal.querySelector('div[class^="prompt_body_"]'); + const portalHolder = portalBody.parentElement.parentElement; + + // set a custom modal height + portalHolder.style.height = "65%"; + + // set the modal HTML + portalBody.parentElement.style.height = "100%"; + //@ts-ignore + portalBody.style.height = "calc(100% - 3.125rem)"; + //@ts-ignore + portalBody.style.wordBreak = "break-all"; + //@ts-ignore + portalBody.style.position = "relative"; + //@ts-ignore + portalBody.style.overflowY = "auto"; + + const input = document.querySelector( + `div[class="ReactModalPortal"] input` + ); + input.addEventListener("input", (e) => { + // @ts-ignore + name = e.target.value; + }); + + const regexLabel = input.parentNode.previousSibling.cloneNode(true); + regexLabel.textContent = "Regular Expression:"; + + const regexInput = document.createElement("input"); + regexInput.setAttribute("class", input.getAttribute("class")); + regexInput.addEventListener("input", (e) => { + // @ts-ignore + regex = e.target.value; + }); + + // Format Function (The funtction to use when formatting the project.) + const funcTypeLabel = input.parentNode.previousSibling.cloneNode(true); + funcTypeLabel.textContent = "Format Function:"; + const selector = document.createElement("select"); + selector.setAttribute("class", input.getAttribute("class")); + selector.addEventListener("input", (e) => { + // @ts-ignore + funcType = e.target.value; + }); + vals.forEach((option) => { + let opt = document.createElement("option"); + opt.value = option.value; + opt.text = option.text; + selector.appendChild(opt); + }); + + // Rule Scopes (What types of objects the rule is allowed to access.) + const scopeLabel = input.parentNode.previousSibling.cloneNode(true); + scopeLabel.textContent = "Scopes:"; + + const scopeInput = document.createElement("input"); + scopeInput.setAttribute("class", input.getAttribute("class")); + scopeInput.addEventListener("input", (e) => { + // @ts-ignore + scope = Cast.toString(e.target.value).split(" "); + }); + + input.parentNode.append(regexLabel, regexInput); + input.parentNode.append(funcTypeLabel, selector); + input.parentNode.append(scopeLabel, scopeInput); + } + + runtime.stopAll(); + }; + + /**Opens a Turbowarp-based Modal. Will Only Work on The Editor. */ + function openModal(type, titleName, msg, func = undefined) { + // Check if we are in the editor + if (typeof scaffolding === "undefined") { + if (type === "error") { + errorModal(titleName, msg); + } else if (type === "prompt") { + //@ts-ignore + ScratchBlocks.prompt( + titleName, + "", + (value) => func(value), + Scratch.translate("Pretty Blocks"), + "broadcast_msg" + ); + } + runtime.stopAll(); + } + } + + let ignoreList; + + // Function Types for Custom Rules. + const funcTypes = [ + { text: Scratch.translate("to uppercase"), value: "uppercase" }, + { text: Scratch.translate("to lowercase"), value: "lowercase" }, + { text: Scratch.translate("regex validation"), value: "regex_validation" }, + { text: Scratch.translate("to lowercase"), value: "lowercase" }, + { text: Scratch.translate("to camelCase"), value: "camelcase" }, + { text: Scratch.translate("to snake_case"), value: "snakecase" }, + { text: Scratch.translate("to PascalCase"), value: "pascal_case" }, + { text: Scratch.translate("space trimming"), value: "trim" }, + ]; + + let customRules = {}; + let rules = { + camelCaseOnly: { + enabled: false, + level: "error", + msg: `"{val}" should be in camelCase.`, + regex: "/^[a-z]+(?:[A-Z][a-z]*)*$/", + }, + griffpatchStyle: { + enabled: true, + level: "error", + msg: `"{val}" should be entirely in {isGlobal}. just as griffpatch intened it.`, + regex: "if /^[^a-z]+/ else /^[^A-Z]+/", + }, + customNoCapitalized: { + enabled: false, + level: "error", + msg: `"{val}" should not be capitalized in a custom block.`, + regex: "/^[A-Z]/", + }, + ...customRules, + }; + + // Turbowarp's extension storage + runtime.on("PROJECT_LOADED", () => { + vm.extensionManager.refreshBlocks(); + try { + // @ts-ignore + const storage = JSON.parse(runtime.extensionStorage["HamPrettyBlocks"]); + + if (storage) { + ignoreList = storage.ignoreList ? JSON.parse(storage.ignoreList) : []; + + rules = storage.rules ? JSON.parse(storage.rules) : rules; + customRules = storage.customRules + ? JSON.parse(storage.customRules) + : customRules; + } + } catch (e) { + console.error(e); + } + }); + + class HamPrettyBlocks { + constructor() { + this.formatErrors = []; + } + + getInfo() { + return { + id: "HamPrettyBlocks", + name: Scratch.translate("Pretty Blocks"), + docsURI: "http://localhost:8000/Hammouda101010/prettyblocks", // https://extensions.turbowarp.org/Hammouda101010/prettyblocks + blocks: [ + { + func: "checkFormatting", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Check Project Formatting"), + }, + "---", + { + func: "newFormatRule", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Add Format Rule"), + }, + { + func: "delFormatRule", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Delete Format Rule"), + }, + { + opcode: "ignoreVariable", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("ignore variable named [VAR_MENU]"), + arguments: { + VAR_MENU: { + type: Scratch.ArgumentType.STRING, + menu: "PRETTYBLOCKS_VARIABLES", + }, + }, + }, + { + opcode: "ignoreCustomBlock", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("ignore custom block named [BLOCK_MENU]"), + arguments: { + BLOCK_MENU: { + type: Scratch.ArgumentType.STRING, + menu: "PRETTYBLOCKS_CUSTOM_BLOCKS", + }, + }, + }, + { + opcode: "formatErrorsReporter", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("format errors"), + }, + { + opcode: "fancyFormatErrors", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("fancify format errors [FORMAT_ERROR]"), + arguments: { + FORMAT_ERROR: {}, + }, + }, + ], + menus: { + PRETTYBLOCKS_CUSTOM_BLOCKS: { + acceptReporters: true, + items: "_getCustomBlocksMenu", + }, + PRETTYBLOCKS_VARIABLES: { + acceptReporters: true, + items: "_getVariablesMenu", + }, + }, + }; + } + // Class Utilities + getLogic(str, opts) { + let codeLine = Cast.toString(str); + const logicCodeLineArray = codeLine.split(" "); + let boolResult = false; + let result = null; + + // Check for each spaces + for (const line of logicCodeLineArray) { + // if it's a boolean + if (/<.*>/.test(line)) { + // is it a primitive value? + if (line === "") { + boolResult = true; + } else if (line === "") { + boolResult = false; + // otherwise, it's an argument + } else { + const optsArray = Object.values(opts).map((value) => + Cast.toBoolean(value) + ); + const boolArgs = codeLine.match(/<([^<>]+)>/g); + for (const boolVal of boolArgs) { + if ( + Cast.toBoolean( + Object.keys(opts).indexOf(boolVal.replace(/[<>]/g, "")) === + boolArgs.indexOf(boolVal) + ) + ) { + boolResult = optsArray[boolArgs.indexOf(boolVal)]; + } + } + } + } else { + if (line === "if") { + continue; + } else if (line === "else") { + if (!boolResult) { + result = logicCodeLineArray + .slice(logicCodeLineArray.indexOf(line) + 1) + .join(" "); + console.log(`second operator: ${result}`); + break; + } + } else if (boolResult === undefined) { + continue; + } else if (boolResult) { + result = line; + console.log(`first operator: ${result}`); + break; + } + } + } + + return result; + } + getCustomBlocks() { + const targets = runtime.targets; + const customBlocks = []; + + for (const target of targets) { + const blocks = target.blocks._blocks; + for (const blockId in blocks) { + const block = blocks[blockId]; + if (block.opcode === "procedures_prototype") { + customBlocks.push(this.formatCustomBlock(block)); + } + } + } + + return customBlocks.length > 0 ? customBlocks : []; + } + getVariables() { + const stage = runtime.getTargetForStage(); + const targets = runtime.targets; + + // Sort the variables + const globalVars = Object.values(stage.variables) + .filter((v) => v.type !== "list") + .map((v) => v.name); + + const allVars = targets + .filter((t) => t.isOriginal) + .map((t) => t.variables); + const localVars = allVars + .map((v) => Object.values(v)) + .map((v) => + v + .filter((v) => v.type !== "list" && !globalVars.includes(v.name)) + .map((v) => v.name) + ) + .flat(1); + + const variables = { + local: localVars, + global: globalVars, + }; + + return variables; + } + + checkFormatRule(rule, val, type, opts = {}) { + if (rules[rule].enabled) { + let str = Cast.toString(rules[rule].regex); + if (str.startsWith("if")) { + str = this.getLogic(str, opts); + } + + const regex = new RegExp(str.split("/")[1], str.split("/")[2]); + console.log(regex); + console.log(regex.test(val)); + console.log(regex.test(val)); + + switch (rule) { + case "griffpatchStyle": + if (!regex.test(val)) { + this.formatErrors.push({ + type: type, + level: rules[rule].level, + subject: val, + msg: Scratch.translate( + Cast.toString(rules[rule].msg).replace( + /\{([^}]+)\}/g, + (e) => { + console.log(e); + if (e === "{isGlobal}") { + return opts.isGlobal ? "UPPERCASE" : "lowercase"; + } else { + return val; + } + } + ) + ), + }); + } + break; + default: + if (!rules[rule].check(val)) { + this.formatErrors.push({ + type: type, + level: rules[rule].level, + subject: val, + msg: Scratch.translate(rules[rule].msg), + }); + } + break; + } + } + } + + checkCustomFormatRules(val, type) { + for (const rule in customRules) { + if ( + (customRules[rule].enabled && + customRules[rule].scopes.includes(type)) || + customRules[rule].scopes.includes("all") + ) { + if (!customRules[rule].check(val)) { + this.formatErrors.push({ + type: type, + level: customRules[rule].level, + subject: val, + msg: Scratch.translate(customRules[rule].msg(val)), + }); + } + } + } + } + + _checkSpriteFormatting() { + const targets = runtime.targets; + for (const target of targets) { + if (target.isSprite()) { + // Format check + this.checkFormatRule("camelCaseOnly", target.sprite.name, "sprite"); + this.checkCustomFormatRules(target.sprite.name, "sprite"); + } + } + } + formatCustomBlock(block) { + const mutation = block.mutation; + const args = JSON.parse(mutation.argumentnames); + + console.log(args); + + let i = 0; + const name = mutation.proccode.replace(/%[snb]/g, (match) => { + let value = args[i++]; + if (match === "%s") return isUnSandBoxed ? `[${value}]` : `(${value})`; + if (match === "%n" && isUnSandBoxed) return `(${value})`; + if (match === "%b") return `<${value}>`; + return match; + }); + return name; + } + + _checkCustomBlockFormatting() { + const blocks = !(this.getCustomBlocks().length > 0) + ? [] + : this.getCustomBlocks(); + + for (const block of blocks) { + this.checkFormatRule("customNoCapitalized", block, "custom_block"); + this.checkFormatRule("camelCaseOnly", block, "custom_block"); + this.checkCustomFormatRules(block, "custom_block"); + } + } + + _checkVariableFormatting() { + const variables = this.getVariables(); + + // Local variable check + console.log("checking local variables"); + for (const variable of variables.local) { + this.checkFormatRule("griffpatchStyle", variable, "variable", { + isGlobal: false, + }); + this.checkFormatRule("camelCaseOnly", variable, "variable"); + this.checkCustomFormatRules(variable, "variable"); + } + + // Global variable check + console.log("checking global variables"); + for (const variable of variables.global) { + this.checkFormatRule("griffpatchStyle", variable, "variable", { + isGlobal: true, + }); + this.checkFormatRule("camelCaseOnly", variable, "custom_block"); + this.checkCustomFormatRules(variable, "variable"); + } + } + + checkFormatting() { + if (!isEditor) return; + this.formatErrors = []; + + this._checkSpriteFormatting(); + this._checkVariableFormatting(); + this._checkCustomBlockFormatting(); + + if (this.formatErrors.length !== 0) { + openModal("error", "Format Error", this.formatErrors); + } else { + alert("No format errors found!"); + } + + console.log(formatError(this.formatErrors)); + } + newFormatRule() { + if (!isEditor) return; // return if we aren't in the editor + newRuleModal( + Scratch.translate("New Rule:"), + funcTypes, + false, + (ruleName, regex, func, scopes) => { + if (!ruleName || !regex) + return alert(Scratch.translate("Missing inputs")); + try { + new RegExp(regex.split("/")[1], regex.split("/")[2]); + } catch { + alert(Scratch.translate("Invalid Regular Expression")); + return; + } + + customRules[ruleName] = { + funcType: func.value, + enabled: true, + level: "warn", + scopes: scopes, + msg: `"{str}" isn't following the custom rule named {str}`, + regex: regex, + }; + + console.log(customRules); + } + ); + } + + delFormatRule() { + if (!isEditor) return; // return if we aren't in the editor + const customRulesList = Object.keys(customRules); + if (customRulesList.length < 1) return alert("There are no Custom Rules"); + + newRuleModal( + Scratch.translate("Delete Rule:"), + customRulesList, + true, + (name) => { + console.log(name); + delete customRules[name]; + + console.log(customRules); + } + ); + } + + formatErrorsReporter() { + return JSON.stringify(this.formatErrors); + } + fancyFormatErrors(args) { + try { + return formatError(JSON.parse(args.FORMAT_ERROR)); + } catch { + return ""; + } + } + + // Dynamic Menus + _getVariablesMenu() { + const stage = runtime.getTargetForStage(); + const targets = runtime.targets; + + const globalVars = Object.values(stage.variables) + .filter((v) => v.type !== "list") + .map((v) => v.name); + + const allVars = targets + .filter((t) => t.isOriginal) + .map((t) => t.variables); + const localVars = allVars + .map((v) => Object.values(v)) + .map((v) => + v + .filter((v) => v.type !== "list" && !globalVars.includes(v.name)) + .map((v) => v.name) + ) + .flat(1); + + return localVars.concat(globalVars); + } + + _getCustomBlocksMenu() { + const targets = runtime.targets; + const customBlocks = []; + + for (const target of targets) { + const blocks = target.blocks._blocks; + for (const blockId in blocks) { + const block = blocks[blockId]; + if (block.opcode === "procedures_prototype") { + customBlocks.push(this.formatCustomBlock(block)); + } + } + } + + return customBlocks.length > 0 + ? customBlocks + : ["no custom blocks found"]; + } + } + + if (isEditor) { + vm.on("EXTENSION_ADDED", () => { + runtime.extensionStorage["HamPrettyBlocks"] = JSON.stringify({ + rules: JSON.stringify(rules), + customRules: JSON.stringify(customRules), + ignore: JSON.stringify(ignoreList), + }); + }); + vm.on("BLOCKSINFO_UPDATE", () => { + runtime.extensionStorage["HamPrettyBlocks"] = JSON.stringify({ + rules: JSON.stringify(rules), + customRules: JSON.stringify(customRules), + ignore: JSON.stringify(ignoreList), + }); + }); + } + + // @ts-ignore + Scratch.extensions.register(new HamPrettyBlocks()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index d8ed71e33d..f22f23253d 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -89,6 +89,7 @@ "vercte/dictionaries", "godslayerakp/http", "godslayerakp/ws", + "Hammouda101010/prettyblocks", "Lily/CommentBlocks", "veggiecan/LongmanDictionary", "CubesterYT/TurboHook", From 50a96c7948b1b23a823e6a16c6da9d6d610d1ad3 Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:29:36 +0100 Subject: [PATCH 02/10] refactor errorModal --- extensions/Hammouda101010/prettyblocks.js | 61 ++++++++++++++++------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index bdf30eeec4..6d0919a2a5 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -58,16 +58,16 @@ const errorModal = (titleName = "Alert", error = []) => { //@ts-ignore // run prompt to get a GUI to modify - ScratchBlocks.prompt(); + ScratchBlocks.prompt( + "test", + "", + () => {}, + Cast.toString(titleName), + "broadcast_msg" + ); // get the portal/modal and its header const portal = document.querySelector("div.ReactModalPortal"); - const header = portal.querySelector( - 'div[class*="modal_header-item-title_"]' - ); - - // add our own custom title - header.textContent = Cast.toString(titleName); // get the portal/modal body const portalBody = portal.querySelector('div[class^="prompt_body_"]'); const portalHolder = portalBody.parentElement.parentElement; @@ -77,20 +77,45 @@ const errorString = formatError(error, true); - const errorHTML = ` - `; - const contentHTML = `

${Scratch.translate("Errors found in project:")}

${errorHTML}

${Scratch.translate('Make sure to fix these manually or with the "Format project" button.')}

`; - const promptButtonPos = `
${portalBody.querySelector("div[class^=prompt_button-row_]").outerHTML}
`; + // Create the custom modal elements + const labelA = document.createElement("p"); + labelA.textContent = Scratch.translate( + "The extension has found errors in your project:" + ); - portalBody.innerHTML = `${contentHTML}${promptButtonPos}`; - const textarea = portalBody.querySelector( - 'textarea[class^="data-url_code_1o8oS"]' + // The error text area + const errorTextArea = document.createElement("textarea"); + + errorTextArea.setAttribute("class", "data-url_code_1o8oS"); + errorTextArea.setAttribute("readonly", "true"); + errorTextArea.setAttribute("spellcheck", "false"); + errorTextArea.setAttribute("autocomplete", "false"); + + errorTextArea.style.display = "inline-block"; + errorTextArea.style.width = "100%"; + errorTextArea.style.height = "12rem"; + errorTextArea.value = errorString; + + const labelB = document.createElement("p"); + labelB.textContent = Scratch.translate( + 'Make sure to fix them manualy or with the "Format Project" button.' ); - //@ts-expect-error - textarea.value = textarea.value.trim(); + // Wrap them inside a div + const div = document.createElement("div"); + div.setAttribute( + "style", + "display:inline-block;width:-webkit-fill-available;height:calc(100% - 2.75rem);" + ); + div.setAttribute("class", "error_list_1o85"); + div.append(labelA, errorTextArea, labelB); + + const input = document.querySelector(`div[class="ReactModalPortal"] input`); + input.parentNode.append(div); + input.parentNode.previousSibling.remove(); + input.remove(); + + errorTextArea.value = errorTextArea.value.trim(); // creating our OK button const okButton = portalBody.querySelector( From fa1f0bbfbdd54399c367b2f01e91c4e383e1cdc0 Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:46:06 +0100 Subject: [PATCH 03/10] more patches GOD DANG IT! --- docs/Hammouda101010/prettyblocks.md | 28 ++++- extensions/Hammouda101010/prettyblocks.js | 123 +++++++++++++++++----- images/Hammouda101010/prettyblocks.svg | 90 ++++++++++++++++ 3 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 images/Hammouda101010/prettyblocks.svg diff --git a/docs/Hammouda101010/prettyblocks.md b/docs/Hammouda101010/prettyblocks.md index d12c21a4aa..8b79b074fd 100644 --- a/docs/Hammouda101010/prettyblocks.md +++ b/docs/Hammouda101010/prettyblocks.md @@ -1,5 +1,12 @@ # Pretty Blocks An extension to add strict formatting rules to your project. +## Table of Contents +- [Pretty Blocks](#pretty-blocks) + - [Rules](#rules) + - [Creating Custom Rules](#creating-custom-rules) + - [RegBool](#regbool) + - [Format Functions](#format-functions) + ## Rules - Camel Case Only: - Affects all variables and sprites. @@ -11,4 +18,23 @@ An extension to add strict formatting rules to your project. - Affects custom blocks only. - Makes sure that custom block names aren't capitalized. ## Creating Custom Rules -if you want to create your own custom rules \ No newline at end of file +If you want to create your own custom rules, first press the "Add Format Rule" button. then fill out these fields: +- The rule name +- The regular expression (or RegBool code) +- The format function +- And optionaly, the scopes +### RegBool +This is a "programming language" that can make simple boolean operations, in order to add logic to your custom rules. + +Here is the available syntax for RegBool: +- ``: represents a boolean value, can be `true`, `false` or a boolean function. +- `if & else`: the tenary operators +### Format Functions +There are a limited amount of functions available for custom rules. more rules might come at the future, but here are the available ones: +- To uppercase: formats the subject's text to "UPPERCASE" +- To lowercase: formats the subject's text to "lowercase" +- regex validation: formats the subject's text to be compatible with the custom rule's regex. +- To camelCase: formats the subject's text to [camelCase](https://en.wikipedia.org/wiki/Camel_case) +- To snake_case: formats the subject's text to [snake_case](https://en.wikipedia.org/wiki/Snake_case) +- To PascalCase: much like "To camelCase", but capitalize the first letter too. +- Space trimming: trims the subject's text \ No newline at end of file diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index 6d0919a2a5..f647831054 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -8,7 +8,7 @@ "use strict"; if (!Scratch.extensions.unsandboxed) { - throw new Error("Pretty Blocks extension must run unsandboxed"); + throw new Error("Pretty Blocks must run unsandboxed"); } const vm = Scratch.vm; @@ -156,7 +156,7 @@ !deleteRule ? titleName : "test", "", !deleteRule ? () => func(name, regex, funcType, scope) : () => func(name), - "Format Rules Manager", + Scratch.translate("Format Rules Manager"), "broadcast_msg" ); @@ -211,7 +211,7 @@ }); const regexLabel = input.parentNode.previousSibling.cloneNode(true); - regexLabel.textContent = "Regular Expression:"; + regexLabel.textContent = Scratch.translate("Regular Expression:"); const regexInput = document.createElement("input"); regexInput.setAttribute("class", input.getAttribute("class")); @@ -222,7 +222,7 @@ // Format Function (The funtction to use when formatting the project.) const funcTypeLabel = input.parentNode.previousSibling.cloneNode(true); - funcTypeLabel.textContent = "Format Function:"; + funcTypeLabel.textContent = Scratch.translate("Format Function:"); const selector = document.createElement("select"); selector.setAttribute("class", input.getAttribute("class")); selector.addEventListener("input", (e) => { @@ -275,14 +275,74 @@ } } - let ignoreList; + const squareInputBlocks = ["HamPrettyBlocks_fancyFormatErrors"]; + + // Custom Square Block Shapes + const ogConverter = runtime._convertBlockForScratchBlocks.bind(runtime); + runtime._convertBlockForScratchBlocks = function (blockInfo, categoryInfo) { + const res = ogConverter(blockInfo, categoryInfo); + if (blockInfo.outputShape) res.json.outputShape = blockInfo.outputShape; + return res; + }; + + if (Scratch.gui) + Scratch.gui.getBlockly().then((SB) => { + // Custom Square Input Shape + const makeShape = (width, height) => { + width -= 10; + // prettier-ignore + height -= 8 + return ` + m 0 4 + A 4 4 0 0 1 4 0 H ${width} + a 4 4 0 0 1 4 4 + v ${height} + a 4 4 0 0 1 -4 4 + H 4 + a 4 4 0 0 1 -4 -4 + z` + .replaceAll("\n", "") + .trim(); + }; + + const ogRender = SB.BlockSvg.prototype.render; + SB.BlockSvg.prototype.render = function (...args) { + const data = ogRender.call(this, ...args); + if (this.svgPath_ && squareInputBlocks.includes(this.type)) { + this.inputList.forEach((input) => { + if (input.name.startsWith("ARRAY")) { + const block = input.connection.targetBlock(); + if ( + block && + block.type === "text" && + block.svgPath_ && + block.type.startsWith("HamPrettyBlocks_menu_") + ) { + block.svgPath_.setAttribute( + "transform", + `scale(1, ${block.height / 33})` + ); + block.svgPath_.setAttribute( + "d", + makeShape(block.width, block.height) + ); + } else if (input.outlinePath) { + input.outlinePath.setAttribute("d", makeShape(46, 32)); + } + } + }); + } + return data; + }; + }); + + let ignoreList = []; // Function Types for Custom Rules. const funcTypes = [ { text: Scratch.translate("to uppercase"), value: "uppercase" }, { text: Scratch.translate("to lowercase"), value: "lowercase" }, { text: Scratch.translate("regex validation"), value: "regex_validation" }, - { text: Scratch.translate("to lowercase"), value: "lowercase" }, { text: Scratch.translate("to camelCase"), value: "camelcase" }, { text: Scratch.translate("to snake_case"), value: "snakecase" }, { text: Scratch.translate("to PascalCase"), value: "pascal_case" }, @@ -342,6 +402,8 @@ id: "HamPrettyBlocks", name: Scratch.translate("Pretty Blocks"), docsURI: "http://localhost:8000/Hammouda101010/prettyblocks", // https://extensions.turbowarp.org/Hammouda101010/prettyblocks + color1: "#0071b0", + color2: "#006095", blocks: [ { func: "checkFormatting", @@ -381,17 +443,22 @@ }, }, }, + "---", { opcode: "formatErrorsReporter", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("format errors"), + text: Scratch.translate("format errors"), // format errors + disableMonitor: true, + outputShape: 3, }, { opcode: "fancyFormatErrors", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("fancify format errors [FORMAT_ERROR]"), + text: Scratch.translate( + "fancify format errors [ARRAY_FORMAT_ERROR]" + ), arguments: { - FORMAT_ERROR: {}, + ARRAY_FORMAT_ERROR: {}, }, }, ], @@ -416,14 +483,14 @@ // Check for each spaces for (const line of logicCodeLineArray) { - // if it's a boolean - if (/<.*>/.test(line)) { - // is it a primitive value? + // Check if it's a boolean + if (/\<([^>]+)\>/g.test(line)) { + // Is it a primitive boolean value? if (line === "") { boolResult = true; } else if (line === "") { boolResult = false; - // otherwise, it's an argument + // Otherwise, it's an argument } else { const optsArray = Object.values(opts).map((value) => Cast.toBoolean(value) @@ -441,6 +508,7 @@ } } } else { + // Tenary operator logic if (line === "if") { continue; } else if (line === "else") { @@ -494,9 +562,8 @@ const localVars = allVars .map((v) => Object.values(v)) .map((v) => - v - .filter((v) => v.type !== "list" && !globalVars.includes(v.name)) - .map((v) => v.name) + // prettier-ignore + v.filter((v) => v.type !== "list" && !globalVars.includes(v.name)).map((v) => v.name) ) .flat(1); @@ -516,9 +583,6 @@ } const regex = new RegExp(str.split("/")[1], str.split("/")[2]); - console.log(regex); - console.log(regex.test(val)); - console.log(regex.test(val)); switch (rule) { case "griffpatchStyle": @@ -549,7 +613,9 @@ type: type, level: rules[rule].level, subject: val, - msg: Scratch.translate(rules[rule].msg), + msg: Scratch.translate( + Cast.toString(rules[rule].msg).replace(/\{([^}]+)\}/g, val) + ), }); } break; @@ -564,12 +630,17 @@ customRules[rule].scopes.includes(type)) || customRules[rule].scopes.includes("all") ) { - if (!customRules[rule].check(val)) { + let str = Cast.toString(rules[rule].regex); + + const regex = new RegExp(str.split("/")[1], str.split("/")[2]); + if (!regex.test(val)) { this.formatErrors.push({ type: type, level: customRules[rule].level, subject: val, - msg: Scratch.translate(customRules[rule].msg(val)), + msg: Scratch.translate( + Cast.toString(rules[rule].msg).replace(/\{([^}]+)\}/g, val) + ), }); } } @@ -578,6 +649,7 @@ _checkSpriteFormatting() { const targets = runtime.targets; + console.log("checking sprites"); for (const target of targets) { if (target.isSprite()) { // Format check @@ -608,6 +680,7 @@ ? [] : this.getCustomBlocks(); + console.log("checking custom blocks"); for (const block of blocks) { this.checkFormatRule("customNoCapitalized", block, "custom_block"); this.checkFormatRule("camelCaseOnly", block, "custom_block"); @@ -618,7 +691,7 @@ _checkVariableFormatting() { const variables = this.getVariables(); - // Local variable check + // Local variable format check console.log("checking local variables"); for (const variable of variables.local) { this.checkFormatRule("griffpatchStyle", variable, "variable", { @@ -628,7 +701,7 @@ this.checkCustomFormatRules(variable, "variable"); } - // Global variable check + // Global variable format check console.log("checking global variables"); for (const variable of variables.global) { this.checkFormatRule("griffpatchStyle", variable, "variable", { @@ -708,7 +781,7 @@ } fancyFormatErrors(args) { try { - return formatError(JSON.parse(args.FORMAT_ERROR)); + return formatError(JSON.parse(args.ARRAY_FORMAT_ERROR)); } catch { return ""; } diff --git a/images/Hammouda101010/prettyblocks.svg b/images/Hammouda101010/prettyblocks.svg new file mode 100644 index 0000000000..3bad7a2a2f --- /dev/null +++ b/images/Hammouda101010/prettyblocks.svg @@ -0,0 +1,90 @@ + + + + From 536e5d583a8c3cc4b132d9008094ef0c0c2b3f0c Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:59:40 +0100 Subject: [PATCH 04/10] new block and ignore list --- extensions/Hammouda101010/prettyblocks.js | 119 +++++++++++++--------- 1 file changed, 73 insertions(+), 46 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index f647831054..fa0d9298f0 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -336,7 +336,7 @@ }; }); - let ignoreList = []; + let ignoreList = new Set(); // Function Types for Custom Rules. const funcTypes = [ @@ -374,21 +374,13 @@ // Turbowarp's extension storage runtime.on("PROJECT_LOADED", () => { - vm.extensionManager.refreshBlocks(); - try { - // @ts-ignore - const storage = JSON.parse(runtime.extensionStorage["HamPrettyBlocks"]); - - if (storage) { - ignoreList = storage.ignoreList ? JSON.parse(storage.ignoreList) : []; - - rules = storage.rules ? JSON.parse(storage.rules) : rules; - customRules = storage.customRules - ? JSON.parse(storage.customRules) - : customRules; - } - } catch (e) { - console.error(e); + // @ts-ignore + const storage = runtime.extensionStorage["HamPrettyBlocks"]; + + if (storage) { + // ignoreList = new Set(JSON.parse(storage.ignoreList)) + rules = JSON.parse(storage.rules); + customRules = JSON.parse(storage.customRules); } }); @@ -425,6 +417,7 @@ opcode: "ignoreVariable", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("ignore variable named [VAR_MENU]"), + color1: "#848484", arguments: { VAR_MENU: { type: Scratch.ArgumentType.STRING, @@ -436,6 +429,7 @@ opcode: "ignoreCustomBlock", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("ignore custom block named [BLOCK_MENU]"), + color1: "#848484", arguments: { BLOCK_MENU: { type: Scratch.ArgumentType.STRING, @@ -444,6 +438,12 @@ }, }, "---", + { + opcode: "checkFormatttingBlock", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("check project formatting"), + }, + "---", { opcode: "formatErrorsReporter", blockType: Scratch.BlockType.REPORTER, @@ -464,17 +464,25 @@ ], menus: { PRETTYBLOCKS_CUSTOM_BLOCKS: { - acceptReporters: true, + acceptReporters: false, items: "_getCustomBlocksMenu", }, PRETTYBLOCKS_VARIABLES: { - acceptReporters: true, + acceptReporters: false, items: "_getVariablesMenu", }, }, }; } // Class Utilities + refreshBlocks() { + vm.extensionManager.refreshBlocks(); + runtime.extensionStorage["HamPrettyBlocks"] = { + rules: JSON.stringify(rules), + customRules: JSON.stringify(customRules), + ignore: ignoreList ? JSON.stringify([...ignoreList]) : "[]", + }; + } getLogic(str, opts) { let codeLine = Cast.toString(str); const logicCodeLineArray = codeLine.split(" "); @@ -484,7 +492,7 @@ // Check for each spaces for (const line of logicCodeLineArray) { // Check if it's a boolean - if (/\<([^>]+)\>/g.test(line)) { + if (/<([^>]+)>/g.test(line)) { // Is it a primitive boolean value? if (line === "") { boolResult = true; @@ -540,7 +548,10 @@ for (const blockId in blocks) { const block = blocks[blockId]; if (block.opcode === "procedures_prototype") { - customBlocks.push(this.formatCustomBlock(block)); + customBlocks.push({ + text: this.formatCustomBlock(block), + value: block.id, + }); } } } @@ -662,8 +673,6 @@ const mutation = block.mutation; const args = JSON.parse(mutation.argumentnames); - console.log(args); - let i = 0; const name = mutation.proccode.replace(/%[snb]/g, (match) => { let value = args[i++]; @@ -681,10 +690,14 @@ : this.getCustomBlocks(); console.log("checking custom blocks"); - for (const block of blocks) { - this.checkFormatRule("customNoCapitalized", block, "custom_block"); - this.checkFormatRule("camelCaseOnly", block, "custom_block"); - this.checkCustomFormatRules(block, "custom_block"); + for (const block in blocks) { + if (!ignoreList.has(blocks[block].value)) { + // prettier-ignore + this.checkFormatRule("customNoCapitalized", blocks[block].text, "custom_block"); + // prettier-ignore + this.checkFormatRule("camelCaseOnly", blocks[block].text, "custom_block"); + this.checkCustomFormatRules(blocks[block].text, "custom_block"); + } } } @@ -753,6 +766,7 @@ regex: regex, }; + this.refreshBlocks(); console.log(customRules); } ); @@ -770,12 +784,37 @@ (name) => { console.log(name); delete customRules[name]; + this.refreshBlocks(); console.log(customRules); } ); } + ignoreVariable(args) { + if (Cast.toBoolean(args.VAR_MENU)) { + ignoreList.add(args.VAR_MENU); + this.refreshBlocks(); + } + console.log(ignoreList); + } + ignoreCustomBlock(args) { + if (Cast.toBoolean(args.BLOCK_MENU)) { + ignoreList.add(args.BLOCK_MENU); + this.refreshBlocks(); + } + console.log(ignoreList); + } + + checkFormatttingBlock() { + if (!isEditor) return; + this.formatErrors = []; + + this._checkSpriteFormatting(); + this._checkVariableFormatting(); + this._checkCustomBlockFormatting(); + } + formatErrorsReporter() { return JSON.stringify(this.formatErrors); } @@ -808,7 +847,9 @@ ) .flat(1); - return localVars.concat(globalVars); + return localVars.concat(globalVars) + ? localVars.concat(globalVars) + : [{ text: Scratch.translate("no variables found"), value: [] }]; } _getCustomBlocksMenu() { @@ -820,34 +861,20 @@ for (const blockId in blocks) { const block = blocks[blockId]; if (block.opcode === "procedures_prototype") { - customBlocks.push(this.formatCustomBlock(block)); + customBlocks.push({ + text: this.formatCustomBlock(block), + value: block.id, + }); } } } return customBlocks.length > 0 ? customBlocks - : ["no custom blocks found"]; + : [{ text: Scratch.translate("no custom blocks found"), value: false }]; } } - if (isEditor) { - vm.on("EXTENSION_ADDED", () => { - runtime.extensionStorage["HamPrettyBlocks"] = JSON.stringify({ - rules: JSON.stringify(rules), - customRules: JSON.stringify(customRules), - ignore: JSON.stringify(ignoreList), - }); - }); - vm.on("BLOCKSINFO_UPDATE", () => { - runtime.extensionStorage["HamPrettyBlocks"] = JSON.stringify({ - rules: JSON.stringify(rules), - customRules: JSON.stringify(customRules), - ignore: JSON.stringify(ignoreList), - }); - }); - } - // @ts-ignore Scratch.extensions.register(new HamPrettyBlocks()); })(Scratch); From f7920f0345cf7a205db4a1383f1252eb046c8c54 Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:30:35 +0100 Subject: [PATCH 05/10] updated isUnSandBoxed --- extensions/Hammouda101010/prettyblocks.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index fa0d9298f0..e1055f81c5 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -16,10 +16,9 @@ const Cast = Scratch.Cast; /**Checks if the extension is in "penguinmod.com".*/ // @ts-ignore - const _isPM = Scratch.extensions.isPenguinMod; + const _isPM = runtime.platform.url === "https://penguinmod.com/"; /**Checks if the extension is in "unsandboxed.org".*/ - const isUnSandBoxed = - JSON.parse(vm.toJSON()).meta.platform.url === "https://unsandboxed.org/"; + const isUnSandBoxed = runtime.platform.url === "https://unsandboxed.org/"; /**Checks if the extension is inside the editor.*/ const isEditor = typeof scaffolding === "undefined"; @@ -498,7 +497,7 @@ boolResult = true; } else if (line === "") { boolResult = false; - // Otherwise, it's an argument + // Otherwise, it's an argument/option } else { const optsArray = Object.values(opts).map((value) => Cast.toBoolean(value) From 912ca8682a85d85cadf535067d73444002af5fba Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:06:30 +0100 Subject: [PATCH 06/10] removed support for other mods --- extensions/Hammouda101010/prettyblocks.js | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index e1055f81c5..5e301e24da 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -16,9 +16,6 @@ const Cast = Scratch.Cast; /**Checks if the extension is in "penguinmod.com".*/ // @ts-ignore - const _isPM = runtime.platform.url === "https://penguinmod.com/"; - /**Checks if the extension is in "unsandboxed.org".*/ - const isUnSandBoxed = runtime.platform.url === "https://unsandboxed.org/"; /**Checks if the extension is inside the editor.*/ const isEditor = typeof scaffolding === "undefined"; @@ -561,10 +558,9 @@ const stage = runtime.getTargetForStage(); const targets = runtime.targets; - // Sort the variables const globalVars = Object.values(stage.variables) .filter((v) => v.type !== "list") - .map((v) => v.name); + .map((v) => ({ text: v.name, value: v.id })); const allVars = targets .filter((t) => t.isOriginal) @@ -573,7 +569,10 @@ .map((v) => Object.values(v)) .map((v) => // prettier-ignore - v.filter((v) => v.type !== "list" && !globalVars.includes(v.name)).map((v) => v.name) + v.filter( + (v) => + v.type !== "list" && !globalVars.map((obj) => obj.text).includes(v.name) + ).map((v) => ({ text: v.name, value: v.id })) ) .flat(1); @@ -675,8 +674,8 @@ let i = 0; const name = mutation.proccode.replace(/%[snb]/g, (match) => { let value = args[i++]; - if (match === "%s") return isUnSandBoxed ? `[${value}]` : `(${value})`; - if (match === "%n" && isUnSandBoxed) return `(${value})`; + if (match === "%s") return `[${value}]`; + if (match === "%n") return `(${value})`; if (match === "%b") return `<${value}>`; return match; }); @@ -832,7 +831,7 @@ const globalVars = Object.values(stage.variables) .filter((v) => v.type !== "list") - .map((v) => v.name); + .map((v) => ({ text: v.name, value: v.id })); const allVars = targets .filter((t) => t.isOriginal) @@ -840,15 +839,17 @@ const localVars = allVars .map((v) => Object.values(v)) .map((v) => - v - .filter((v) => v.type !== "list" && !globalVars.includes(v.name)) - .map((v) => v.name) + // prettier-ignore + v.filter( + (v) => + v.type !== "list" && !globalVars.map((obj) => obj.text).includes(v.name) + ).map((v) => ({ text: v.name, value: v.id })) ) .flat(1); return localVars.concat(globalVars) ? localVars.concat(globalVars) - : [{ text: Scratch.translate("no variables found"), value: [] }]; + : [{ text: Scratch.translate("no variables found"), value: false }]; } _getCustomBlocksMenu() { From 35dadb2fabfa4a48fbd8b640c8003e5c986d1d4a Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:45:08 +0100 Subject: [PATCH 07/10] added sprite origin to local variables --- extensions/Hammouda101010/prettyblocks.js | 54 ++++++++++++++--------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index 5e301e24da..8757f88355 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -585,6 +585,7 @@ } checkFormatRule(rule, val, type, opts = {}) { + if (ignoreList.has(val.value)) return; if (rules[rule].enabled) { let str = Cast.toString(rules[rule].regex); if (str.startsWith("if")) { @@ -595,11 +596,11 @@ switch (rule) { case "griffpatchStyle": - if (!regex.test(val)) { + if (!regex.test(val.text)) { this.formatErrors.push({ type: type, level: rules[rule].level, - subject: val, + subject: val.text, msg: Scratch.translate( Cast.toString(rules[rule].msg).replace( /\{([^}]+)\}/g, @@ -608,7 +609,7 @@ if (e === "{isGlobal}") { return opts.isGlobal ? "UPPERCASE" : "lowercase"; } else { - return val; + return val.text; } } ) @@ -621,7 +622,7 @@ this.formatErrors.push({ type: type, level: rules[rule].level, - subject: val, + subject: val.text, msg: Scratch.translate( Cast.toString(rules[rule].msg).replace(/\{([^}]+)\}/g, val) ), @@ -633,6 +634,7 @@ } checkCustomFormatRules(val, type) { + if (ignoreList.has(val.value)) return; for (const rule in customRules) { if ( (customRules[rule].enabled && @@ -642,11 +644,11 @@ let str = Cast.toString(rules[rule].regex); const regex = new RegExp(str.split("/")[1], str.split("/")[2]); - if (!regex.test(val)) { + if (!regex.test(val.text)) { this.formatErrors.push({ type: type, level: customRules[rule].level, - subject: val, + subject: val.text, msg: Scratch.translate( Cast.toString(rules[rule].msg).replace(/\{([^}]+)\}/g, val) ), @@ -657,14 +659,14 @@ } _checkSpriteFormatting() { - const targets = runtime.targets; + const targets = runtime.targets + .filter((t) => t.isSprite()) + .map((t) => ({ text: t.sprite.name, value: t.id })); console.log("checking sprites"); for (const target of targets) { - if (target.isSprite()) { - // Format check - this.checkFormatRule("camelCaseOnly", target.sprite.name, "sprite"); - this.checkCustomFormatRules(target.sprite.name, "sprite"); - } + // Format check + this.checkFormatRule("camelCaseOnly", target, "sprite"); + this.checkCustomFormatRules(target, "sprite"); } } formatCustomBlock(block) { @@ -732,12 +734,11 @@ this._checkCustomBlockFormatting(); if (this.formatErrors.length !== 0) { + console.log(formatError(this.formatErrors)); openModal("error", "Format Error", this.formatErrors); } else { alert("No format errors found!"); } - - console.log(formatError(this.formatErrors)); } newFormatRule() { if (!isEditor) return; // return if we aren't in the editor @@ -835,15 +836,24 @@ const allVars = targets .filter((t) => t.isOriginal) - .map((t) => t.variables); + .map((t) => ({ spriteName: t.sprite.name, variables: t.variables })); + const localVars = allVars - .map((v) => Object.values(v)) + .map((t) => ({ + spriteName: t.spriteName, + vars: Object.values(t.variables), + })) .map((v) => - // prettier-ignore - v.filter( - (v) => - v.type !== "list" && !globalVars.map((obj) => obj.text).includes(v.name) - ).map((v) => ({ text: v.name, value: v.id })) + v.vars + .filter( + (variable) => + variable.type !== "list" && + !globalVars.map((obj) => obj.text).includes(variable.name) + ) + .map((variable) => ({ + text: `${v.spriteName}: ${variable.name}`, + value: variable.id, + })) ) .flat(1); @@ -862,7 +872,7 @@ const block = blocks[blockId]; if (block.opcode === "procedures_prototype") { customBlocks.push({ - text: this.formatCustomBlock(block), + text: `${target.sprite.name}: ${this.formatCustomBlock(block)}`, value: block.id, }); } From 2fd4f45de13e1ead58b67ea8023134a2bbd7f219 Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:31:11 +0100 Subject: [PATCH 08/10] added format project button --- docs/Hammouda101010/prettyblocks.md | 36 ++++++- extensions/Hammouda101010/prettyblocks.js | 121 +++++++++++++++++++++- 2 files changed, 152 insertions(+), 5 deletions(-) diff --git a/docs/Hammouda101010/prettyblocks.md b/docs/Hammouda101010/prettyblocks.md index 8b79b074fd..e41770fbb6 100644 --- a/docs/Hammouda101010/prettyblocks.md +++ b/docs/Hammouda101010/prettyblocks.md @@ -6,6 +6,12 @@ An extension to add strict formatting rules to your project. - [Creating Custom Rules](#creating-custom-rules) - [RegBool](#regbool) - [Format Functions](#format-functions) + - [Blocks](#blocks) + - [Ignore List](#ignore-list) + - [Ignore Variable Named \[\]](#ignore-variable-named-) + - [Ignore Custom Block Named \[\]](#ignore-custom-block-named-) + - [Reset Ignore List](#reset-ignore-list) + - [Check Project Formatting](#check-project-formatting) ## Rules - Camel Case Only: @@ -37,4 +43,32 @@ There are a limited amount of functions available for custom rules. more rules m - To camelCase: formats the subject's text to [camelCase](https://en.wikipedia.org/wiki/Camel_case) - To snake_case: formats the subject's text to [snake_case](https://en.wikipedia.org/wiki/Snake_case) - To PascalCase: much like "To camelCase", but capitalize the first letter too. -- Space trimming: trims the subject's text \ No newline at end of file +- Space trimming: trims the subject's text +## Blocks +Here are a list of blocks to interact with the formatter. +### Ignore List +```scratch +ignore list :: #848484 reporter +``` +Shows the list of all sprites, variables, etc. that are ignored by the formatter as a set. +Keep in mind that it stores values by ID, not by name. +### Ignore Variable Named [] +```scratch +ignore variable named [my variable v] :: #848484 +``` +Adds a variable to the ignore list. It can also acces all local variables. +### Ignore Custom Block Named [] +```scratch +ignore custom block named [block name \(number\) \[text\] \ v] :: #848484 +``` +Adds a custom block to the ignore list. +### Reset Ignore List +```scratch +reset ignore list :: #848484 +``` +Resets the ignore list. +### Check Project Formatting +```scratch +check project formatting :: #0071b0 +``` +Does the same thing as the "Check Project Formatting" button. \ No newline at end of file diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index 8757f88355..84f561d6b2 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -14,9 +14,7 @@ const vm = Scratch.vm; const runtime = vm.runtime; const Cast = Scratch.Cast; - /**Checks if the extension is in "penguinmod.com".*/ - // @ts-ignore - /**Checks if the extension is inside the editor.*/ + const workspace = ScratchBlocks.getMainWorkspace(); const isEditor = typeof scaffolding === "undefined"; function formatError(errors, logToConsole = false) { @@ -398,6 +396,11 @@ blockType: Scratch.BlockType.BUTTON, text: Scratch.translate("Check Project Formatting"), }, + { + func: "formatProject", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Format Project"), + }, "---", { func: "newFormatRule", @@ -409,6 +412,12 @@ blockType: Scratch.BlockType.BUTTON, text: Scratch.translate("Delete Format Rule"), }, + { + opcode: "ignoreList", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("ignore list"), + color1: "#848484", + }, { opcode: "ignoreVariable", blockType: Scratch.BlockType.COMMAND, @@ -434,6 +443,13 @@ }, }, "---", + { + opcode: "resetIgnoreList", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("reset ignore list"), + color1: "#848484", + }, + "---", { opcode: "checkFormatttingBlock", blockType: Scratch.BlockType.COMMAND, @@ -472,6 +488,7 @@ } // Class Utilities refreshBlocks() { + vm.refreshWorkspace(); vm.extensionManager.refreshBlocks(); runtime.extensionStorage["HamPrettyBlocks"] = { rules: JSON.stringify(rules), @@ -633,6 +650,82 @@ } } + /** + * Formats a variable using a rule. + * @param {string} rule The rule used to format the variable name. + * @param { {name: string, id: string}} targetVariable The target variable to format the name of. + * @param {object} opts Optional options. + */ + _formatVariable(rule, targetVariable, opts) { + const targets = runtime.targets; + const stage = runtime.getTargetForStage(); + if (opts.isGlobal) { + for (const variable of Object.values(stage.variables)) { + if (variable.id === targetVariable.id) + workspace.renameVariableById( + variable.id, + this.formatRule(rule, variable.name, opts) + ); + } + } else { + for (const target of targets) { + if (target.isSprite()) { + const variable = target.lookupOrCreateVariable( + targetVariable.id, + targetVariable.name + ); + if (variable.id in stage.variables) return; + // @ts-ignore + if (variable.type !== "list") + workspace.renameVariableById( + variable.id, + this.formatRule( + rule, + targetVariable.name, + targetVariable.id, + opts + ) + ); + } + } + } + } + + _formatVariables() { + const variables = this.getVariables(); + for (const variable of variables.local) { + const variableData = { name: variable.text, id: variable.value }; + this._formatVariable("griffpatchStyle", variableData, { + isGlobal: false, + }); + } + for (const variable of variables.global) { + const variableData = { name: variable.text, id: variable.value }; + this._formatVariable("griffpatchStyle", variableData, { + isGlobal: true, + }); + } + } + + formatRule(rule, val, valID, opts = {}) { + if (ignoreList.has(valID)) return; + if (rules[rule].enabled) { + switch (rule) { + case "griffpatchStyle": + const { isGlobal } = opts; + return isGlobal ? val.toUpperCase() : val.toLowerCase(); + case "camelCaseOnly": + // prettier-ignore + return val.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase()); + case "customNoCapitalized": + // prettier-ignore + return val.charAt(0).toLowerCase() + val.slice(1); + } + } else { + return val; + } + } + checkCustomFormatRules(val, type) { if (ignoreList.has(val.value)) return; for (const rule in customRules) { @@ -740,6 +833,17 @@ alert("No format errors found!"); } } + + formatProject() { + if (!isEditor) return; + // prettier-ignore + if (confirm("!~~~WARNING~~~! \n\n This will format the entire project according to the enabled rules. \n\n This process is irreversible and might break the entire project. \n Do you want to proceed?")){ + console.log("formatting project...") + this._formatVariables() + console.info("formatting completed") + } + } + newFormatRule() { if (!isEditor) return; // return if we aren't in the editor newRuleModal( @@ -774,7 +878,8 @@ delFormatRule() { if (!isEditor) return; // return if we aren't in the editor const customRulesList = Object.keys(customRules); - if (customRulesList.length < 1) return alert("There are no Custom Rules"); + if (customRulesList.length < 1) + return alert("There Are No Custom Rules Left."); newRuleModal( Scratch.translate("Delete Rule:"), @@ -789,6 +894,9 @@ } ); } + ignoreList() { + return JSON.stringify([...ignoreList]); + } ignoreVariable(args) { if (Cast.toBoolean(args.VAR_MENU)) { @@ -805,6 +913,11 @@ console.log(ignoreList); } + resetIgnoreList() { + ignoreList = new Set(); + this.refreshBlocks; + } + checkFormatttingBlock() { if (!isEditor) return; this.formatErrors = []; From 53ef9bc7bff6551633e36acf0d10b57f5e61abfe Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:40:20 +0100 Subject: [PATCH 09/10] fixed lint --- extensions/Hammouda101010/prettyblocks.js | 36 +++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index 84f561d6b2..2f68e55d58 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -355,7 +355,7 @@ enabled: true, level: "error", msg: `"{val}" should be entirely in {isGlobal}. just as griffpatch intened it.`, - regex: "if /^[^a-z]+/ else /^[^A-Z]+/", + regex: "/special/", }, customNoCapitalized: { enabled: false, @@ -487,10 +487,11 @@ }; } // Class Utilities - refreshBlocks() { - vm.refreshWorkspace(); - vm.extensionManager.refreshBlocks(); + refreshProject() { + setTimeout(() => vm.refreshWorkspace(), 100); // Refresh workspace after a delay + vm.extensionManager.refreshBlocks(); // Refresh block list runtime.extensionStorage["HamPrettyBlocks"] = { + // refresh extension storage rules: JSON.stringify(rules), customRules: JSON.stringify(customRules), ignore: ignoreList ? JSON.stringify([...ignoreList]) : "[]", @@ -610,10 +611,13 @@ } const regex = new RegExp(str.split("/")[1], str.split("/")[2]); + regex.lastIndex = 0; switch (rule) { - case "griffpatchStyle": - if (!regex.test(val.text)) { + case "griffpatchStyle": { + // prettier-ignore + const isValidName = Cast.toBoolean(opts.isGlobal) ? val.text === val.text.toUpperCase() : val.text === val.text.toLowerCase() + if (!isValidName) { this.formatErrors.push({ type: type, level: rules[rule].level, @@ -634,8 +638,9 @@ }); } break; + } default: - if (!rules[rule].check(val)) { + if (!regex.test(val)) { this.formatErrors.push({ type: type, level: rules[rule].level, @@ -664,7 +669,7 @@ if (variable.id === targetVariable.id) workspace.renameVariableById( variable.id, - this.formatRule(rule, variable.name, opts) + this.formatRule(rule, variable.name, variable.id, opts) ); } } else { @@ -708,12 +713,13 @@ } formatRule(rule, val, valID, opts = {}) { - if (ignoreList.has(valID)) return; + if (ignoreList.has(valID)) return val; if (rules[rule].enabled) { switch (rule) { - case "griffpatchStyle": + case "griffpatchStyle": { const { isGlobal } = opts; return isGlobal ? val.toUpperCase() : val.toLowerCase(); + } case "camelCaseOnly": // prettier-ignore return val.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase()); @@ -869,7 +875,7 @@ regex: regex, }; - this.refreshBlocks(); + this.refreshProject(); console.log(customRules); } ); @@ -888,7 +894,7 @@ (name) => { console.log(name); delete customRules[name]; - this.refreshBlocks(); + this.refreshProject(); console.log(customRules); } @@ -901,21 +907,21 @@ ignoreVariable(args) { if (Cast.toBoolean(args.VAR_MENU)) { ignoreList.add(args.VAR_MENU); - this.refreshBlocks(); + this.refreshProject(); } console.log(ignoreList); } ignoreCustomBlock(args) { if (Cast.toBoolean(args.BLOCK_MENU)) { ignoreList.add(args.BLOCK_MENU); - this.refreshBlocks(); + this.refreshProject(); } console.log(ignoreList); } resetIgnoreList() { ignoreList = new Set(); - this.refreshBlocks; + this.refreshProject; } checkFormatttingBlock() { From 66c6edab0994f849f587f3a94c2e00ad28272c8e Mon Sep 17 00:00:00 2001 From: hammouda101010 <114447430+hammouda101010@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:51:28 +0100 Subject: [PATCH 10/10] removed patches :pensive: --- extensions/Hammouda101010/prettyblocks.js | 61 ----------------------- 1 file changed, 61 deletions(-) diff --git a/extensions/Hammouda101010/prettyblocks.js b/extensions/Hammouda101010/prettyblocks.js index 2f68e55d58..216c2cc4e8 100644 --- a/extensions/Hammouda101010/prettyblocks.js +++ b/extensions/Hammouda101010/prettyblocks.js @@ -269,67 +269,6 @@ } } - const squareInputBlocks = ["HamPrettyBlocks_fancyFormatErrors"]; - - // Custom Square Block Shapes - const ogConverter = runtime._convertBlockForScratchBlocks.bind(runtime); - runtime._convertBlockForScratchBlocks = function (blockInfo, categoryInfo) { - const res = ogConverter(blockInfo, categoryInfo); - if (blockInfo.outputShape) res.json.outputShape = blockInfo.outputShape; - return res; - }; - - if (Scratch.gui) - Scratch.gui.getBlockly().then((SB) => { - // Custom Square Input Shape - const makeShape = (width, height) => { - width -= 10; - // prettier-ignore - height -= 8 - return ` - m 0 4 - A 4 4 0 0 1 4 0 H ${width} - a 4 4 0 0 1 4 4 - v ${height} - a 4 4 0 0 1 -4 4 - H 4 - a 4 4 0 0 1 -4 -4 - z` - .replaceAll("\n", "") - .trim(); - }; - - const ogRender = SB.BlockSvg.prototype.render; - SB.BlockSvg.prototype.render = function (...args) { - const data = ogRender.call(this, ...args); - if (this.svgPath_ && squareInputBlocks.includes(this.type)) { - this.inputList.forEach((input) => { - if (input.name.startsWith("ARRAY")) { - const block = input.connection.targetBlock(); - if ( - block && - block.type === "text" && - block.svgPath_ && - block.type.startsWith("HamPrettyBlocks_menu_") - ) { - block.svgPath_.setAttribute( - "transform", - `scale(1, ${block.height / 33})` - ); - block.svgPath_.setAttribute( - "d", - makeShape(block.width, block.height) - ); - } else if (input.outlinePath) { - input.outlinePath.setAttribute("d", makeShape(46, 32)); - } - } - }); - } - return data; - }; - }); - let ignoreList = new Set(); // Function Types for Custom Rules.