const BlockType = require('../../extension-support/block-type'); const ArgumentType = require('../../extension-support/argument-type'); const Cast = require('../../util/cast'); const xmlEscape = require('../../util/xml-escape'); /** * @param {string} line The line with the quote * @param {number} index The point where the quote appears */ const isEscapedQuote = (line, index) => { const quote = line.charAt(index); if (quote !== '"') return false; let lastIndex = index - 1; let escaped = false; while (line.charAt(lastIndex) === "\\") { escaped = !escaped; lastIndex -= 1; } return escaped; } const CommandDescriptions = { "help": "List all commands and how to use them.\n\tSpecify a command after to only include that explanation.", "exit": "Closes the debugger.", "start": "Restarts the project like the flag was clicked.", "stop": "Stops the project.", "pause": "Pauses the project.", "resume": "Resumes the project.", "broadcast": "Starts a broadcast by name.", "getvar": "Gets the value of a variable by name.\n\tAdd a sprite name to specify a variable in a sprite.", "setvar": "Sets the value of a variable by name.\n\tAdd a sprite name to specify a variable in a sprite.", "getlist": "Gets the value of a list by name.\n\tReturns an array.\n\tAdd a sprite name to specify a list in a sprite.", "setlist": "Sets the value of a list by name.\n\tThe list will be set to the array specified.\n\tUse a sprite name as the first parameter instead to specify a list in a sprite.", }; /** * Class for Debugging blocks * @constructor */ class jgDebuggingBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; /** * The console element. * @type {HTMLDivElement} */ this.console = document.body.appendChild(document.createElement("div")); this.console.style = 'display: none;' + 'position: absolute; left: 40px; top: 40px;' + 'resize: both; border-radius: 8px;' + 'box-shadow: 0px 0px 10px black; border: 1px solid rgba(0, 0, 0, 0.15);' + 'background: black; font-family: monospace;' + 'min-height: 3rem; min-width: 128px; width: 480px; height: 480px;' + 'overflow: hidden; z-index: 1000000;'; this.consoleHeader = this.console.appendChild(document.createElement("div")); this.consoleHeader.style = 'width: 100%; height: 2rem;' + 'position: absolute; left: 0px; top: 0px;' + 'display: flex; flex-direction: column; align-items: center;' + 'justify-content: center; color: white; cursor: move;' + 'background: #333333; z-index: 1000001; user-select: none;'; this.consoleHeader.innerHTML = '
Debugger
'; this.consoleLogs = this.console.appendChild(document.createElement("div")); this.consoleLogs.style = 'width: 100%; height: calc(100% - 3rem);' + 'position: absolute; left: 0px; top: 2rem;' + 'color: white; cursor: text; overflow: auto;' + 'background: transparent; outline: unset !important;' + 'border: 0; margin: 0; padding: 0; font-family: monospace;' + 'display: flex; flex-direction: column; align-items: flex-start;' + 'z-index: 1000005; user-select: text;'; this.consoleBar = this.console.appendChild(document.createElement("div")); this.consoleBar.style = 'width: 100%; height: 1rem;' + 'position: absolute; left: 0px; bottom: 0px;' + 'display: flex; flex-direction: row;' + 'color: white; cursor: text; background: black;' + 'z-index: 1000001; user-select: none;'; this.consoleBarInput = this.consoleBar.appendChild(document.createElement("input")); this.consoleBarInput.style = 'width: calc(100% - 16px); height: 100%;' + 'position: absolute; left: 16px; top: 0px;' + 'border: 0; padding: 0; margin: 0; font-family: monospace;' + 'color: white; cursor: text; background: black;' + 'z-index: 1000003; user-select: none; outline: unset !important;'; const consoleBarIndicator = this.consoleBar.appendChild(document.createElement("div")); consoleBarIndicator.style = 'width: 16px; height: 100%;' + 'position: absolute; left: 0px; top: 0px;' + 'color: white; cursor: text;' + 'z-index: 1000002; user-select: none;'; consoleBarIndicator.innerHTML = '>'; consoleBarIndicator.onclick = () => { this.consoleBarInput.focus(); }; // this.consoleLogs.onclick = () => { // this.consoleBarInput.focus(); // }; this.consoleBarInput.onkeydown = (e) => { if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; if (e.key.toLowerCase() !== "enter") return; const command = this.consoleBarInput.value; this.consoleBarInput.value = ""; this._addLog(`> ${command}`, "opacity: 0.7;"); let parsed = {}; try { parsed = this._parseCommand(command); } catch (err) { this._addLog(`${err}`, "color: red;"); return; } console.log(parsed); this._runCommand(parsed); }; // setup events for moving the console around let mouseDown = false; let clickDifferenceX = 0; let clickDifferenceY = 0; // let oldConsoleHeight = 480; this.consoleHeader.onmousedown = (e) => { // if (e.button === 2) { // e.preventDefault(); // let newHeight = getComputedStyle(this.consoleHeader, null).height; // if (this.console.style.height === newHeight) { // newHeight = oldConsoleHeight; // } else { // oldConsoleHeight = this.console.style.height; // } // this.console.style.height = newHeight; // return; // } if (e.button !== 0) return; mouseDown = true; e.preventDefault(); const rect = this.console.getBoundingClientRect(); clickDifferenceX = e.clientX - rect.left; clickDifferenceY = e.clientY - rect.top; }; document.addEventListener('mousemove', (e) => { if (!mouseDown) { return; } e.preventDefault(); this.console.style.left = `${e.clientX - clickDifferenceX}px`; this.console.style.top = `${e.clientY - clickDifferenceY}px`; }); document.addEventListener('mouseup', (e) => { if (!mouseDown) { return; } mouseDown = false; }); this._logs = []; this.commandSet = {}; this.commandExplanations = {}; } /** * @returns {object} metadata for this extension and its blocks. */ getInfo() { return { id: 'jgDebugging', name: 'Debugging', color1: '#878787', color2: '#757575', blocks: [ { opcode: 'openDebugger', text: 'open debugger', blockType: BlockType.COMMAND }, { opcode: 'closeDebugger', text: 'close debugger', blockType: BlockType.COMMAND }, '---', { opcode: 'log', text: 'log [INFO]', blockType: BlockType.COMMAND, arguments: { INFO: { type: ArgumentType.STRING, defaultValue: "Hello!" } } }, { opcode: 'warn', text: 'warn [INFO]', blockType: BlockType.COMMAND, arguments: { INFO: { type: ArgumentType.STRING, defaultValue: "Warning" } } }, { opcode: 'error', text: 'error [INFO]', blockType: BlockType.COMMAND, arguments: { INFO: { type: ArgumentType.STRING, defaultValue: "Error" } } }, ] }; } _addLog(log, style) { const logElement = this.consoleLogs.appendChild(document.createElement("p")); this._logs.push(log); logElement.style = 'white-space: break-spaces;'; if (style) { logElement.style = `white-space: break-spaces; ${style}`; } logElement.innerHTML = xmlEscape(log); this.consoleLogs.scrollBy(0, 1000000); } _parseCommand(command) { const rawCommand = Cast.toString(command); const data = { command: '', args: [] }; let chunk = ''; let readingCommand = true; let isInString = false; let idx = -1; // idx gets added to at the start since there a bunch of continue statemnets for (const letter of rawCommand.split('')) { idx++; if (readingCommand) { if (letter === ' ' || letter === '\t') { if (chunk.length <= 0) { throw new SyntaxError('No command before white-space'); } data.command = chunk; chunk = ''; readingCommand = false; continue; } chunk += letter; continue; } // we are reading args if (!isInString) { if (letter !== '"') { if (letter === ' ' || letter === '\t') { data.args.push(chunk); chunk = ''; continue; } chunk += letter; continue; } else { if (chunk.length > 0) { // ex: run thing"Hello!" throw new SyntaxError("Cannot prefix string argument"); } isInString = true; continue; } } // we are inside of a string if (letter === '"' && !isEscapedQuote(rawCommand, idx)) { isInString = false; data.args.push(JSON.parse(`"${chunk}"`)); chunk = ''; } else { chunk += letter; } } // reached end of the command if (isInString) throw new SyntaxError('String never terminates in command'); if (readingCommand && chunk.length > 0) { data.command = chunk; readingCommand = false; } else if (chunk.length > 0) { data.args.push(chunk); } return data; } _runCommand(parsedCommand) { if (!parsedCommand) return; if (!parsedCommand.command) return; const command = parsedCommand.command; const args = parsedCommand.args; switch (command) { case 'help': { if (args.length > 0) { const command = args[0]; let explanation = "No description defined for this command."; if (command in this.commandExplanations) { explanation = this.commandExplanations[command]; } else if (command in CommandDescriptions) { explanation = CommandDescriptions[command]; } this._addLog(`- Command: ${command}\n${explanation}`); break; } const commadnDescriptions = { ...this.commandExplanations, ...CommandDescriptions, }; let log = ""; for (const commandName in commadnDescriptions) { log += `${commandName} - ${commadnDescriptions[commandName]}\n`; } this._addLog(log); break; } case 'exit': this.closeDebugger(); break; default: if (!(command in this.commandSet)) { this._addLog(`Command "${command}" not found. Check "help" for command list.`, "color: red;"); break; } try { this.commandSet[command](...args); } catch (err) { this._addLog(`Error: ${err}`, "color: red;"); } break; } } _findBlockFromId(id, target) { if (!target) return; if (!target.blocks) return; if (!target.blocks._blocks) return; const block = target.blocks._blocks[id]; return block; } openDebugger() { this.console.style.display = ''; } closeDebugger() { this.console.style.display = 'none'; } log(args) { const text = Cast.toString(args.INFO); console.log(text); this._addLog(text); } warn(args) { const text = Cast.toString(args.INFO); console.warn(text); this._addLog(text, "color: yellow;"); } error(args, util) { // create error stack const stack = []; const target = util.target; const thread = util.thread; if (thread.stackClick) { stack.push('clicked blocks'); } const commandBlockId = thread.peekStack(); const block = this._findBlockFromId(commandBlockId, target); if (block) { stack.push(`block ${block.opcode}`); } else { stack.push(`block ${commandBlockId}`); } const eventBlock = this._findBlockFromId(thread.topBlock, target); if (eventBlock) { stack.push(`event ${eventBlock.opcode}`); } else { stack.push(`event ${thread.topBlock}`); } stack.push(`sprite ${target.sprite.name}`); const text = `Error: ${Cast.toString(args.INFO)}` + `\n${stack.map(text => (`\tat ${text}`)).join("\n")}`; console.error(text); this._addLog(text, "color: red;"); } } module.exports = jgDebuggingBlocks;