From 568f325b96e7409ae7f4b745478feb4dcf182335 Mon Sep 17 00:00:00 2001 From: Richard Lobb Date: Sat, 17 Feb 2024 21:06:33 +1300 Subject: [PATCH] Add to the Scratchpad UI the ability to direct requests directly to specified Jobe server(s) rather than going via the run-in-sandbox Moodle web service. Experimental feature. Selected Jobe servers need to be accessible from all machines that are running the relevant CodeRunner questions. An API key is strongly recommended. --- amd/build/outputdisplayarea.min.js | 2 +- amd/build/outputdisplayarea.min.js.map | 2 +- amd/build/ui_scratchpad.min.js | 2 +- amd/build/ui_scratchpad.min.js.map | 2 +- amd/src/outputdisplayarea.js | 121 ++++++++++++++++++++++++- amd/src/ui_scratchpad.js | 12 ++- amd/src/ui_scratchpad.json | 8 ++ lang/en/qtype_coderunner.php | 3 + 8 files changed, 145 insertions(+), 7 deletions(-) diff --git a/amd/build/outputdisplayarea.min.js b/amd/build/outputdisplayarea.min.js index 88586d0e2..2b66c47d3 100644 --- a/amd/build/outputdisplayarea.min.js +++ b/amd/build/outputdisplayarea.min.js @@ -23,6 +23,6 @@ define("qtype_coderunner/outputdisplayarea",["exports","core/ajax","core/str"],( * @module qtype_coderunner/outputdisplayarea * @copyright James Napier, 2023, The University of Canterbury * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const JSON_DISPLAY_PROPS=["returncode","stdout","stderr","files"],setLangString=async _ref=>{let{stringName:stringName,callback:callback,node:node}=_ref;const langString=await(async stringName=>await(0,_str.get_string)(stringName,"qtype_coderunner"))(stringName);callback instanceof Function?callback(langString):node.innerText=langString},combinedOutput=response=>response.cmpinfo+response.output+response.stderr,getImage=function(base64){let type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png";const image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image};_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode,lang,sandboxParams){this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}clearDisplay(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML="",this.textDisplay.style.backgroundColor="#eff",this.imageDisplay.style.backgroundColor="#eff"}displayText(response){this.textDisplay.innerText=combinedOutput(response)}displayHtml(response){this.textDisplay.innerHTML=combinedOutput(response);const inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}displayJson(response){const result=this.validateJson(response.output);if(null===result)return;let text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&setLangString({stringName:"error_timeout",callback:langString=>{this.textDisplay.innerText+="*** ".concat(langString," ***\n")}});const numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}displayError(error_msg){this.textDisplay.style.backgroundColor="#faa",this.textDisplay.innerText=error_msg}validateJson(jsonString){let result=null;try{result=JSON.parse(jsonString)}catch(e){return setLangString({stringName:"outputdisplayarea_invalid_json",callback:langString=>{this.displayError("".concat(langString,"\n")+"".concat(jsonString,"\n")+"".concat(e.message," \n"))}}),null}const missing=((obj,props)=>props.filter((prop=>!obj.hasOwnProperty(prop))))(result,JSON_DISPLAY_PROPS);return missing.length>0?(setLangString({stringName:"outputdisplayarea_missing_json_fields",callback:langString=>{this.displayError("".concat(langString,"\n")+"".concat(missing.join()))}}),null):result}displayNoOutput(response){const isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){const span=document.createElement("span");span.style.color="red",setLangString({stringName:"nooutput",node:span}),this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}display(response){const error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(let i=0;i{this.displayError(langString+" "+this.mode)}})):setLangString({stringName:error,callback:langString=>{this.textDisplay.innerText="*** ".concat(langString," ***\n")}})}runCode(code,stdin){let shouldClearDisplay=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{this.displayError(error.message)}}])}addInput(){const inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');const inputEl=document.getElementById(inputId);setLangString({stringName:"enter_to_submit",callback:langString=>{inputEl.placeholder+=langString}}),this.addInputEvents(inputEl)}addInputEvents(inputEl){inputEl.focus(),inputEl.addEventListener("keydown",(e=>{"Enter"===e.key&&e.preventDefault()})),inputEl.addEventListener("keyup",(e=>{if("Enter"===e.key){const line=inputEl.value;inputEl.remove(),this.textDisplay.innterHTML+=line,this.prevRunSettings[1]+=line+"\n",this.runCode(...this.prevRunSettings,!1)}}))}displayImages(files){let numImages=0;for(const[fname,fcontents]of Object.entries(files)){const fileType=fname.split(".")[1];if(fileType){const image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else setLangString({stringName:"outputdisplayarea_missing_image_extension",callback:langString=>{this.imageDisplay("".concat(langString," ")+fname)}})}return numImages}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.OutputDisplayArea=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};const JSON_DISPLAY_PROPS=["returncode","stdout","stderr","files"],setLangString=async _ref=>{let{stringName:stringName,callback:callback,node:node}=_ref;const langString=await(async stringName=>await(0,_str.get_string)(stringName,"qtype_coderunner"))(stringName);callback instanceof Function?callback(langString):node.innerText=langString},combinedOutput=response=>response.cmpinfo+response.output+response.stderr,getImage=function(base64){let type=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"png";const image=document.createElement("img");return image.src="data:image/".concat(type,";base64,").concat(base64),image};_exports.OutputDisplayArea=class{constructor(displayAreaId,outputMode,lang,sandboxParams){this.displayAreaId=displayAreaId,this.lang=lang,this.mode=outputMode,this.sandboxParams=sandboxParams,this.textDisplay=document.getElementById(displayAreaId+"-text"),this.imageDisplay=document.getElementById(displayAreaId+"-images"),this.prevRunSettings=null}clearDisplay(){this.textDisplay.innerHTML="",this.imageDisplay.innerHTML="",this.textDisplay.style.backgroundColor="#eff",this.imageDisplay.style.backgroundColor="#eff"}displayText(response){this.textDisplay.innerText=combinedOutput(response)}displayHtml(response){this.textDisplay.innerHTML=combinedOutput(response);const inputEl=this.textDisplay.querySelector(".coderunner-run-input");inputEl&&this.addInputEvents(inputEl)}displayJson(response){const result=this.validateJson(response.output);if(null===result)return;let text=result.stdout;42!==result.returncode&&(text+=result.stderr),13==result.returncode&&setLangString({stringName:"error_timeout",callback:langString=>{this.textDisplay.innerText+="*** ".concat(langString," ***\n")}});const numImages=this.displayImages(result.files);""===text.trim()&&42!==result.returncode?0==numImages&&this.displayNoOutput(null):this.textDisplay.innerText=text,42===result.returncode&&this.addInput()}displayError(error_msg){this.textDisplay.style.backgroundColor="#faa",this.textDisplay.innerText=error_msg}validateJson(jsonString){let result=null;try{result=JSON.parse(jsonString)}catch(e){return setLangString({stringName:"outputdisplayarea_invalid_json",callback:langString=>{this.displayError("".concat(langString,"\n")+"".concat(jsonString,"\n")+"".concat(e.message," \n"))}}),null}const missing=((obj,props)=>props.filter((prop=>!obj.hasOwnProperty(prop))))(result,JSON_DISPLAY_PROPS);return missing.length>0?(setLangString({stringName:"outputdisplayarea_missing_json_fields",callback:langString=>{this.displayError("".concat(langString,"\n")+"".concat(missing.join()))}}),null):result}displayNoOutput(response){const isNoOutput=!response||0===combinedOutput(response).length;if(isNoOutput||null===response){const span=document.createElement("span");span.style.color="red",setLangString({stringName:"nooutput",node:span}),this.clearDisplay(),this.textDisplay.append(span)}return isNoOutput}display(response){const error=(response=>{const ERROR_RESPONSES=[[1,0,"error_access_denied"],[2,0,"error_unknown_language"],[3,0,"error_access_denied"],[4,0,"error_submission_limit_reached"],[5,0,"error_sandbox_server_overload"],[0,11,""],[0,12,""],[0,13,"error_timeout"],[0,15,""],[0,17,"error_memory_limit"],[0,21,"error_sandbox_server_overload"],[0,30,"error_excessive_output"]];for(let i=0;i{this.displayError(langString+" "+this.mode)}})):setLangString({stringName:error,callback:langString=>{this.textDisplay.innerText="*** ".concat(langString," ***\n")}})}runCode(code,stdin){let shouldClearDisplay=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay(),_ajax.default.call([{methodname:"qtype_coderunner_run_in_sandbox",args:{contextid:M.cfg.contextid,sourcecode:code,language:this.lang,stdin:stdin,params:JSON.stringify(this.sandboxParams)},done:responseJson=>{const response=JSON.parse(responseJson);this.display(response)},fail:error=>{this.displayError(error.message)}}])}runCodeDirect(code,stdin,jobeServers,apiKeys){let shouldClearDisplay=arguments.length>4&&void 0!==arguments[4]&&arguments[4];this.prevRunSettings=[code,stdin],shouldClearDisplay&&this.clearDisplay();const lang=this.lang.toLowerCase(),runspec={run_spec:{language_id:lang,sourcecode:code,sourcefilename:"java"===lang?this.getJavaFilename(code):"__tester__.".concat(lang),input:stdin}},xhr=new XMLHttpRequest,t=this;xhr.onreadystatechange=function(){if(xhr.readyState==XMLHttpRequest.DONE)if(200===xhr.status||203===xhr.status){const sandboxResponse=t.convertToSandboxFormat(xhr.responseText);t.display(sandboxResponse)}else t.displayError("Request to sandbox server failed ".concat(xhr.status,": ").concat(xhr.statusText," ").concat(xhr.responseText))},apiKeys&&jobeServers.length!=apiKeys.length&&(alert("Misconfigured scratchpad-direct. API key list length must equal jobe server list length"),jobeServers=[jobeServers[0]],apiKeys=[apiKeys[0]]);const index=Math.floor(Math.random()*jobeServers.length);xhr.open("POST","http://".concat(jobeServers[index],"/jobe/index.php/restapi/runs"),!0),xhr.setRequestHeader("Content-type","application/json; charset=utf-8"),xhr.setRequestHeader("Accept","application/json"),apiKeys&&xhr.setRequestHeader("X-API-KEY",apiKeys[index]),xhr.send(JSON.stringify(runspec))}getJavaFilename(code){const matches=code.match(/(^|\W)public\s+class\s+(\w+)[^{]*\{.*?((public\s([a-z]*\s)*static)|(static\s([a-z]*\s)*public))\s([a-z]*\s)*void\s+main\s*\(\s*String/ms);return matches?matches[2]+".java":"NO_PUBLIC_CLASS_FOUND.java"}convertToSandboxFormat(responseText){let response="";try{response=JSON.parse(responseText)}catch(e){return{error:7,stderr:"HTTP response was ".concat(JSON.stringify(responseText))}}if(21===response.outcome)return{error:9};{const stderr=response.stderr.trim();return{error:0,stderr:stderr,result:stderr?12:response.outcome,signal:0,cmpinfo:response.cmpinfo,output:response.stdout}}}addInput(){const inputId="".concat(this.displayAreaId,"-input-field");this.textDisplay.innerHTML+='');const inputEl=document.getElementById(inputId);setLangString({stringName:"enter_to_submit",callback:langString=>{inputEl.placeholder+=langString}}),this.addInputEvents(inputEl)}addInputEvents(inputEl){inputEl.focus(),inputEl.addEventListener("keydown",(e=>{"Enter"===e.key&&e.preventDefault()})),inputEl.addEventListener("keyup",(e=>{if("Enter"===e.key){const line=inputEl.value;inputEl.remove(),this.textDisplay.innterHTML+=line,this.prevRunSettings[1]+=line+"\n",this.runCode(...this.prevRunSettings,!1)}}))}displayImages(files){let numImages=0;for(const[fname,fcontents]of Object.entries(files)){const fileType=fname.split(".")[1];if(fileType){const image=getImage(fcontents,fileType);this.imageDisplay.append(image),numImages+=1}else setLangString({stringName:"outputdisplayarea_missing_image_extension",callback:langString=>{this.imageDisplay("".concat(langString," ")+fname)}})}return numImages}}})); //# sourceMappingURL=outputdisplayarea.min.js.map \ No newline at end of file diff --git a/amd/build/outputdisplayarea.min.js.map b/amd/build/outputdisplayarea.min.js.map index 042ea406e..f3691ab27 100644 --- a/amd/build/outputdisplayarea.min.js.map +++ b/amd/build/outputdisplayarea.min.js.map @@ -1 +1 @@ -{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\nimport { get_string } from \"core/str\";\n\nconst INPUT_INTERRUPT = 42;\nconst RESULT_SUCCESS = 15;\nconst INPUT_CLASS = \"coderunner-run-input\";\nconst DEFAULT_DISPLAY_COLOUR = \"#eff\";\nconst ERROR_DISPLAY_COLOUR = \"#faa\";\nconst JSON_DISPLAY_PROPS = [\"returncode\", \"stdout\", \"stderr\", \"files\"];\n\n/**\n * Retrieve a language string from qtype_coderunner.\n * @param {string} stringName of language string to retrieve.\n * @returns {string} a language string from qtype_coderunner.\n */\nconst getLangString = async (stringName) => {\n const string = await get_string(stringName, \"qtype_coderunner\");\n return string;\n};\n\n/**\n * Get the specified language string using. If callback is provided then pass\n * the language string into that function, otherwise plug it into the given node.\n * @param {Object} settings The settings\n * @param {string} settings.stringName The language string name to retrieve.\n * @param {Function} settings.callback Callback function, with langString as arg.\n * @param {Element} settings.node text area into which the error message should be plugged.\n * @example\n * // Set a div element's text to be a language string\n * setLangString({stringName: 'nooutput', node: div})\n * @example\n * // Set a div element's text to be a language string with *** on either side\n * setLangString setLangString({stringName: 'error_timeout', callback: (langString) => {\n * div.innerText += `*** ${langString} ***\\n`;\n * }});\n */\nconst setLangString = async ({ stringName, callback, node }) => {\n const langString = await getLangString(stringName);\n if (callback instanceof Function) {\n callback(langString);\n } else {\n node.innerText = langString;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, \"error_access_denied\"], // Sandbox AUTH_ERROR\n [2, 0, \"error_unknown_language\"], // Sandbox WRONG_LANG_ID\n [3, 0, \"error_access_denied\"], // Sandbox ACCESS_DENIED\n [4, 0, \"error_submission_limit_reached\"], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, \"error_sandbox_server_overload\"], // Sandbox SERVER_OVERLOAD\n [0, 11, \"\"], // RESULT_COMPILATION_ERROR\n [0, 12, \"\"], // RESULT_RUNTIME_ERROR\n [0, 13, \"error_timeout\"], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, \"\"], // RESULT_SUCCESS\n [0, 17, \"error_memory_limit\"], // RESULT_MEMORY_LIMIT\n [0, 21, \"error_sandbox_server_overload\"], // RESULT_SERVER_OVERLOAD\n [0, 30, \"error_excessive_output\"], // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return \"error_unknown_runtime\"; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter((prop) => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {HTMLImageElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = \"png\") => {\n const image = document.createElement(\"img\");\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + \"-text\");\n this.imageDisplay = document.getElementById(displayAreaId + \"-images\");\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n this.textDisplay.style.backgroundColor = DEFAULT_DISPLAY_COLOUR;\n this.imageDisplay.style.backgroundColor = DEFAULT_DISPLAY_COLOUR;\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector(\".\" + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n if (result === null) {\n return;\n } // Invalid JSON response received from wrapper.\n\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) {\n // Timeout\n setLangString({\n stringName: \"error_timeout\",\n callback: (langString) => {\n this.textDisplay.innerText += `*** ${langString} ***\\n`;\n },\n });\n }\n\n const numImages = this.displayImages(result.files);\n if (text.trim() === \"\" && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Display an error message, with red background.\n * Typically, these would be caused by the wrapper.\n * But they can also happen when the webservice responds with an error.\n * @param {string} error_msg to be displayed.\n */\n displayError(error_msg) {\n this.textDisplay.style.backgroundColor = ERROR_DISPLAY_COLOUR;\n this.textDisplay.innerText = error_msg;\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * Return null if malformed JSON or or required fields are missing.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object | null} JSON as object, or null if invalid.\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n setLangString({\n stringName: \"outputdisplayarea_invalid_json\",\n callback: (langString) => {\n this.displayError(`${langString}\\n` + `${jsonString}\\n` + `${e.message} \\n`);\n },\n });\n return null;\n }\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n setLangString({\n stringName: \"outputdisplayarea_missing_json_fields\",\n callback: (langString) => {\n this.displayError(`${langString}\\n` + `${missing.join()}`);\n },\n });\n return null;\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement(\"span\");\n span.style.color = \"red\";\n setLangString({ stringName: \"nooutput\", node: span });\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== \"\") {\n setLangString({\n stringName: error,\n callback: (langString) => {\n this.textDisplay.innerText = `*** ${langString} ***\\n`;\n },\n });\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === \"json\") {\n this.displayJson(response);\n } else if (this.mode === \"html\") {\n this.displayHtml(response);\n } else if (this.mode === \"text\") {\n this.displayText(response);\n } else {\n setLangString({\n stringName: \"outputdisplayarea_invalid_mode\",\n callback: (langString) => {\n this.displayError(langString + \" \" + this.mode);\n },\n });\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([\n {\n methodname: \"qtype_coderunner_run_in_sandbox\",\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams), // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n this.displayError(error.message);\n },\n },\n ]);\n }\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n setLangString({\n stringName: \"enter_to_submit\",\n callback: (langString) => {\n inputEl.placeholder += langString;\n },\n });\n\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {HTMLInputElement} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener(\"keyup\", (e) => {\n if (e.key === \"Enter\") {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + \"\\n\";\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split(\".\")[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n setLangString({\n stringName: \"outputdisplayarea_missing_image_extension\",\n callback: (langString) => {\n this.imageDisplay(`${langString} ` + fname);\n },\n });\n }\n }\n return numImages;\n }\n}\n\nexport { OutputDisplayArea };\n"],"names":["JSON_DISPLAY_PROPS","setLangString","async","stringName","callback","node","langString","getLangString","Function","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","constructor","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","clearDisplay","innerHTML","style","backgroundColor","displayText","displayHtml","inputEl","this","querySelector","addInputEvents","displayJson","result","validateJson","text","stdout","returncode","numImages","displayImages","files","trim","displayNoOutput","addInput","displayError","error_msg","jsonString","JSON","parse","e","message","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","color","append","display","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","runCode","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","fail","inputId","placeholder","focus","addEventListener","key","preventDefault","line","value","remove","innterHTML","fname","fcontents","Object","entries","fileType","split"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"outputdisplayarea.min.js","sources":["../src/outputdisplayarea.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/**\n * A module used for running code using the Coderunner webservice (CRWS) and displaying output. Originally\n * developed for use in the Scratchpad UI. It has three modes of operation:\n * - 'text': Just display the output as text, html escaped.\n * - 'json': The recommended way to display programs that use stdin or output images (or both).\n * - Accepts JSON in the CRWS response output with fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * - When input from stdin is required the returncode 42 should be returned, raise this\n * any time the program asks for input. An (html) input will be added after the last stdout received.\n * When enter is pressed, runCode is called with value of the input added to the stdin string.\n * This repeats until returncode is no longer 42.\n * - 'html': Display program output as raw html inside the output area.\n * - This can be used to show images and insert other HTML tags (and beyond).\n * - Giving an tag the class 'coderunner-run-input' will add an event that\n * on pressing enter will call the runCode method again with the value of that input field added to stdin.\n * This method of receiving stdin is harder to use but more flexible than JSON, enter at your own risk.\n *\n * @module qtype_coderunner/outputdisplayarea\n * @copyright James Napier, 2023, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from \"core/ajax\";\nimport { get_string } from \"core/str\";\n\nconst INPUT_INTERRUPT = 42;\nconst INPUT_CLASS = \"coderunner-run-input\";\nconst DEFAULT_DISPLAY_COLOUR = \"#eff\";\nconst ERROR_DISPLAY_COLOUR = \"#faa\";\nconst JSON_DISPLAY_PROPS = [\"returncode\", \"stdout\", \"stderr\", \"files\"];\n\n/**\n * Error codes returned by the CodeRunner sandbox web service\n */\nconst UNKNOWN_SERVER_ERROR = 7;\nconst SERVER_OVERLOAD = 9;\n\n/**\n * RESULT status values from a direct call to a Jobe server\n */\nconst RESULT_RUNTIME_ERROR = 12;\nconst RESULT_SUCCESS = 15;\nconst RESULT_SERVER_OVERLOAD = 21;\n\n\n/**\n * Retrieve a language string from qtype_coderunner.\n * @param {string} stringName of language string to retrieve.\n * @returns {string} a language string from qtype_coderunner.\n */\nconst getLangString = async (stringName) => {\n const string = await get_string(stringName, \"qtype_coderunner\");\n return string;\n};\n\n/**\n * Get the specified language string using. If callback is provided then pass\n * the language string into that function, otherwise plug it into the given node.\n * @param {Object} settings The settings\n * @param {string} settings.stringName The language string name to retrieve.\n * @param {Function} settings.callback Callback function, with langString as arg.\n * @param {Element} settings.node text area into which the error message should be plugged.\n * @example\n * // Set a div element's text to be a language string\n * setLangString({stringName: 'nooutput', node: div})\n * @example\n * // Set a div element's text to be a language string with *** on either side\n * setLangString setLangString({stringName: 'error_timeout', callback: (langString) => {\n * div.innerText += `*** ${langString} ***\\n`;\n * }});\n */\nconst setLangString = async ({ stringName, callback, node }) => {\n const langString = await getLangString(stringName);\n if (callback instanceof Function) {\n callback(langString);\n } else {\n node.innerText = langString;\n }\n};\n\nconst diagnoseWebserviceResponse = (response) => {\n // Table of error conditions.\n // Each row is response.error, response.result, langstring\n // response.result is ignored if response.error is non-zero.\n // Any condition not in the table is deemed an \"Unknown runtime error\".\n const ERROR_RESPONSES = [\n [1, 0, \"error_access_denied\"], // Sandbox AUTH_ERROR\n [2, 0, \"error_unknown_language\"], // Sandbox WRONG_LANG_ID\n [3, 0, \"error_access_denied\"], // Sandbox ACCESS_DENIED\n [4, 0, \"error_submission_limit_reached\"], // Sandbox SUBMISSION_LIMIT_EXCEEDED\n [5, 0, \"error_sandbox_server_overload\"], // Sandbox SERVER_OVERLOAD\n [0, 11, \"\"], // RESULT_COMPILATION_ERROR\n [0, 12, \"\"], // RESULT_RUNTIME_ERROR\n [0, 13, \"error_timeout\"], // RESULT TIME_LIMIT\n [0, RESULT_SUCCESS, \"\"], // RESULT_SUCCESS\n [0, 17, \"error_memory_limit\"], // RESULT_MEMORY_LIMIT\n [0, 21, \"error_sandbox_server_overload\"], // RESULT_SERVER_OVERLOAD\n [0, 30, \"error_excessive_output\"], // RESULT OUTPUT_LIMIT\n ];\n for (let i = 0; i < ERROR_RESPONSES.length; i++) {\n let row = ERROR_RESPONSES[i];\n if (row[0] == response.error && (response.error != 0 || response.result == row[1])) {\n return row[2];\n }\n }\n return \"error_unknown_runtime\"; // We're dead, Fred.\n};\n\n/**\n * Concatenates the cmpinfo, stdout and stderr fields of the sandbox\n * response, truncating both stdout and stderr to a given maximum length\n * if necessary (in which case '... (truncated)' is appended.\n * @param {object} response Sandbox response object\n */\nconst combinedOutput = (response) => {\n return response.cmpinfo + response.output + response.stderr;\n};\n\n/**\n * Check whether obj has the properties in props, returns missing properties.\n * @param {object} obj to check properties of\n * @param {array} props to check for.\n * @returns {array} of missing properties.\n */\nconst missingProperties = (obj, props) => {\n return props.filter((prop) => !obj.hasOwnProperty(prop));\n};\n\n/**\n * Insert a base64 encoded string into HTML image.\n * @param {string} base64 encoded string.\n * @param {string} type of encoded image file.\n * @returns {HTMLImageElement} image tag containing encoded image from string.\n */\nconst getImage = (base64, type = \"png\") => {\n const image = document.createElement(\"img\");\n image.src = `data:image/${type};base64,${base64}`;\n return image;\n};\n\n/**\n * Constructor for OutputDisplayArea object. For use with the output_displayarea template.\n * @param {string} displayAreaId The id of the display area div, this should match the 'id'\n * from the template.\n * @param {string} outputMode The mode being used for output, must be text, html or json.\n * @param {string} lang The language to run code with.\n * @param {string} sandboxParams The sandbox params to run code with.\n */\nclass OutputDisplayArea {\n constructor(displayAreaId, outputMode, lang, sandboxParams) {\n this.displayAreaId = displayAreaId;\n this.lang = lang;\n this.mode = outputMode;\n this.sandboxParams = sandboxParams;\n\n this.textDisplay = document.getElementById(displayAreaId + \"-text\");\n this.imageDisplay = document.getElementById(displayAreaId + \"-images\");\n\n this.prevRunSettings = null;\n }\n\n /**\n * Clear the display of any images and text.\n */\n clearDisplay() {\n this.textDisplay.innerHTML = \"\";\n this.imageDisplay.innerHTML = \"\";\n this.textDisplay.style.backgroundColor = DEFAULT_DISPLAY_COLOUR;\n this.imageDisplay.style.backgroundColor = DEFAULT_DISPLAY_COLOUR;\n }\n\n /**\n * Display text from a CRWS response to the display (escaped).\n * @param {object} response Coderunner webservice response JSON.\n */\n displayText(response) {\n this.textDisplay.innerText = combinedOutput(response);\n }\n\n /**\n * Display HTML from a CRWS response to the display (un-escaped).\n * Find the first HTML input element with the input class and\n * add event listeners to handle reading stdin.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing HTML.\n */\n displayHtml(response) {\n this.textDisplay.innerHTML = combinedOutput(response);\n const inputEl = this.textDisplay.querySelector(\".\" + INPUT_CLASS);\n if (inputEl) {\n this.addInputEvents(inputEl);\n }\n }\n\n /**\n * Display JSON from a CRWS response to the display.\n * Assumes response.output will be a JSON with the fields:\n * - \"returncode\": Error/return code from running program.\n * - \"stdout\": Stdout text from running program.\n * - \"stderr\": Error text from running program.\n * - \"files\": An object containing filenames mapped to base64 encoded images.\n * These will be displayed below any stdout text.\n * NOTE: See file header/readme for more info.\n * @param {object} response Coderunner webservice response JSON,\n * with output field containing JSON string.\n */\n displayJson(response) {\n const result = this.validateJson(response.output);\n if (result === null) {\n return;\n } // Invalid JSON response received from wrapper.\n\n let text = result.stdout;\n\n if (result.returncode !== INPUT_INTERRUPT) {\n text += result.stderr;\n }\n if (result.returncode == 13) {\n // Timeout\n setLangString({\n stringName: \"error_timeout\",\n callback: (langString) => {\n this.textDisplay.innerText += `*** ${langString} ***\\n`;\n },\n });\n }\n\n const numImages = this.displayImages(result.files);\n if (text.trim() === \"\" && result.returncode !== INPUT_INTERRUPT) {\n if (numImages == 0) {\n this.displayNoOutput(null);\n }\n } else {\n this.textDisplay.innerText = text;\n }\n if (result.returncode === INPUT_INTERRUPT) {\n this.addInput();\n }\n }\n\n /**\n * Display an error message, with red background.\n * Typically, these would be caused by the wrapper.\n * But they can also happen when the webservice responds with an error.\n * @param {string} error_msg to be displayed.\n */\n displayError(error_msg) {\n this.textDisplay.style.backgroundColor = ERROR_DISPLAY_COLOUR;\n this.textDisplay.innerText = error_msg;\n }\n\n /**\n * Validate JSON to display, make sure it is valid json and has required fields.\n * Return null if malformed JSON or or required fields are missing.\n * @param {string} jsonString string of JSON to be displayed.\n * @returns {object | null} JSON as object, or null if invalid.\n */\n validateJson(jsonString) {\n let result = null;\n try {\n result = JSON.parse(jsonString);\n } catch (e) {\n setLangString({\n stringName: \"outputdisplayarea_invalid_json\",\n callback: (langString) => {\n this.displayError(`${langString}\\n` + `${jsonString}\\n` + `${e.message} \\n`);\n },\n });\n return null;\n }\n const missing = missingProperties(result, JSON_DISPLAY_PROPS);\n if (missing.length > 0) {\n setLangString({\n stringName: \"outputdisplayarea_missing_json_fields\",\n callback: (langString) => {\n this.displayError(`${langString}\\n` + `${missing.join()}`);\n },\n });\n return null;\n }\n return result;\n }\n\n /**\n * Display no output message if no output to display or response is null.\n * @param {object} response Coderunner webservice response JSON, set to null to force\n * display of no output message.\n */\n displayNoOutput(response) {\n const isNoOutput = response ? combinedOutput(response).length === 0 : true;\n if (isNoOutput || response === null) {\n const span = document.createElement(\"span\");\n span.style.color = \"red\";\n setLangString({ stringName: \"nooutput\", node: span });\n this.clearDisplay();\n this.textDisplay.append(span);\n }\n return isNoOutput;\n }\n /**\n * Display response using the current display mode.\n * @param {object} response Coderunner webservice response JSON.\n */\n display(response) {\n const error = diagnoseWebserviceResponse(response);\n if (error !== \"\") {\n setLangString({\n stringName: error,\n callback: (langString) => {\n this.textDisplay.innerText = `*** ${langString} ***\\n`;\n },\n });\n return;\n }\n if (this.displayNoOutput(response)) {\n return;\n }\n\n if (this.mode === \"json\") {\n this.displayJson(response);\n } else if (this.mode === \"html\") {\n this.displayHtml(response);\n } else if (this.mode === \"text\") {\n this.displayText(response);\n } else {\n setLangString({\n stringName: \"outputdisplayarea_invalid_mode\",\n callback: (langString) => {\n this.displayError(langString + \" \" + this.mode);\n },\n });\n }\n }\n\n /**\n * Run code using the Coderunner webservice and then display the output\n * using the selected mode. This function uses AJAX to asynchronously run and\n * display code.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCode(code, stdin, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n ajax.call([\n {\n methodname: \"qtype_coderunner_run_in_sandbox\",\n args: {\n contextid: M.cfg.contextid, // Moodle context ID\n sourcecode: code,\n language: this.lang,\n stdin: stdin,\n params: JSON.stringify(this.sandboxParams), // Sandbox params\n },\n done: (responseJson) => {\n const response = JSON.parse(responseJson);\n this.display(response);\n },\n fail: (error) => {\n this.displayError(error.message);\n },\n },\n ]);\n }\n\n /**\n * Run code by connecting directly with AJAX to one of the given Jobe\n * servers, selected randomly.\n * @param {string} code to be run.\n * @param {string} stdin to be fed into the program.\n * @param {list} jobeServers a non-empty list of jobe servers\n * @param {list} apiKeys a possibly empty list of API keys for the jobe-servers\n * @param {boolean} shouldClearDisplay will reset the display before displaying.\n * Use false when doing stdin runs.\n */\n runCodeDirect(code, stdin, jobeServers, apiKeys, shouldClearDisplay = false) {\n this.prevRunSettings = [code, stdin];\n if (shouldClearDisplay) {\n this.clearDisplay();\n }\n const lang = this.lang.toLowerCase();\n const runspec = {\n \"run_spec\": {\n 'language_id': lang,\n 'sourcecode': code,\n 'sourcefilename': lang === 'java' ? this.getJavaFilename(code) : `__tester__.${lang}`,\n 'input': stdin\n }\n };\n const xhr = new XMLHttpRequest();\n const t = this;\n xhr.onreadystatechange = function() {\n if (xhr.readyState == XMLHttpRequest.DONE) {\n if (xhr.status === 200 || xhr.status === 203) {\n const sandboxResponse = t.convertToSandboxFormat(xhr.responseText);\n t.display(sandboxResponse);\n } else {\n t.displayError(`Request to sandbox server failed ${xhr.status}: ${xhr.statusText} ${xhr.responseText}`);\n }\n }\n };\n\n if (apiKeys) {\n if (jobeServers.length != apiKeys.length) {\n alert(\"Misconfigured scratchpad-direct. API key list length must equal jobe server list length\");\n jobeServers = [jobeServers[0]];\n apiKeys = [apiKeys[0]];\n }\n }\n const index = Math.floor(Math.random() * jobeServers.length);\n\n xhr.open('POST', `http://${jobeServers[index]}/jobe/index.php/restapi/runs`, true);\n xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');\n xhr.setRequestHeader('Accept', 'application/json');\n if (apiKeys) {\n xhr.setRequestHeader('X-API-KEY', apiKeys[index]);\n }\n xhr.send(JSON.stringify(runspec));\n }\n\n /**\n * Try to come up with the right filename for a Java program by using regular\n * expressions to find the main class. This is by no means guaranteed to work in all cases\n * but it handles the most common ways of writing a Java program.\n * @param {string} code The java sourcecode\n * @return The main class name with '.java' appended.\n */\n getJavaFilename(code) {\n // eslint-disable-next-line max-len\n let pattern = /(^|\\W)public\\s+class\\s+(\\w+)[^{]*\\{.*?((public\\s([a-z]*\\s)*static)|(static\\s([a-z]*\\s)*public))\\s([a-z]*\\s)*void\\s+main\\s*\\(\\s*String/ms;\n const matches = code.match(pattern);\n if (!matches) {\n return 'NO_PUBLIC_CLASS_FOUND.java';\n } else {\n return matches[2] + '.java';\n }\n }\n\n /**\n * Convert the response from a direct AJAX request to a web server to roughly match the\n * object returned from a webservice request to the CodeRunner run-in-sandbox service.\n * @param {string} responseText The JSON-encoded response from Jobe\n */\n convertToSandboxFormat(responseText) {\n let response = '';\n try {\n response = JSON.parse(responseText);\n } catch (e) {\n return {\n 'error': UNKNOWN_SERVER_ERROR,\n 'stderr': `HTTP response was ${JSON.stringify(responseText)}`\n };\n }\n if (response.outcome === RESULT_SERVER_OVERLOAD) {\n return {\n 'error': SERVER_OVERLOAD\n };\n } else {\n const stderr = response.stderr.trim();\n return {\n 'error': 0,\n 'stderr': stderr,\n 'result': stderr ? RESULT_RUNTIME_ERROR : response.outcome,\n 'signal': 0,\n 'cmpinfo': response.cmpinfo,\n 'output': response.stdout\n };\n }\n }\n\n\n /**\n * Add an input field with event listeners to support running again\n * with new stdin entered by user.\n */\n addInput() {\n const inputId = `${this.displayAreaId}-input-field`;\n this.textDisplay.innerHTML += ``;\n const inputEl = document.getElementById(inputId);\n setLangString({\n stringName: \"enter_to_submit\",\n callback: (langString) => {\n inputEl.placeholder += langString;\n },\n });\n\n this.addInputEvents(inputEl);\n }\n\n /**\n * Add event listeners to inputEl overriding enter key to:\n * - Prevent form-submit.\n * - Call runCode again, adding value in inputEl to stdin.\n * @param {HTMLInputElement} inputEl to add event listeners to.\n */\n addInputEvents(inputEl) {\n inputEl.focus();\n\n inputEl.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault(); // Do NOT form submit.\n }\n });\n inputEl.addEventListener(\"keyup\", (e) => {\n if (e.key === \"Enter\") {\n const line = inputEl.value;\n inputEl.remove();\n this.textDisplay.innterHTML += line; // Perhaps this should be sanitized.\n this.prevRunSettings[1] += line + \"\\n\";\n this.runCode(...this.prevRunSettings, false);\n }\n });\n }\n\n /**\n * Take the files from a JSON response and display them.\n * @param {object} files from response, in filename: filecontents pairs.\n * @returns {number} number of images displayed.\n */\n displayImages(files) {\n let numImages = 0;\n for (const [fname, fcontents] of Object.entries(files)) {\n const fileType = fname.split(\".\")[1];\n if (fileType) {\n const image = getImage(fcontents, fileType);\n this.imageDisplay.append(image);\n numImages += 1;\n } else {\n setLangString({\n stringName: \"outputdisplayarea_missing_image_extension\",\n callback: (langString) => {\n this.imageDisplay(`${langString} ` + fname);\n },\n });\n }\n }\n return numImages;\n }\n}\n\nexport { OutputDisplayArea };\n"],"names":["JSON_DISPLAY_PROPS","setLangString","async","stringName","callback","node","langString","getLangString","Function","innerText","combinedOutput","response","cmpinfo","output","stderr","getImage","base64","type","image","document","createElement","src","constructor","displayAreaId","outputMode","lang","sandboxParams","mode","textDisplay","getElementById","imageDisplay","prevRunSettings","clearDisplay","innerHTML","style","backgroundColor","displayText","displayHtml","inputEl","this","querySelector","addInputEvents","displayJson","result","validateJson","text","stdout","returncode","numImages","displayImages","files","trim","displayNoOutput","addInput","displayError","error_msg","jsonString","JSON","parse","e","message","missing","obj","props","filter","prop","hasOwnProperty","missingProperties","length","join","isNoOutput","span","color","append","display","error","ERROR_RESPONSES","i","row","diagnoseWebserviceResponse","runCode","code","stdin","shouldClearDisplay","call","methodname","args","contextid","M","cfg","sourcecode","language","params","stringify","done","responseJson","fail","runCodeDirect","jobeServers","apiKeys","toLowerCase","runspec","getJavaFilename","xhr","XMLHttpRequest","t","onreadystatechange","readyState","DONE","status","sandboxResponse","convertToSandboxFormat","responseText","statusText","alert","index","Math","floor","random","open","setRequestHeader","send","matches","match","outcome","inputId","placeholder","focus","addEventListener","key","preventDefault","line","value","remove","innterHTML","fname","fcontents","Object","entries","fileType","split"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/ui_scratchpad.min.js b/amd/build/ui_scratchpad.min.js index 371789d61..3cd5d7da2 100644 --- a/amd/build/ui_scratchpad.min.js +++ b/amd/build/ui_scratchpad.min.js @@ -52,6 +52,6 @@ define("qtype_coderunner/ui_scratchpad",["exports","core/templates","qtype_coder * @copyright Richard Lobb, 2022, The University of Canterbury * @copyright James Napier, 2022, The University of Canterbury * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],escapeRegExp=string=>string.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null,open_delimiter:"{|",close_delimiter:"|}",escape:!1};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper();const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[this.context.answer_code.text],test_code:[this.context.test_code.text],show_hide:[this.context.show_hide.show],prefix_ans:[invertSerial(this.context.prefix_ans.checked)]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse")&&!el.classList.contains("collapsing"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)?serialisation.show_hide=["1"]:serialisation.show_hide=[""],null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad?serialisation.prefix_ans=["1"]:serialisation.prefix_ans=[""],this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=code=>this.uiParams.escape?JSON.stringify(code).slice(1,-1):code,code=function(answerCode,testCode,prefixAns,template){let open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");const escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,(()=>answerCode))).replaceAll(scratchpadRegex,(()=>testCode))}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Constructor=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};const invertSerial=current=>"1"===current[0]?[""]:["1"],escapeRegExp=string=>string.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),overwriteValues=(defaults,prescribed)=>{let overwritten={...defaults};if(prescribed)for(const[key,value]of Object.entries(defaults))overwritten[key]=prescribed[key]||value;return overwritten};_exports.Constructor=class{constructor(textAreaId,width,height,uiParams){const DEF_UI_PARAMS={scratchpad_name:"",button_name:"",prefix_name:"",help_text:"",params:{},run_lang:uiParams.lang,output_display_mode:"text",disable_scratchpad:!1,wrapper_src:null,open_delimiter:"{|",close_delimiter:"|}",escape:!1,jobe_servers:[],api_keys:[]};this.textArea=document.getElementById(textAreaId),this.textAreaId=textAreaId,this.height=height,this.readOnly=this.textArea.readonly,this.fail=!1,this.outerDiv=null,this.outputDisplay=null,this.invertPreload=uiParams.invert_prefix,this.jobeServers=uiParams.jobe_servers,this.apiKeys=uiParams.api_keys,this.lang=uiParams.lang,this.numRows=this.textArea.rows,this.uiParams=overwriteValues(DEF_UI_PARAMS,uiParams),this.runWrapper=this.getRunWrapper();const preloadString=this.textArea.value;let preload;try{preload=this.readJson(preloadString)}catch(error){return this.fail=!0,void(this.failString="scratchpad_ui_invalidserialisation")}this.updateContext(preload),this.reload()}getRunWrapper(){const wrapperSrc=this.uiParams.wrapper_src;let runWrapper=null;return wrapperSrc&&("globalextra"===wrapperSrc||"prototypeextra"===wrapperSrc?runWrapper=this.textArea.dataset[wrapperSrc]:(this.fail=!0,this.failString="scratchpad_ui_badrunwrappersrc")),runWrapper}failed(){return this.fail}failMessage(){return this.failString}sync(){if(!this.context)return;const serialisation=this.getSerialisation();this.setSerialisation(serialisation)}getSerialisation(){const prefixAns=document.getElementById(this.context.prefix_ans.id),showHide=document.getElementById(this.context.show_hide.id);let serialisation={answer_code:[this.context.answer_code.text],test_code:[this.context.test_code.text],show_hide:[this.context.show_hide.show],prefix_ans:[invertSerial(this.context.prefix_ans.checked)]};return this.answerTextarea&&(serialisation.answer_code=[this.answerTextarea.value]),this.testTextarea&&(serialisation.test_code=[this.testTextarea.value]),showHide&&!(el=>{if(!el.classList.contains("collapse")&&!el.classList.contains("collapsing"))throw Error("Element does not have collapse class");return!el.classList.contains("show")})(showHide)?serialisation.show_hide=["1"]:serialisation.show_hide=[""],null!=prefixAns&&prefixAns.checked||this.context.disable_scratchpad?serialisation.prefix_ans=["1"]:serialisation.prefix_ans=[""],this.invertPreload&&(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans)),serialisation}setSerialisation(serialisation){serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),Object.values(serialisation).some((val=>1===val.length&&val[0].length>0))?(serialisation.prefix_ans=invertSerial(serialisation.prefix_ans),this.textArea.value=JSON.stringify(serialisation)):this.textArea.value=""}getElement(){return this.outerDiv}handleRunButtonClick(){if(null===this.outputDisplay)return;this.sync();const preloadString=this.textArea.value,serial=this.readJson(preloadString),escape=code=>this.uiParams.escape?JSON.stringify(code).slice(1,-1):code,code=function(answerCode,testCode,prefixAns,template){let open=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"\\(",close=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"\\)";template||(template="".concat(open," ANSWER_CODE ").concat(close,"\n")+"".concat(open," SCRATCHPAD_CODE ").concat(close)),prefixAns||(answerCode="");const escOpen=escapeRegExp(open),escClose=escapeRegExp(close),answerRegex=new RegExp("".concat(escOpen,"\\s*ANSWER_CODE\\s*").concat(escClose),"g"),scratchpadRegex=new RegExp("".concat(escOpen,"\\s*SCRATCHPAD_CODE\\s*").concat(escClose),"g");return(template=template.replaceAll(answerRegex,(()=>answerCode))).replaceAll(scratchpadRegex,(()=>testCode))}(escape(serial.answer_code[0]),escape(serial.test_code[0]),serial.prefix_ans[0],this.runWrapper,this.uiParams.open_delimiter,this.uiParams.close_delimiter);this.jobeServers?this.outputDisplay.runCodeDirect(code,"",this.jobeServers,this.apiKeys,!0):this.outputDisplay.runCode(code,"",!0)}updateContext(preload){this.context={id:this.textAreaId,disable_scratchpad:this.uiParams.disable_scratchpad,scratchpad_name:this.uiParams.scratchpad_name,button_name:this.uiParams.button_name,help_text:{text:this.uiParams.help_text},answer_code:{id:this.textAreaId+"_answer-code",name:"answer_code",text:preload.answer_code[0],lang:this.lang,rows:this.numRows},test_code:{id:this.textAreaId+"_test-code",name:"test_code",text:preload.test_code[0],lang:this.lang,rows:6},show_hide:{id:this.textAreaId+"_scratchpad",show:preload.show_hide[0]},prefix_ans:{id:this.textAreaId+"_prefix-ans",label:this.uiParams.prefix_name,checked:preload.prefix_ans[0]},output_display:{id:this.textAreaId+"_run-output"},jquery_escape:function(){return function(text,render){return CSS.escape(render(text))}}}}readJson(preloadString){let serial;if(""!==preloadString){try{serial=JSON.parse(preloadString)}catch{serial={answer_code:[preloadString]}}if(!serial.hasOwnProperty("answer_code"))throw TypeError("JSON has wrong signature, missing answer_code field.")}return serial=overwriteValues({answer_code:[""],test_code:[""],show_hide:[""],prefix_ans:["1"]},serial),this.invertPreload&&(serial.prefix_ans=invertSerial(serial.prefix_ans)),serial}async reload(){try{const{html:html}=await _templates.default.renderForPromise("qtype_coderunner/scratchpad_ui",this.context);this.drawUi(html),this.addAceUis(),this.outputDisplay=new _outputdisplayarea.OutputDisplayArea(this.context.output_display.id,this.uiParams.output_display_mode,this.uiParams.run_lang,this.uiParams.params),this.addEventListeners()}catch(e){this.fail=!0,this.failString="scratchpad_ui_templateloadfail"}}drawUi(html){const wrapperDiv=document.getElementById(this.textAreaId).nextSibling;wrapperDiv.innerHTML=html,this.outerDiv=wrapperDiv.firstChild,wrapperDiv.style.resize="none"}addAceUis(){this.answerTextarea=document.getElementById(this.context.answer_code.id),this.testTextarea=document.getElementById(this.context.test_code.id),this.answerCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.answer_code.id),this.testTextarea&&(this.testCodeUi=(0,_userinterfacewrapper.newUiWrapper)("ace",this.context.test_code.id))}addEventListeners(){const runButton=document.getElementById(this.textAreaId+"_run-btn");runButton&&runButton.addEventListener("click",(()=>this.handleRunButtonClick()))}resize(){}hasFocus(){var _this$answerCodeUi,_this$testCodeUi;let focused=!1;return null!==(_this$answerCodeUi=this.answerCodeUi)&&void 0!==_this$answerCodeUi&&_this$answerCodeUi.uiInstance.hasFocus()&&(focused=!0),null!==(_this$testCodeUi=this.testCodeUi)&&void 0!==_this$testCodeUi&&_this$testCodeUi.uiInstance.hasFocus()&&(focused=!0),focused}destroy(){var _this$answerCodeUi2,_this$testCodeUiCodeU,_this$outerDiv;this.sync(),null===(_this$answerCodeUi2=this.answerCodeUi)||void 0===_this$answerCodeUi2||_this$answerCodeUi2.uiInstance.destroy(),null===(_this$testCodeUiCodeU=this.testCodeUiCodeUi)||void 0===_this$testCodeUiCodeU||_this$testCodeUiCodeU.uiInstance.destroy(),null===(_this$outerDiv=this.outerDiv)||void 0===_this$outerDiv||_this$outerDiv.remove(),this.outerDiv=null}}})); //# sourceMappingURL=ui_scratchpad.min.js.map \ No newline at end of file diff --git a/amd/build/ui_scratchpad.min.js.map b/amd/build/ui_scratchpad.min.js.map index 6a3f87783..d2ea1dbde 100644 --- a/amd/build/ui_scratchpad.min.js.map +++ b/amd/build/ui_scratchpad.min.js.map @@ -1 +1 @@ -{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n // Use arrow functions in replace operations to avoid special-case treatment of $.\n template = template.replaceAll(answerRegex, () => answerCode);\n template = template.replaceAll(scratchpadRegex, () => testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n // Initialise using the JSON string from the server.\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [invertSerial(this.context.prefix_ans.checked)]\n };\n // If the UI is up and running, update elements from the UI.\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n } else {\n serialisation.show_hide = [''];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n } else {\n serialisation.prefix_ans = [''];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","numRows","rows","runWrapper","getRunWrapper","preloadString","preload","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","serial","code","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCode","render","CSS","parse","hasOwnProperty","TypeError","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ui_scratchpad.min.js","sources":["../src/ui_scratchpad.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Implementation of the scratchpad_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin replaces the usual textarea answer element with a UI is designed to\n * allow the execution of code in the CodeRunner question in a manner similar to an IDE.\n * It contains two editor boxes, one on top of another, allowing users to enter and\n * edit code in both. It contains two embedded Ace UIs.\n * By default, only the top editor is visible and the bottom editor (Scratchpad Area) is hidden,\n * clicking the Scratchpad button shows it. The Scratchpad area contains a second editor,\n * a Run button and a Prefix with Answer checkbox. Additionally, there is a help button that\n * provides information about how to use the Scratchpad.\n * It's possible to run code 'in-browser' by clicking the Run Button,\n * without making a submission via the Check Button:\n * If Prefix with Answer is not checked, only the code in the Scratchpad is run --\n * allowing for a rough working spot to quickly check the result of code.\n * Otherwise, when Prefix with Answer is checked, the code in the Scratchpad is\n * appended to the code in the first editor before being run.\n * The Run Button has some limitations when using its default configuration:\n * Does not support programs that use STDIN (by default);\n * Only supports textual STDOUT (by default).\n * Note: These features can be supported, see the README section on wrappers...\n * The serialisation of this UI is a JSON object with the fields\n * with fields:\n * answer_code: [\"\"] A list containing a string with answer code from the first editor;\n * test_code: [\"\"] A list containing a string with containing answer code from the second editor;\n * show_hide: [\"1\"] when scratchpad is visible, otherwise [\"\"];\n * prefix_ans: [\"1\"] when Prefix with Answer is checked, otherwise [\"\"].\n *\n * UI Parameters:\n * - scratchpad_name: display name of the scratchpad, used to hide/un-hide the scratchpad.\n * - button_name: run button text.\n * - prefix_name: prefix with answer check-box label text.\n * - help_text: help text to show.\n * - run_lang: language used to run code when the run button is clicked,\n * this should be the language your wrapper is written in (if applicable).\n * - wrapper_src: location of wrapper code to be used by the run button, if applicable:\n * setting to globalextra will use text in global extra field,\n * - prototypeextra will use the prototype extra field.\n * - output_display_mode: control how program output is displayed on runs, there are three modes:\n * - text: display program output as text, html escaped;\n * - json: display program output, when it is json,\n * - html: display program output as raw html.\n * NOTE: see qtype_coderunner/outputdisplayarea.js for more info...\n * - disable_scratchpad: disable the scratchpad, effectively reverting to the Ace UI\n * from student perspective.\n * - invert_prefix: inverts meaning of prefix_ans serialisation -- '1' means un-ticked, vice versa.\n * This can be used to swap the default state.\n *\n * @module qtype_coderunner/ui_scratchpad\n * @copyright Richard Lobb, 2022, The University of Canterbury\n * @copyright James Napier, 2022, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\nimport Templates from 'core/templates';\n\nimport {newUiWrapper} from 'qtype_coderunner/userinterfacewrapper';\nimport {OutputDisplayArea} from 'qtype_coderunner/outputdisplayarea';\n\n\n/**\n * Invert serialisation from '1' to '', vice versa.\n * @param {string} current serialisation.\n * @returns {string} inverted serialisation.\n */\nconst invertSerial = (current) => current[0] === '1' ? [''] : ['1'];\n\n/**\n * Insert the answer code and test code into the wrapper. This may\n * be defined by the user, in UI Params or globalextra. If prefixAns is\n * false: do not include answerCode in final wrapper.\n * @param {string} answerCode text from first editor.\n * @param {string} testCode text from second editor.\n * @param {string} prefixAns '1' for true, '' for false.\n * @param {string} template provided in UI Params or globalextra.\n * @param {string} open delimiter to look for, e.g. '[['\n * @param {string} close delimiter to look for, e.g. ']]'\n * @returns {string} filled template.\n */\nconst fillWrapper = (answerCode, testCode, prefixAns, template, open = '\\\\(', close = '\\\\)') => {\n if (!template) {\n template = `${open} ANSWER_CODE ${close}\\n` +\n `${open} SCRATCHPAD_CODE ${close}`;\n }\n if (!prefixAns) {\n answerCode = '';\n }\n const escOpen = escapeRegExp(open);\n const escClose = escapeRegExp(close);\n const answerRegex = new RegExp(`${escOpen}\\\\s*ANSWER_CODE\\\\s*${escClose}`, 'g');\n const scratchpadRegex = new RegExp(`${escOpen}\\\\s*SCRATCHPAD_CODE\\\\s*${escClose}`, 'g');\n // Use arrow functions in replace operations to avoid special-case treatment of $.\n template = template.replaceAll(answerRegex, () => answerCode);\n template = template.replaceAll(scratchpadRegex, () => testCode);\n return template;\n};\n\n/**\n * Escapes a string for use in regex.\n * @param {string} string to escape.\n * @returns {string} RegEx escaped string\n */\nconst escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"); // $& means the whole matched string\n\n/**\n * Returns a new object contain default values. If a matching key exists in\n * prescribed, the corresponding value from prescribed will replace the default value.\n * Does not add keys/values to the result if that key is not in defaults.\n * @param {object} defaults object with values to be overwritten.\n * @param {object} prescribed settings, typically set by a user.\n * @returns {object} filled with default values, overwritten by their prescribed value (iff included).\n */\nconst overwriteValues = (defaults, prescribed) => {\n let overwritten = {...defaults};\n if (prescribed) {\n for (const [key, value] of Object.entries(defaults)) {\n overwritten[key] = prescribed[key] || value;\n }\n }\n return overwritten;\n};\n\n/**\n * Is a collapsed element currently collapsed?\n * @param {Element} el which is collapsed using a bootstrap collapse.\n * @returns {boolean} true if el is collapsed.\n */\nconst isCollapsed = (el) => {\n if (!(el.classList.contains('collapse') || el.classList.contains('collapsing'))) {\n throw Error('Element does not have collapse class');\n }\n return !el.classList.contains('show');\n};\n\n\n/**\n * Constructor for the ScratchpadUi object.\n * @param {string} textAreaId The ID of the html textarea.\n * @param {int} width The width in pixels of the textarea.\n * @param {int} height The height in pixels of the textarea.\n * @param {object} uiParams The UI parameter object.\n */\nclass ScratchpadUi {\n constructor(textAreaId, width, height, uiParams) {\n const DEF_UI_PARAMS = {\n scratchpad_name: '',\n button_name: '',\n prefix_name: '',\n help_text: '',\n params: {},\n run_lang: uiParams.lang, // Use answer's ace language if not specified.\n output_display_mode: 'text',\n disable_scratchpad: false,\n wrapper_src: null,\n open_delimiter: '{|',\n close_delimiter: '|}',\n escape: false,\n jobe_servers: [],\n api_keys: []\n };\n this.textArea = document.getElementById(textAreaId);\n this.textAreaId = textAreaId;\n this.height = height;\n this.readOnly = this.textArea.readonly;\n this.fail = false;\n this.outerDiv = null;\n this.outputDisplay = null;\n this.invertPreload = uiParams.invert_prefix;\n this.jobeServers = uiParams.jobe_servers;\n this.apiKeys = uiParams.api_keys;\n this.lang = uiParams.lang;\n this.numRows = this.textArea.rows;\n this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams);\n this.runWrapper = this.getRunWrapper();\n const preloadString = this.textArea.value;\n let preload;\n try {\n preload = this.readJson(preloadString);\n } catch (error) {\n this.fail = true;\n this.failString = 'scratchpad_ui_invalidserialisation';\n return;\n }\n this.updateContext(preload);\n this.reload(); // Draw my beautiful blobs.\n }\n\n getRunWrapper() {\n const wrapperSrc = this.uiParams.wrapper_src;\n let runWrapper = null;\n if (wrapperSrc) {\n if (wrapperSrc === 'globalextra' || wrapperSrc === 'prototypeextra') {\n runWrapper = this.textArea.dataset[wrapperSrc];\n } else {\n this.fail = true;\n this.failString = 'scratchpad_ui_badrunwrappersrc';\n }\n }\n return runWrapper;\n }\n\n failed() {\n return this.fail;\n }\n\n failMessage() {\n return this.failString;\n }\n\n sync() {\n if (!this.context) {\n return;\n }\n const serialisation = this.getSerialisation();\n this.setSerialisation(serialisation);\n }\n\n getSerialisation() {\n const prefixAns = document.getElementById(this.context.prefix_ans.id);\n const showHide = document.getElementById(this.context.show_hide.id);\n // Initialise using the JSON string from the server.\n let serialisation = {\n answer_code: [this.context.answer_code.text],\n test_code: [this.context.test_code.text],\n show_hide: [this.context.show_hide.show],\n prefix_ans: [invertSerial(this.context.prefix_ans.checked)]\n };\n // If the UI is up and running, update elements from the UI.\n if (this.answerTextarea) {\n serialisation.answer_code = [this.answerTextarea.value];\n }\n if (this.testTextarea) {\n serialisation.test_code = [this.testTextarea.value];\n }\n if (showHide && !isCollapsed(showHide)) {\n serialisation.show_hide = ['1'];\n } else {\n serialisation.show_hide = [''];\n }\n if (prefixAns?.checked || this.context.disable_scratchpad) {\n serialisation.prefix_ans = ['1'];\n } else {\n serialisation.prefix_ans = [''];\n }\n if (this.invertPreload) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n }\n return serialisation;\n }\n\n setSerialisation(serialisation) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n if (Object.values(serialisation).some((val) => val.length === 1 && val[0].length > 0)) {\n serialisation.prefix_ans = invertSerial(serialisation.prefix_ans);\n this.textArea.value = JSON.stringify(serialisation);\n } else {\n this.textArea.value = ''; // All fields empty...\n }\n }\n\n getElement() {\n return this.outerDiv;\n }\n\n handleRunButtonClick() {\n if (this.outputDisplay === null) {\n return;\n }\n this.sync(); // Use up-to-date serialization.\n const preloadString = this.textArea.value;\n const serial = this.readJson(preloadString);\n const escape = (code) => this.uiParams.escape ? JSON.stringify(code).slice(1, -1) : code;\n const answerCode = escape(serial.answer_code[0]);\n const testCode = escape(serial.test_code[0]);\n const code = fillWrapper(\n answerCode,\n testCode,\n serial.prefix_ans[0],\n this.runWrapper,\n this.uiParams.open_delimiter,\n this.uiParams.close_delimiter\n );\n if (this.jobeServers) {\n this.outputDisplay.runCodeDirect(code, '', this.jobeServers, this.apiKeys, true);\n } else {\n this.outputDisplay.runCode(code, '', true); // Call with no stdin.\n }\n }\n\n updateContext(preload) {\n this.context = {\n \"id\": this.textAreaId,\n \"disable_scratchpad\": this.uiParams.disable_scratchpad,\n \"scratchpad_name\": this.uiParams.scratchpad_name,\n \"button_name\": this.uiParams.button_name,\n \"help_text\": {\"text\": this.uiParams.help_text},\n \"answer_code\": {\n \"id\": this.textAreaId + '_answer-code',\n \"name\": \"answer_code\",\n \"text\": preload.answer_code[0],\n \"lang\": this.lang,\n \"rows\": this.numRows\n },\n \"test_code\": {\n \"id\": this.textAreaId + '_test-code',\n \"name\": \"test_code\",\n \"text\": preload.test_code[0],\n \"lang\": this.lang,\n \"rows\": 6\n },\n \"show_hide\": {\n \"id\": this.textAreaId + '_scratchpad',\n \"show\": preload.show_hide[0]\n },\n \"prefix_ans\": {\n \"id\": this.textAreaId + '_prefix-ans',\n \"label\": this.uiParams.prefix_name,\n \"checked\": preload.prefix_ans[0]\n },\n \"output_display\": {\n \"id\": this.textAreaId + '_run-output'\n },\n // Bootstrap collapse requires jQuery friendly ids to work...\n \"jquery_escape\": function() {\n return function(text, render) {\n return CSS.escape(render(text));\n };\n }\n };\n }\n\n readJson(preloadString) {\n const defaultSerial = {\n \"answer_code\": [''],\n \"test_code\": [''],\n \"show_hide\": [''],\n \"prefix_ans\": ['1'] // Ticked by default!\n };\n let serial;\n if (preloadString !== \"\") {\n try {\n serial = JSON.parse(preloadString);\n } catch {\n // Preload is not JSON, so use preloaded string as answer_code.\n serial = {\"answer_code\": [preloadString]};\n }\n if (!serial.hasOwnProperty(\"answer_code\")) {\n // No student_answer field... something is wrong!\n throw TypeError(\"JSON has wrong signature, missing answer_code field.\");\n }\n }\n serial = overwriteValues(defaultSerial, serial);\n\n if (this.invertPreload) {\n serial.prefix_ans = invertSerial(serial.prefix_ans);\n }\n return serial;\n }\n\n async reload() {\n try {\n const {html} = await Templates.renderForPromise('qtype_coderunner/scratchpad_ui', this.context);\n this.drawUi(html);\n this.addAceUis();\n this.outputDisplay = new OutputDisplayArea(\n this.context.output_display.id,\n this.uiParams.output_display_mode,\n this.uiParams.run_lang,\n this.uiParams.params\n );\n this.addEventListeners();\n } catch (e) {\n this.fail = true;\n this.failString = \"scratchpad_ui_templateloadfail\";\n }\n }\n\n drawUi(html) {\n const wrapperDiv = document.getElementById(this.textAreaId).nextSibling;\n wrapperDiv.innerHTML = html;\n this.outerDiv = wrapperDiv.firstChild;\n // No resizing the outer wrapper. Instead, resize the two sub UIs,\n // they will expand accordingly.\n wrapperDiv.style.resize = 'none';\n }\n\n addAceUis() {\n this.answerTextarea = document.getElementById(this.context.answer_code.id);\n this.testTextarea = document.getElementById(this.context.test_code.id);\n this.answerCodeUi = newUiWrapper('ace', this.context.answer_code.id);\n if (this.testTextarea) {\n this.testCodeUi = newUiWrapper('ace', this.context.test_code.id);\n }\n }\n\n addEventListeners() {\n const runButton = document.getElementById(this.textAreaId + '_run-btn');\n if (runButton) {\n runButton.addEventListener('click', () => this.handleRunButtonClick());\n }\n }\n\n resize() {} // Nothing to see here. Move along please.\n\n hasFocus() {\n let focused = false;\n if (this.answerCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n if (this.testCodeUi?.uiInstance.hasFocus()) {\n focused = true;\n }\n return focused;\n }\n\n destroy() {\n this.sync();\n this.answerCodeUi?.uiInstance.destroy();\n this.testCodeUiCodeUi?.uiInstance.destroy();\n this.outerDiv?.remove();\n this.outerDiv = null;\n }\n}\n\n\nexport {ScratchpadUi as Constructor};\n"],"names":["invertSerial","current","escapeRegExp","string","replace","overwriteValues","defaults","prescribed","overwritten","key","value","Object","entries","constructor","textAreaId","width","height","uiParams","DEF_UI_PARAMS","scratchpad_name","button_name","prefix_name","help_text","params","run_lang","lang","output_display_mode","disable_scratchpad","wrapper_src","open_delimiter","close_delimiter","escape","jobe_servers","api_keys","textArea","document","getElementById","readOnly","this","readonly","fail","outerDiv","outputDisplay","invertPreload","invert_prefix","jobeServers","apiKeys","numRows","rows","runWrapper","getRunWrapper","preloadString","preload","readJson","error","failString","updateContext","reload","wrapperSrc","dataset","failed","failMessage","sync","context","serialisation","getSerialisation","setSerialisation","prefixAns","prefix_ans","id","showHide","show_hide","answer_code","text","test_code","show","checked","answerTextarea","testTextarea","el","classList","contains","Error","isCollapsed","values","some","val","length","JSON","stringify","getElement","handleRunButtonClick","serial","code","slice","answerCode","testCode","template","open","close","escOpen","escClose","answerRegex","RegExp","scratchpadRegex","replaceAll","fillWrapper","runCodeDirect","runCode","render","CSS","parse","hasOwnProperty","TypeError","html","Templates","renderForPromise","drawUi","addAceUis","OutputDisplayArea","output_display","addEventListeners","e","wrapperDiv","nextSibling","innerHTML","firstChild","style","resize","answerCodeUi","testCodeUi","runButton","addEventListener","hasFocus","focused","_this$answerCodeUi","uiInstance","_this$testCodeUi","destroy","testCodeUiCodeUi","remove"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/outputdisplayarea.js b/amd/src/outputdisplayarea.js index 89fbb1b79..c7233bb4a 100644 --- a/amd/src/outputdisplayarea.js +++ b/amd/src/outputdisplayarea.js @@ -42,12 +42,25 @@ import ajax from "core/ajax"; import { get_string } from "core/str"; const INPUT_INTERRUPT = 42; -const RESULT_SUCCESS = 15; const INPUT_CLASS = "coderunner-run-input"; const DEFAULT_DISPLAY_COLOUR = "#eff"; const ERROR_DISPLAY_COLOUR = "#faa"; const JSON_DISPLAY_PROPS = ["returncode", "stdout", "stderr", "files"]; +/** + * Error codes returned by the CodeRunner sandbox web service + */ +const UNKNOWN_SERVER_ERROR = 7; +const SERVER_OVERLOAD = 9; + +/** + * RESULT status values from a direct call to a Jobe server + */ +const RESULT_RUNTIME_ERROR = 12; +const RESULT_SUCCESS = 15; +const RESULT_SERVER_OVERLOAD = 21; + + /** * Retrieve a language string from qtype_coderunner. * @param {string} stringName of language string to retrieve. @@ -372,6 +385,112 @@ class OutputDisplayArea { ]); } + /** + * Run code by connecting directly with AJAX to one of the given Jobe + * servers, selected randomly. + * @param {string} code to be run. + * @param {string} stdin to be fed into the program. + * @param {list} jobeServers a non-empty list of jobe servers + * @param {list} apiKeys a possibly empty list of API keys for the jobe-servers + * @param {boolean} shouldClearDisplay will reset the display before displaying. + * Use false when doing stdin runs. + */ + runCodeDirect(code, stdin, jobeServers, apiKeys, shouldClearDisplay = false) { + this.prevRunSettings = [code, stdin]; + if (shouldClearDisplay) { + this.clearDisplay(); + } + const lang = this.lang.toLowerCase(); + const runspec = { + "run_spec": { + 'language_id': lang, + 'sourcecode': code, + 'sourcefilename': lang === 'java' ? this.getJavaFilename(code) : `__tester__.${lang}`, + 'input': stdin + } + }; + const xhr = new XMLHttpRequest(); + const t = this; + xhr.onreadystatechange = function() { + if (xhr.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200 || xhr.status === 203) { + const sandboxResponse = t.convertToSandboxFormat(xhr.responseText); + t.display(sandboxResponse); + } else { + t.displayError(`Request to sandbox server failed ${xhr.status}: ${xhr.statusText} ${xhr.responseText}`); + } + } + }; + + if (apiKeys) { + if (jobeServers.length != apiKeys.length) { + alert("Misconfigured scratchpad-direct. API key list length must equal jobe server list length"); + jobeServers = [jobeServers[0]]; + apiKeys = [apiKeys[0]]; + } + } + const index = Math.floor(Math.random() * jobeServers.length); + + xhr.open('POST', `http://${jobeServers[index]}/jobe/index.php/restapi/runs`, true); + xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); + xhr.setRequestHeader('Accept', 'application/json'); + if (apiKeys) { + xhr.setRequestHeader('X-API-KEY', apiKeys[index]); + } + xhr.send(JSON.stringify(runspec)); + } + + /** + * Try to come up with the right filename for a Java program by using regular + * expressions to find the main class. This is by no means guaranteed to work in all cases + * but it handles the most common ways of writing a Java program. + * @param {string} code The java sourcecode + * @return The main class name with '.java' appended. + */ + getJavaFilename(code) { + // eslint-disable-next-line max-len + let pattern = /(^|\W)public\s+class\s+(\w+)[^{]*\{.*?((public\s([a-z]*\s)*static)|(static\s([a-z]*\s)*public))\s([a-z]*\s)*void\s+main\s*\(\s*String/ms; + const matches = code.match(pattern); + if (!matches) { + return 'NO_PUBLIC_CLASS_FOUND.java'; + } else { + return matches[2] + '.java'; + } + } + + /** + * Convert the response from a direct AJAX request to a web server to roughly match the + * object returned from a webservice request to the CodeRunner run-in-sandbox service. + * @param {string} responseText The JSON-encoded response from Jobe + */ + convertToSandboxFormat(responseText) { + let response = ''; + try { + response = JSON.parse(responseText); + } catch (e) { + return { + 'error': UNKNOWN_SERVER_ERROR, + 'stderr': `HTTP response was ${JSON.stringify(responseText)}` + }; + } + if (response.outcome === RESULT_SERVER_OVERLOAD) { + return { + 'error': SERVER_OVERLOAD + }; + } else { + const stderr = response.stderr.trim(); + return { + 'error': 0, + 'stderr': stderr, + 'result': stderr ? RESULT_RUNTIME_ERROR : response.outcome, + 'signal': 0, + 'cmpinfo': response.cmpinfo, + 'output': response.stdout + }; + } + } + + /** * Add an input field with event listeners to support running again * with new stdin entered by user. diff --git a/amd/src/ui_scratchpad.js b/amd/src/ui_scratchpad.js index 41fa7a699..05677da7d 100644 --- a/amd/src/ui_scratchpad.js +++ b/amd/src/ui_scratchpad.js @@ -171,7 +171,9 @@ class ScratchpadUi { wrapper_src: null, open_delimiter: '{|', close_delimiter: '|}', - escape: false + escape: false, + jobe_servers: [], + api_keys: [] }; this.textArea = document.getElementById(textAreaId); this.textAreaId = textAreaId; @@ -181,6 +183,8 @@ class ScratchpadUi { this.outerDiv = null; this.outputDisplay = null; this.invertPreload = uiParams.invert_prefix; + this.jobeServers = uiParams.jobe_servers; + this.apiKeys = uiParams.api_keys; this.lang = uiParams.lang; this.numRows = this.textArea.rows; this.uiParams = overwriteValues(DEF_UI_PARAMS, uiParams); @@ -293,7 +297,11 @@ class ScratchpadUi { this.uiParams.open_delimiter, this.uiParams.close_delimiter ); - this.outputDisplay.runCode(code, '', true); // Call with no stdin. + if (this.jobeServers) { + this.outputDisplay.runCodeDirect(code, '', this.jobeServers, this.apiKeys, true); + } else { + this.outputDisplay.runCode(code, '', true); // Call with no stdin. + } } updateContext(preload) { diff --git a/amd/src/ui_scratchpad.json b/amd/src/ui_scratchpad.json index 050b7a30a..6807b609f 100644 --- a/amd/src/ui_scratchpad.json +++ b/amd/src/ui_scratchpad.json @@ -52,6 +52,14 @@ "escape": { "type": "boolean", "default": false + }, + "jobe_servers": { + "type": "string[]", + "default": "" + }, + "api_keys": { + "type": "string[]", + "default": "" } } } diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php index c24f38422..c877781c1 100644 --- a/lang/en/qtype_coderunner.php +++ b/lang/en/qtype_coderunner.php @@ -1071,6 +1071,9 @@ class definition is followed by a public __Tester__  class that $string['scratchpadui_disable_scratchpad_descr'] = 'Disable the scratchpad, effectively revert back to Ace UI from student perspective.'; $string['scratchpadui_invert_prefix_descr'] = 'Inverts meaning of prefix_ans serialisation: \'1\' means un-ticked -- and vice versa. This can be used to swap the default state.'; $string['scratchpadui_escape_descr'] = 'Escape (JSON with " removed from start and end) ANSWER_CODE and SCRATCHPAD_CODE before insertion into wrapper. Useful when inserting code into a string. NOTE: single quotes \' are NOT escaped.'; +$string['scratchpadui_jobe_servers_descr'] = 'A list of Jobe servers, one of which will be randomly selected as a direct target for AJAX requests, rather than going via Moodle. EXPERIMENTAL and potentially insecure.'; +$string['scratchpadui_api_keys_descr'] = 'A list of API keys to use with the jobe_servers (if given). If empty, no API key is used. EXPERIMENTAL and potentially insecure.'; + // SCRATCHPAD UI Errors. $string['scratchpad_ui_badrunwrappersrc'] = 'Invalid run wrapper source given, please contact question author.'; $string['scratchpad_ui_invalidserialisation'] = 'Invalid JSON serialisation provided, must include \"answer_code\" field.';