soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
15.1 kB
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 = '<p>Debugger</p>';
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;