diff --git a/index.html b/index.html index f52ac615b8..d53ae26010 100644 --- a/index.html +++ b/index.html @@ -52,7 +52,13 @@ - + + + diff --git a/js/js-export/interface.js b/js/js-export/interface.js index a1fed72050..ad3caa5733 100644 --- a/js/js-export/interface.js +++ b/js/js-export/interface.js @@ -634,8 +634,1179 @@ class JSInterface { return finalArgs; } + + _methodArgConstraints = { + // Rhythm blocks + playNote: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + playNoteMillis: [ + { + type: "number", + constraints: { + min: 0, + max: 100000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + dot: [ + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + tie: [ + { + type: "function", + constraints: { + async: true + } + } + ], + multiplyNoteValue: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + swing: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + // Meter blocks + setMeter: [ + { + type: "number", + constraints: { + min: 1, + max: 16, + integer: true + } + }, + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + } + ], + PICKUP: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + } + ], + setBPM: [ + { + type: "number", + constraints: { + min: 40, + max: 208, + integer: true + } + }, + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + } + ], + setMasterBPM: [ + { + type: "number", + constraints: { + min: 40, + max: 208, + integer: true + } + }, + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + } + ], + onEveryNoteDo: [ + { + type: "string", + constraints: { + type: "any" + } + } + ], + onEveryBeatDo: [ + { + type: "string", + constraints: { + type: "any" + } + } + ], + onStrongBeatDo: [ + { + type: "number", + constraints: { + min: 1, + max: 16, + integer: true + } + }, + { + type: "string", + constraints: { + type: "any" + } + } + ], + onWeakBeatDo: [ + { + type: "string", + constraints: { + type: "any" + } + } + ], + setNoClock: [ + { + type: "function", + constraints: { + async: true + } + } + ], + getNotesPlayed: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + } + ], + // Pitch blocks + playPitch: [ + { + type: "string", + constraints: { + type: "solfegeorletter" + } + }, + { + type: "number", + constraints: { + min: 1, + max: 8, + integer: true + } + } + ], + stepPitch: [ + { + type: "number", + constraints: { + min: -7, + max: 7, + integer: true + } + } + ], + playNthModalPitch: [ + { + type: "number", + constraints: { + min: -7, + max: 7, + integer: true + } + }, + { + type: "number", + constraints: { + min: 1, + max: 8, + integer: true + } + } + ], + playPitchNumber: [ + { + type: "number", + constraints: { + min: -3, + max: 12, + integer: true + } + } + ], + playHertz: [ + { + type: "number", + constraints: { + min: 20, + max: 20000, + integer: false + } + } + ], + setAccidental: [ + { + type: "string", + constraints: { + type: "accidental" + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setScalarTranspose: [ + { + type: "number", + constraints: { + min: -10, + max: 10, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setSemitoneTranspose: [ + { + type: "number", + constraints: { + min: -10, + max: 10, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setRegister: [ + { + type: "number", + constraints: { + min: -3, + max: 3, + integer: true + } + } + ], + invert: [ + { + type: "string", + constraints: { + type: "solfegeorletter" + } + }, + { + type: "number", + constraints: { + min: 1, + max: 8, + integer: true + } + }, + { + type: "string", + constraints: { + type: "oneof", + values: ["even", "odd", "scalar"], + defaultIndex: 0 + } + } + ], + setPitchNumberOffset: [ + { + type: "string", + constraints: { + type: "solfegeorletter" + } + }, + { + type: "number", + constraints: { + min: 1, + max: 8, + integer: true + } + } + ], + numToPitch: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + numToOctave: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + // Intervals blocks + setKey: [ + { + type: "string", + constraints: { + type: "letterkey" + } + }, + { + type: "string", + constraints: { + type: "oneof", + values: [ + "major", + "ionian", + "dorian", + "phrygian", + "lydian", + "myxolydian", + "minor", + "aeolian" + ], + defaultIndex: 0 + } + } + ], + MOVABLEDO: [ + { + type: "boolean" + } + ], + setScalarInterval: [ + { + type: "number", + constraints: { + min: -7, + max: 7, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setSemitoneInterval: [ + { + type: "number", + constraints: { + min: -12, + max: 12, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setTemperament: [ + { + type: "string", + constraints: { + type: "oneof", + values: [ + "equal", + "just intonation", + "Pythagorean", + "1/3 comma meantone", + "1/4 comma meantone" + ], + defaultIndex: 0 + } + }, + { + type: "string", + constraints: { + type: "solfegeorletter" + } + }, + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: true + } + } + ], + // Tone blocks + setInstrument: [ + { + type: "string", + constraints: { + type: "synth" + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doVibrato: [ + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doChorus: [ + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 10, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doPhaser: [ + { + type: "number", + constraints: { + min: 0, + max: 20, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 3, + integer: true + } + }, + { + type: "number", + constraints: { + min: 20, + max: 20000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doTremolo: [ + { + type: "number", + constraints: { + min: 0, + max: 20, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doDistortion: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doHarmonic: [ + { + type: "number", + constraints: { + min: 0, + max: 11, + integer: true + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + // Ornament blocks + setStaccato: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setSlur: [ + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doNeighbor: [ + { + type: "number", + constraints: { + min: -7, + max: 7, + integer: true + } + }, + { + type: "number", + constraints: { + min: 0, + max: 1000, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + // Volume blocks + doCrescendo: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + doDecrescendo: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + PANNING: [ + { + type: "number", + constraints: { + min: -100, + max: 100, + integer: true + } + } + ], + MASTERVOLUME: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setRelativeVolume: [ + { + type: "number", + constraints: { + min: -50, + max: 50, + integer: false + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + setSynthVolume: [ + { + type: "string", + constraints: { + type: "synth" + } + }, + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: false + } + } + ], + getSynthVolume: [ + { + type: "string", + constraints: { + type: "synth" + } + } + ], + // Drum blocks + playDrum: [ + { + type: "string", + constraints: { + type: "drum" + } + } + ], + setDrum: [ + { + type: "string", + constraints: { + type: "drum" + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + mapPitchToDrum: [ + { + type: "string", + constraints: { + type: "drum" + } + }, + { + type: "function", + constraints: { + async: true + } + } + ], + playNoise: [ + { + type: "string", + constraints: { + type: "noise" + } + } + ], + // Graphics blocks + goForward: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + goBackward: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + turnRight: [ + { + type: "number", + constraints: { + min: -360, + max: 360, + integer: false + } + } + ], + turnLeft: [ + { + type: "number", + constraints: { + min: -360, + max: 360, + integer: false + } + } + ], + setXY: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + }, + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + setHeading: [ + { + type: "number", + constraints: { + min: -360, + max: 360, + integer: false + } + } + ], + drawArc: [ + { + type: "number", + constraints: { + min: -360, + max: 360, + integer: false + } + }, + { + type: "number", + constraints: { + min: 0, + max: 100000, + integer: false + } + } + ], + drawBezier: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + }, + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + setBezierControlPoint1: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + }, + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + // setBezierControlPoint1: [ + // { + // type: "number", + // constraints: { + // min: -100000, + // max: 100000, + // integer: false + // } + // }, + // { + // type: "number", + // constraints: { + // min: -100000, + // max: 100000, + // integer: false + // } + // } + // ], + scrollXY: [ + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + }, + { + type: "number", + constraints: { + min: -100000, + max: 100000, + integer: false + } + } + ], + // Pen blocks + setColor: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setGrey: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setShade: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setHue: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setTranslucency: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + setPensize: [ + { + type: "number", + constraints: { + min: 0, + max: 100, + integer: true + } + } + ], + // Dictionary blocks + setValue: [ + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "number", + constraints: { + min: -1000, + max: 1000, + integer: true + } + } + ], + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "number", + constraints: { + min: -1000, + max: 1000, + integer: true + } + } + ], + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "undefined" + } + ] + ], + getValue: [ + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "number", + constraints: { + min: -1000, + max: 1000, + integer: true + } + } + ], + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "undefined" + } + ] + ], + getDict: [ + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "undefined" + } + ] + ], + showDict: [ + [ + { + type: "string", + constraints: { + type: "any" + } + }, + { + type: "undefined" + } + ] + ] + } } if (typeof module !== "undefined" && module.exports) { module.exports = JSInterface; } - \ No newline at end of file diff --git a/js/widgets/jseditor.js b/js/widgets/jseditor.js index 9ecc9ac7f2..aa62099d65 100644 --- a/js/widgets/jseditor.js +++ b/js/widgets/jseditor.js @@ -65,11 +65,188 @@ class JSEditor { return link; }); this._styles[this._currentStyle].removeAttribute("disabled"); + this._addErrorStyles(); this._setup(); this._setLinesCount(this._code); } + /** + * Adds CSS styles for error highlighting + * @returns {void} + */ + _addErrorStyles() { + if (document.getElementById("js-error-styles")) { + return; + } + + const style = document.createElement("style"); + style.id = "js-error-styles"; + style.textContent = ` + .error { + background-color: #ff4444 !important; + color: white !important; + border-radius: 2px; + padding: 1px 2px; + position: relative; + } + + .hljs-keyword { + color: #007acc !important; + font-weight: bold; + } + + .hljs-built_in { + color: #00d4aa !important; + } + + .hljs-title.function_ { + color: #ffcc00 !important; + } + + .hljs-number { + color: #4ec9b0 !important; + } + + .hljs-string { + color: #ff8c00 !important; + } + + .hljs-subst { + color: #ff8c00 !important; + background-color: rgba(255, 140, 0, 0.1) !important; + } + + .hljs-comment { + color: #57a64a !important; + font-style: italic; + } + + .hljs-title.class_ { + color: #c586c0 !important; + } + + .hljs-variable { + color: #4fc1ff !important; + } + + .hljs-params { + color: #4fc1ff !important; + } + + .hljs-property { + color: #ff79c6 !important; + } + + .hljs-literal { + color: #007acc !important; + } + + .hljs-type { + color: #00d4aa !important; + } + + .hljs-operator { + color: #ffffff !important; + } + + .hljs-punctuation { + color: #cccccc !important; + } + + .hljs-regexp { + color: #ff5555 !important; + } + + .hljs-symbol { + color: #ffcc00 !important; + } + `; + document.head.appendChild(style); + } + + /** + * Highlights syntax errors in the editor + * @param {HTMLElement} editor - the editor element + * @returns {void} + */ + _highlightErrors(editor) { + const existingErrors = editor.querySelectorAll(".error"); + existingErrors.forEach(el => { + const text = el.textContent; + el.replaceWith(text); + }); + + try { + const code = editor.textContent; + acorn.parse(code, { ecmaVersion: 2020 }); + } catch (error) { + if (error.pos !== undefined) { + this._markErrorAtPosition(editor, error.pos, error.message); + } + + JSEditor.logConsole(`Syntax Error at position ${error.pos}: ${error.message}`); + } + } + + /** + * Marks an error at a specific position in the editor + * @param {HTMLElement} editor - the editor element + * @param {Number} position - the character position of the error + * @param {String} message - the error message + * @returns {void} + */ + _markErrorAtPosition(editor, position, message) { + const text = editor.textContent; + + let errorStart = position; + let errorEnd = position; + + while (errorStart > 0) { + const char = text.charAt(errorStart - 1); + if (char === " " || char === "\n" || char === "\t" || char === ";" || char === "{" || char === "}" || char === "(" || char === ")" || char === ",") { + break; + } + errorStart--; + } + + while (errorEnd < text.length) { + const char = text.charAt(errorEnd); + if (char === " " || char === "\n" || char === "\t" || char === ";" || char === "{" || char === "}" || char === "(" || char === ")" || char === ",") { + break; + } + errorEnd++; + } + + if (errorStart === errorEnd) { + errorEnd = Math.min(errorStart + 1, text.length); + } + + this._markErrorSpan(editor, errorStart, errorEnd, message); + } + + /** + * Marks an error span in the editor with a simple approach + * @param {HTMLElement} editor - the editor element + * @param {Number} start - the start position of the error + * @param {Number} end - the end position of the error + * @param {String} message - the error message + * @returns {void} + */ + _markErrorSpan(editor, start, end, message) { + const text = editor.textContent; + const errorText = text.substring(start, end); + + const beforeError = text.substring(0, start); + const afterError = text.substring(end); + + const highlightedHTML = beforeError + + `${errorText}` + + afterError; + + editor.innerHTML = highlightedHTML; + } + /** * Renders the editor and all the subcomponents in the DOM. * Sets up CodeJar. @@ -277,7 +454,7 @@ class JSEditor { const codebox = document.createElement("div"); codebox.classList.add("editor"); - codebox.classList.add("language-js"); + codebox.classList.add("language-javascript"); codebox.style.width = "100%"; codebox.style.height = "100%"; codebox.style.position = "absolute"; @@ -352,8 +529,16 @@ class JSEditor { this._editor.appendChild(editorconsole); const highlight = (editor) => { - // editor.textContent = editor.textContent; - hljs.highlightBlock(editor); + // Configure highlight.js for JavaScript + hljs.configure({ + languages: ["javascript"] + }); + + // Apply highlight.js syntax highlighting for JavaScript + hljs.highlightElement(editor); + + // Add error highlighting + this._highlightErrors(editor); }; this._jar = new CodeJar(codebox, highlight); @@ -478,11 +663,22 @@ class JSEditor { JSEditor.clearConsole(); + try { + acorn.parse(this._code, { ecmaVersion: 2020 }); + } catch (e) { + JSEditor.logConsole(`Syntax Error: ${e.message}`, "red"); + return; + } + try { MusicBlocks.init(true); new Function(this._code)(); + JSEditor.logConsole("Code executed successfully!", "green"); } catch (e) { - JSEditor.logConsole(e, "maroon"); + JSEditor.logConsole(`Runtime Error: ${e.message}`, "maroon"); + if (e.stack) { + JSEditor.logConsole(`Stack trace: ${e.stack}`, "maroon"); + } } }