From 5ececb389b51ec99778a557c0fa7ac8ebc0f3fb6 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 10 May 2020 15:05:36 +0300 Subject: [PATCH 01/10] support streaming output --- package.json | 1 + src/executable-code/executable-fragment.js | 93 ++++++++++++--- src/executable-code/executable-fragment.monk | 23 +--- src/js-executor/index.js | 4 +- src/utils.js | 19 +-- src/view/output-view.js | 80 +++++++------ src/webdemo-api.js | 119 ++++++++++++++----- yarn.lock | 5 + 8 files changed, 220 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index b3d3714d..a02ccdc0 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "github-markdown-css": "^3.0.1", "html-webpack-plugin": "^3.2.0", "is-empty-object": "^1.1.1", + "jsonpipe": "2.2.0", "markdown-it": "^8.4.2", "markdown-it-highlightjs": "^3.0.0", "monkberry": "4.0.8", diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index b52e71ab..a7446676 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -1,9 +1,9 @@ -import merge from 'deepmerge'; import CodeMirror from 'codemirror'; import Monkberry from 'monkberry'; import directives from 'monkberry-directives'; import 'monkberry-events'; import ExecutableCodeTemplate from './executable-fragment.monk'; +import Exception from './exception'; import WebDemoApi from '../webdemo-api'; import TargetPlatform from "../target-platform"; import JsExecutor from "../js-executor" @@ -51,9 +51,12 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { code: '', foldButtonHover: false, folded: true, + exception: null, output: null, + errors: [] }; instance.codemirror = new CodeMirror(); + instance.element = element instance.on('click', SELECTORS.FOLD_BUTTON, () => { instance.update({folded: !instance.state.folded}); @@ -123,16 +126,20 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } } - this.state = merge.all([this.state, state, { - isShouldBeFolded: this.isShouldBeFolded && state.isFoldedButton - }]); - + if (state.output === null) { + this.removeAllOutputNodes() + } + this.applyStateUpdate(state) super.update(this.state); + this.renderNewOutputNodes(state) + if (!this.initialized) { this.initializeCodeMirror(state); this.initialized = true; } else { - this.showDiagnostics(state.errors); + if (state.errors !== undefined) { // rerender errors if the array was explicitly changed + this.showDiagnostics(this.state.errors); + } if (state.folded === undefined) { return } @@ -181,6 +188,51 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } } + removeAllOutputNodes() { + const outputNode = this.element.getElementsByClassName("code-output").item(0) + while(outputNode && outputNode.lastChild) { + outputNode.removeChild(outputNode.lastChild) + } + } + + applyStateUpdate(stateUpdate) { + if (stateUpdate.errors === null) { + stateUpdate.errors = [] + } else if (stateUpdate.errors !== undefined) { + this.state.errors.push(...stateUpdate.errors) + stateUpdate.errors = this.state.errors + } + + Object.keys(stateUpdate).forEach(key =>{ + if (stateUpdate[key] === undefined) { + delete stateUpdate[key]; + } + }) + Object.assign(this.state, stateUpdate, { + isShouldBeFolded: this.isShouldBeFolded && stateUpdate.isFoldedButton + }) + } + + renderNewOutputNodes(stateUpdate) { + if (stateUpdate.output) { + const template = document.createElement('template'); + template.innerHTML = stateUpdate.output.trim(); + const node = template.content.firstChild; + this.element.getElementsByClassName("code-output").item(0).appendChild(node) + } + + if (stateUpdate.exception) { + const outputNode = this.element.getElementsByClassName("code-output").item(0) + const exceptionView = Monkberry.render(Exception, outputNode, { + 'directives': directives + }) + exceptionView.update({ + ...stateUpdate.exception, + onExceptionClick: this.onExceptionClick.bind(this) + }) + } + } + markPlaceHolders() { let taskRanges = this.getTaskRanges(); this.codemirror.setValue(this.codemirror.getValue() @@ -230,7 +282,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { // creates a new iframe and removes the old one, thereby stops execution of any running script if (targetPlatform === TargetPlatform.CANVAS || targetPlatform === TargetPlatform.JS) this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); - this.update({output: "", openConsole: false, exception: null}); + this.update({output: null, openConsole: false, exception: null}); if (onCloseConsole) onCloseConsole(); } @@ -248,31 +300,38 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { return } this.update({ + errors: null, + output: null, + exception: null, waitingForOutput: true, openConsole: false }); if (onOpenConsole) onOpenConsole(); //open when waitingForOutput=true if (onRun) onRun(); if (targetPlatform === TargetPlatform.JAVA || targetPlatform === TargetPlatform.JUNIT) { - WebDemoApi.executeKotlinCode( + WebDemoApi.executeKotlinCodeTEMP( this.getCode(), compilerVersion, targetPlatform, args, theme, hiddenDependencies, onTestPassed, - onTestFailed).then( + onTestFailed, state => { - state.waitingForOutput = false; - if (state.output || state.exception) { + if (state.waitingForOutput) { + this.update(state) + return + } + // all chunks were processed + + if (this.state.output || this.state.exception) { // previous chunk contained some text state.openConsole = true; - } else { - if (onCloseConsole) onCloseConsole(); + } else if (onCloseConsole) { + onCloseConsole() } - if ((state.errors.length > 0 || state.exception) && onError) onError(); + if ((this.state.errors.length > 0 || this.state.exception) && onError) onError(); this.update(state); - }, - () => this.update({waitingForOutput: false}) + } ) } else { this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); @@ -357,7 +416,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { return; } diagnostics.forEach(diagnostic => { - const interval = diagnostic.interval; + const interval = Object.assign({}, diagnostic.interval); interval.start = this.recalculatePosition(interval.start); interval.end = this.recalculatePosition(interval.end); diff --git a/src/executable-code/executable-fragment.monk b/src/executable-code/executable-fragment.monk index e23c71a4..39ae8aff 100644 --- a/src/executable-code/executable-fragment.monk +++ b/src/executable-code/executable-fragment.monk @@ -1,5 +1,3 @@ -{% import Exception from './exception' %} -
{% if (!highlightOnly) %} @@ -18,26 +16,15 @@ {% if (openConsole) %}
{% endif %} + {% if output || exception %} +
+
+
+ {% endif %} {% if (waitingForOutput) %}
- {% else %} - {% if (output && output != "") || exception %} -
-
- {% unsafe output %} - - {% if exception %} - - {% endif %} -
-
- {% endif %} {% endif %}
diff --git a/src/js-executor/index.js b/src/js-executor/index.js index 06638f1a..1f78285c 100644 --- a/src/js-executor/index.js +++ b/src/js-executor/index.js @@ -2,7 +2,7 @@ import './index.scss' import {API_URLS} from "../config"; import TargetPlatform from "../target-platform"; import {showJsException} from "../view/output-view"; -import {processingHtmlBrackets} from "../utils"; +import {escapeBrackets} from "../utils"; const INIT_SCRIPT = "if(kotlin.BufferedOutput!==undefined){kotlin.out = new kotlin.BufferedOutput()}" + "else{kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput()}"; @@ -37,7 +37,7 @@ export default class JsExecutor { if (loadedScripts === jsLibs.size + 2) { try { const output = this.iframe.contentWindow.eval(jsCode); - return output ? `${processingHtmlBrackets(output)}` : ""; + return output ? `${escapeBrackets(output)}` : ""; } catch (e) { if (onError) onError(); let exceptionOutput = showJsException(e); diff --git a/src/utils.js b/src/utils.js index d35fbe4f..82ba9eab 100644 --- a/src/utils.js +++ b/src/utils.js @@ -102,7 +102,7 @@ export function insertAfter(newNode, referenceNode) { * @param string * @returns {*} */ -export function processingHtmlBrackets(string) { +export function escapeBrackets(string) { const tagsToReplace = { "<": "<", ">": ">" @@ -133,23 +133,6 @@ export function unEscapeString(string) { return unEscapedString } -/** - * convert all `<` and `>` to `<` and `>` - * @param string - * @returns {*} - */ -export function convertToHtmlTag(string) { - const tagsToReplace = { - "<": "&lt;", - ">": "&gt;", - }; - let unEscapedString = string; - Object.keys(tagsToReplace).forEach(function (key) { - unEscapedString = unEscapedString.replace(new RegExp(tagsToReplace[key], 'g'), key) - }); - return unEscapedString -} - /** * Getting count of lines * @param string diff --git a/src/view/output-view.js b/src/view/output-view.js index c3b85959..3501f1f6 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -1,4 +1,4 @@ -import {arrayFrom, convertToHtmlTag, processingHtmlBrackets} from "../utils"; +import {arrayFrom, convertToHtmlTag, escapeBrackets} from "../utils"; import isEmptyObject from "is-empty-object" import escapeHtml from "escape-html" @@ -16,53 +16,61 @@ const TEST_STATUS = { PASSED : { value: "OK", text: "Passed" } }; -const BUG_FLAG = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}BUG${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`; -const BUG_REPORT_MESSAGE = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}Hey! It seems you just found a bug! \uD83D\uDC1E\n` + +const BUG_FLAG_TEMP = 'BUG' +const BUG_REPORT_MESSAGE_TEMP = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + `Please click here to submit it ` + `to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + - `✅ Don't forget to attach code to the issue${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}\n`; + `✅ Don't forget to attach code to the issue\n`; -export function processJVMOutput(output, theme) { - let processedOutput = processingHtmlBrackets(output); // don't need to escape `&` - return processedOutput - .split(BUG_FLAG).join(BUG_REPORT_MESSAGE) - .split(`${ANGLE_BRACKETS_LEFT_HTML}outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) - .split(`${ANGLE_BRACKETS_LEFT_HTML}/outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("") - .split(`${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) - .split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(""); +export function processJVMStdout(output, theme) { + const processedOutput = escapeBrackets(output); + return `${processedOutput}` } -export function processJUnitResults(data, onTestPassed, onTestFailed) { - let result = ""; - let totalTime = 0; - let passed = true; - if (isEmptyObject(data)) return NO_TEST_FOUND; - for (let testClass in data) { - let listOfResults = arrayFrom(data[testClass]); - result += listOfResults.reduce((previousTest, currentTest) => { - totalTime = totalTime + (currentTest.executionTime / 1000); - if (currentTest.status === TEST_STATUS.ERROR.value || currentTest.status === TEST_STATUS.FAIL.value) passed = false; - switch (currentTest.status) { - case TEST_STATUS.FAIL.value: - return previousTest + buildOutputTestLine(TEST_STATUS.FAIL.text, currentTest.methodName, currentTest.comparisonFailure.message); - case TEST_STATUS.ERROR.value: - return previousTest + buildOutputTestLine(TEST_STATUS.ERROR.text, currentTest.methodName, currentTest.exception.message); - case TEST_STATUS.PASSED.value: - return previousTest + buildOutputTestLine(TEST_STATUS.PASSED.text, currentTest.methodName, ""); - } - }, ""); +export function processJVMStderr(output, theme) { + if (output === BUG_FLAG_TEMP) { + output = BUG_REPORT_MESSAGE_TEMP + } + const processedOutput = escapeBrackets(output); + return `${processedOutput}` +} + +export function processJUnitTotalResults(testResults, onTestPassed, onTestFailed) { + if (testResults.testsRun === 0) { + return NO_TEST_FOUND + } + if (testResults.success) { + if (onTestPassed) onTestPassed() + } else { + if (onTestFailed) onTestFailed() + } + return `
Total test time: ${testResults.totalTime}s
` +} + +export function processJUnitTestResult(testRunInfo, testResults) { + let output = ""; + testResults.testsRun++ + testResults.totalTime += testRunInfo.executionTime / 1000 + switch (testRunInfo.status) { + case TEST_STATUS.FAIL.value: + testResults.success = false; + output = buildOutputTestLine(TEST_STATUS.FAIL.text, testRunInfo.methodName, testRunInfo.comparisonFailure.message); + break; + case TEST_STATUS.ERROR.value: + testResults.success = false; + output = buildOutputTestLine(TEST_STATUS.ERROR.text, testRunInfo.methodName, testRunInfo.exception.message); + break; + case TEST_STATUS.PASSED.value: + output = buildOutputTestLine(TEST_STATUS.PASSED.text, testRunInfo.methodName, ""); } - if (passed && onTestPassed) onTestPassed(); - if (!passed && onTestFailed) onTestFailed(); - let testTime = `
Total test time: ${totalTime}s
`; - return testTime + result; + return output; } function buildOutputTestLine(status, method, message) { return `
-
${status}: ${method}${message ? ': ' + convertToHtmlTag(message) : ''}
+
${status}: ${method}${message ? ': ' + escapeBrackets(message) : ''}
`; } diff --git a/src/webdemo-api.js b/src/webdemo-api.js index 7eb5a03d..882a6a73 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -3,12 +3,15 @@ import URLSearchParams from 'url-search-params'; import TargetPlatform from "./target-platform"; import {API_URLS} from "./config"; import flatten from 'flatten' +import jsonpipe from 'jsonpipe' import { findSecurityException, getExceptionCauses, processErrors, - processJUnitResults, - processJVMOutput + processJUnitTestResult, + processJUnitTotalResults, + processJVMStdout, + processJVMStderr } from "./view/output-view"; /** @@ -68,44 +71,68 @@ export default class WebDemoApi { /** * Request on execute Kotlin code. * - * @param code - string - * @param compilerVersion - string kotlin compiler - * @param platform - TargetPlatform - * @param args - command line arguments - * @param theme - theme of editor - * @param onTestPassed - function will call after test's passed - * @param onTestFailed - function will call after test's failed + * @param code - string + * @param compilerVersion - string kotlin compiler + * @param platform - TargetPlatform + * @param args - command line arguments + * @param theme - editor theme + * @param onTestPassed - a function that will be called if all tests pass + * @param onTestFailed - a function that will be called if some tests fail (but after all tests are executed) * @param hiddenDependencies - read only additional files - * @returns {*|PromiseLike|Promise} + * @param callback - a callback for output chunks */ - static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed) { - return executeCode(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies).then(function (data) { - let output = ""; - let errorsAndWarnings = flatten(Object.values(data.errors)); - let errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + static executeKotlinCodeTEMP(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + const testResults = { + testsRun: 0, + totalTime: 0, + success: true + } + return executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { + if (result.done) { + if (platform === TargetPlatform.JUNIT) { + const output = processJUnitTotalResults(testResults, onTestPassed, onTestFailed) + callback({ + waitingForOutput: true, + output: output + }) + } + callback({ + waitingForOutput: false + }); + return + } + const data = result.data + + let errorsAndWarnings + let errors = [] + if (data.hasOwnProperty('errors')) { + errorsAndWarnings = flatten(Object.values(data.errors)); + errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + } + + let output; if (errors.length > 0) { output = processErrors(errors, theme); - } else { - switch (platform) { - case TargetPlatform.JAVA: - if (data.text) output = processJVMOutput(data.text, theme); - break; - case TargetPlatform.JUNIT: - data.testResults ? output = processJUnitResults(data.testResults, onTestPassed, onTestFailed) : output = processJVMOutput(data.text, theme); - break; - } + } else if (data.hasOwnProperty('errStream')) { + output = processJVMStderr(data.errStream, theme) + } else if (data.hasOwnProperty('outStream')) { + output = processJVMStdout(data.outStream) + } else if (data.hasOwnProperty('testResult') && platform === TargetPlatform.JUNIT) { + output = processJUnitTestResult(data.testResult, testResults) } - let exceptions = null; - if (data.exception != null) { - exceptions = findSecurityException(data.exception); - exceptions.causes = getExceptionCauses(exceptions); - exceptions.cause = undefined; + + let exception; + if (data.hasOwnProperty('exception')) { + exception = findSecurityException(data.exception); + exception.causes = getExceptionCauses(exception); + exception.cause = undefined; } - return { + callback({ + waitingForOutput: true, errors: errorsAndWarnings, output: output, - exception: exceptions - } + exception: exception + }) }) } @@ -141,7 +168,7 @@ export default class WebDemoApi { } } -function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { +function createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { const files = [buildFileObject(code, DEFAULT_FILE_NAME)] .concat(hiddenDependencies.map((file, index) => buildFileObject(file, `hiddenDependency${index}.kt`))); const projectJson = JSON.stringify({ @@ -164,6 +191,12 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep body.set(option, options[option]) } } + return body +} + +function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { + const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies, options) + return fetch(url + targetPlatform.id, { method: 'POST', body: body.toString(), @@ -173,6 +206,26 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep }).then(response => response.json()) } +function executeCodeStreaming(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, callback) { + const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies) + + jsonpipe.flow(url + targetPlatform.id, { + success: data => { + callback({'data': data}) + }, + complete: () => { + callback({'done': true}) + }, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Enable-Streaming': 'true' + }, + data: body.toString(), + withCredentials: false + }); +} + /** * * Build file object. diff --git a/yarn.lock b/yarn.lock index 2f58577e..2aaa60de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3683,6 +3683,11 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonpipe@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jsonpipe/-/jsonpipe-2.2.0.tgz#70a1344b9f9111d8ff4a7c23a95f22c90d87d214" + integrity sha512-K+vnwaebP0Zu61Q6Dmdw6QLrHQVTVun7PbiUSqQmdcyOx00KmHCJnY+x1oWxAtmOrGTOkW5TcpGcJ5NV8ugCFQ== + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" From c1f269d9586ff8d1fd6aeccaa2b228a5fea6f5b3 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 10 May 2020 15:26:09 +0300 Subject: [PATCH 02/10] merge nodes of same type --- src/executable-code/executable-fragment.js | 18 ++++++++++++------ src/view/output-view.js | 2 -- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index a7446676..fcad097d 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -190,7 +190,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { removeAllOutputNodes() { const outputNode = this.element.getElementsByClassName("code-output").item(0) - while(outputNode && outputNode.lastChild) { + while (outputNode && outputNode.lastChild) { outputNode.removeChild(outputNode.lastChild) } } @@ -203,7 +203,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { stateUpdate.errors = this.state.errors } - Object.keys(stateUpdate).forEach(key =>{ + Object.keys(stateUpdate).forEach(key => { if (stateUpdate[key] === undefined) { delete stateUpdate[key]; } @@ -215,10 +215,16 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { renderNewOutputNodes(stateUpdate) { if (stateUpdate.output) { - const template = document.createElement('template'); + const template = document.createElement("template"); template.innerHTML = stateUpdate.output.trim(); - const node = template.content.firstChild; - this.element.getElementsByClassName("code-output").item(0).appendChild(node) + const newNode = template.content.firstChild; + const parent = this.element.getElementsByClassName("code-output").item(0) + const isMergeable = newNode.className.startsWith("standard-output") || newNode.className.startsWith("error-output") + if (isMergeable && parent.lastChild !== null && parent.lastChild.className === newNode.className) { + parent.lastChild.textContent += newNode.textContent + } else { + parent.appendChild(newNode) + } } if (stateUpdate.exception) { @@ -278,7 +284,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } onConsoleCloseButtonEnter() { - const {jsLibs, onCloseConsole, targetPlatform } = this.state; + const {jsLibs, onCloseConsole, targetPlatform} = this.state; // creates a new iframe and removes the old one, thereby stops execution of any running script if (targetPlatform === TargetPlatform.CANVAS || targetPlatform === TargetPlatform.JS) this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); diff --git a/src/view/output-view.js b/src/view/output-view.js index 3501f1f6..26c16bb0 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -7,8 +7,6 @@ const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; const NO_TEST_FOUND = "No tests methods are found"; -const ANGLE_BRACKETS_LEFT_HTML = "<"; -const ANGLE_BRACKETS_RIGHT_HTML = ">"; const TEST_STATUS = { FAIL : { value: "FAIL", text: "Fail" }, From 27d890a832adf8d857c970ab33b1d1d5e65606a6 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 10 May 2020 15:30:25 +0300 Subject: [PATCH 03/10] rename api methods --- src/executable-code/executable-fragment.js | 2 +- src/view/output-view.js | 8 ++++---- src/webdemo-api.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index fcad097d..11deb338 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -315,7 +315,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { if (onOpenConsole) onOpenConsole(); //open when waitingForOutput=true if (onRun) onRun(); if (targetPlatform === TargetPlatform.JAVA || targetPlatform === TargetPlatform.JUNIT) { - WebDemoApi.executeKotlinCodeTEMP( + WebDemoApi.executeKotlinCode( this.getCode(), compilerVersion, targetPlatform, args, diff --git a/src/view/output-view.js b/src/view/output-view.js index 26c16bb0..42d2f75e 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -14,8 +14,8 @@ const TEST_STATUS = { PASSED : { value: "OK", text: "Passed" } }; -const BUG_FLAG_TEMP = 'BUG' -const BUG_REPORT_MESSAGE_TEMP = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + +const BUG_FLAG = 'BUG' +const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + `Please click
here to submit it ` + `to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + `✅ Don't forget to attach code to the issue\n`; @@ -26,8 +26,8 @@ export function processJVMStdout(output, theme) { } export function processJVMStderr(output, theme) { - if (output === BUG_FLAG_TEMP) { - output = BUG_REPORT_MESSAGE_TEMP + if (output === BUG_FLAG) { + output = BUG_REPORT_MESSAGE } const processedOutput = escapeBrackets(output); return `${processedOutput}` diff --git a/src/webdemo-api.js b/src/webdemo-api.js index 882a6a73..c5ff29e7 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -81,7 +81,7 @@ export default class WebDemoApi { * @param hiddenDependencies - read only additional files * @param callback - a callback for output chunks */ - static executeKotlinCodeTEMP(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { const testResults = { testsRun: 0, totalTime: 0, From 6e07cb1fc07f3dfc800ce4ac1945ee716675a18c Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 10 May 2020 16:09:10 +0300 Subject: [PATCH 04/10] fix bug report message --- src/view/output-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/output-view.js b/src/view/output-view.js index 42d2f75e..2b3cb604 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -16,7 +16,7 @@ const TEST_STATUS = { const BUG_FLAG = 'BUG' const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + - `Please click here to submit it ` + + `Please go here -> http://kotl.in/issue to submit it ` + `to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + `✅ Don't forget to attach code to the issue\n`; From 0e44ec04ec3ef50f9853a92d63442f16767ec014 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sat, 23 May 2020 22:24:49 +0300 Subject: [PATCH 05/10] refactor code and print error when request fails --- src/executable-code/executable-fragment.js | 56 +++++++++++++--------- src/view/output-view.js | 14 ++++-- src/webdemo-api.js | 41 +++++++++------- 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index 11deb338..3cce436b 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -21,6 +21,9 @@ const KEY_CODES = { F9: 120 }; const DEBOUNCE_TIME = 500; +const CODE_OUTPUT_CLASS_NAME = "code-output" +const STANDARD_OUTPUT_CLASS_NAME = "standard-output" +const ERROR_OUTPUT_CLASS_NAME = "error-output" const SELECTORS = { JS_CODE_OUTPUT_EXECUTOR: ".js-code-output-executor", @@ -189,27 +192,30 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } removeAllOutputNodes() { - const outputNode = this.element.getElementsByClassName("code-output").item(0) - while (outputNode && outputNode.lastChild) { - outputNode.removeChild(outputNode.lastChild) + const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) + if (outputNode) { + outputNode.innerHTML = "" } } applyStateUpdate(stateUpdate) { - if (stateUpdate.errors === null) { - stateUpdate.errors = [] - } else if (stateUpdate.errors !== undefined) { - this.state.errors.push(...stateUpdate.errors) - stateUpdate.errors = this.state.errors + const filteredStateUpdate = Object.keys(stateUpdate) + .reduce((result, key) => { + if (stateUpdate[key] !== undefined) { + result[key] = stateUpdate[key] + } + return result + }, {}) + + if (filteredStateUpdate.errors === null) { + filteredStateUpdate.errors = [] + } else if (filteredStateUpdate.errors !== undefined) { + this.state.errors.push(...filteredStateUpdate.errors) + filteredStateUpdate.errors = this.state.errors } - Object.keys(stateUpdate).forEach(key => { - if (stateUpdate[key] === undefined) { - delete stateUpdate[key]; - } - }) - Object.assign(this.state, stateUpdate, { - isShouldBeFolded: this.isShouldBeFolded && stateUpdate.isFoldedButton + Object.assign(this.state, filteredStateUpdate, { + isShouldBeFolded: this.isShouldBeFolded && filteredStateUpdate.isFoldedButton }) } @@ -218,9 +224,11 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { const template = document.createElement("template"); template.innerHTML = stateUpdate.output.trim(); const newNode = template.content.firstChild; - const parent = this.element.getElementsByClassName("code-output").item(0) - const isMergeable = newNode.className.startsWith("standard-output") || newNode.className.startsWith("error-output") - if (isMergeable && parent.lastChild !== null && parent.lastChild.className === newNode.className) { + const parent = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) + const isMergeable = newNode.className.startsWith(STANDARD_OUTPUT_CLASS_NAME) || + newNode.className.startsWith(ERROR_OUTPUT_CLASS_NAME) + + if (isMergeable && parent.lastChild && parent.lastChild.className === newNode.className) { parent.lastChild.textContent += newNode.textContent } else { parent.appendChild(newNode) @@ -228,7 +236,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } if (stateUpdate.exception) { - const outputNode = this.element.getElementsByClassName("code-output").item(0) + const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) const exceptionView = Monkberry.render(Exception, outputNode, { 'directives': directives }) @@ -328,14 +336,18 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { this.update(state) return } - // all chunks were processed + // no more chunks will be received + + const hasOutput = state.output || this.state.output + const hasException = state.exception || this.state.exception + const hasErrors = this.state.errors.length > 0 || (state.errors && state.errors.length > 0) - if (this.state.output || this.state.exception) { // previous chunk contained some text + if (hasOutput || hasException) { state.openConsole = true; } else if (onCloseConsole) { onCloseConsole() } - if ((this.state.errors.length > 0 || this.state.exception) && onError) onError(); + if ((hasErrors || hasException) && onError) onError(); this.update(state); } ) diff --git a/src/view/output-view.js b/src/view/output-view.js index 2b3cb604..f5e48004 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -6,7 +6,7 @@ import escapeHtml from "escape-html" const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; -const NO_TEST_FOUND = "No tests methods are found"; +const NO_TEST_FOUND = "No test methods were found"; const TEST_STATUS = { FAIL : { value: "FAIL", text: "Fail" }, @@ -25,17 +25,21 @@ export function processJVMStdout(output, theme) { return `${processedOutput}` } +export function createErrorText(output, theme) { + const processedOutput = escapeBrackets(output); + return `${processedOutput}` +} + export function processJVMStderr(output, theme) { if (output === BUG_FLAG) { output = BUG_REPORT_MESSAGE } - const processedOutput = escapeBrackets(output); - return `${processedOutput}` + return createErrorText(output, theme) } -export function processJUnitTotalResults(testResults, onTestPassed, onTestFailed) { +export function processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) { if (testResults.testsRun === 0) { - return NO_TEST_FOUND + return createErrorText(NO_TEST_FOUND, theme) } if (testResults.success) { if (onTestPassed) onTestPassed() diff --git a/src/webdemo-api.js b/src/webdemo-api.js index c5ff29e7..6cec2cbe 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -11,7 +11,7 @@ import { processJUnitTestResult, processJUnitTotalResults, processJVMStdout, - processJVMStderr + processJVMStderr, createErrorText } from "./view/output-view"; /** @@ -87,20 +87,18 @@ export default class WebDemoApi { totalTime: 0, success: true } - return executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { - if (result.done) { - if (platform === TargetPlatform.JUNIT) { - const output = processJUnitTotalResults(testResults, onTestPassed, onTestFailed) - callback({ - waitingForOutput: true, - output: output - }) - } - callback({ - waitingForOutput: false - }); - return + executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { + let output; + if (result.errorText) { + output = createErrorText(result.errorText, theme) + } else if (platform === TargetPlatform.JUNIT) { + output = processJUnitTotalResults(testResults, onTestPassed, onTestFailed) } + callback({ + waitingForOutput: false, + output: output + }) + }, result => { const data = result.data let errorsAndWarnings @@ -206,15 +204,22 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep }).then(response => response.json()) } -function executeCodeStreaming(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, callback) { +function executeCodeStreaming(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, onDone, onData) { const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies) - jsonpipe.flow(url + targetPlatform.id, { + let xmlHttpRequest; + xmlHttpRequest = jsonpipe.flow(url + targetPlatform.id, { success: data => { - callback({'data': data}) + onData({'data': data}) }, complete: () => { - callback({'done': true}) + if (xmlHttpRequest && xmlHttpRequest.status === 0) { + onDone({errorText: "REQUEST CANCELLED"}) + } else if (xmlHttpRequest && (xmlHttpRequest.status < 200 || xmlHttpRequest.status > 299)) { + onDone({errorText: `SERVER RETURNED CODE ${xmlHttpRequest.status}`}) + } else { + onDone({}) + } }, method: 'POST', headers: { From 0fb6cba00e69a27aa6cbc11d2f7cb2fce9dae843 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 24 May 2020 01:40:07 +0300 Subject: [PATCH 06/10] return synchronous requests back --- src/config.js | 7 + src/executable-code/executable-fragment.js | 33 ++-- src/utils.js | 17 +++ src/view/output-view.js | 37 +++-- src/webdemo-api.js | 166 ++++++++++++++------- webpack.config.js | 2 + 6 files changed, 193 insertions(+), 69 deletions(-) diff --git a/src/config.js b/src/config.js index 67ee07c5..0014199f 100644 --- a/src/config.js +++ b/src/config.js @@ -11,9 +11,16 @@ export const RUNTIME_CONFIG = {...getConfigFromElement(currentScript)}; */ export const API_URLS = { server: RUNTIME_CONFIG.server || __WEBDEMO_URL__, + asyncServer: RUNTIME_CONFIG.asyncServer || __ASYNC_SERVER_URL__, get COMPILE() { return `${this.server}/kotlinServer?type=run&runConf=`; }, + get COMPILE_ASYNC() { + if (this.asyncServer) { + return `${this.asyncServer}/kotlinServer?type=run&runConf=` + } + return null; + }, get HIGHLIGHT() { return `${this.server}/kotlinServer?type=highlight&runConf=`; }, diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index 3cce436b..23d50a3e 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -219,19 +219,34 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { }) } + appendOneNodeToDOM(parent, newNode) { // used for streaming + const isMergeable = newNode.className.startsWith(STANDARD_OUTPUT_CLASS_NAME) || + newNode.className.startsWith(ERROR_OUTPUT_CLASS_NAME) + + if (isMergeable && parent.lastChild && parent.lastChild.className === newNode.className) { + parent.lastChild.textContent += newNode.textContent + } else { + parent.appendChild(newNode) + } + } + + appendMultipleNodesToDOM(parent, newNodes) { // used for synchronous batch output update + const documentFragment = document.createDocumentFragment() + while (newNodes.item(0)) { + documentFragment.append(newNodes.item(0)) + } + parent.appendChild(documentFragment) + } + renderNewOutputNodes(stateUpdate) { if (stateUpdate.output) { + const parent = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) const template = document.createElement("template"); template.innerHTML = stateUpdate.output.trim(); - const newNode = template.content.firstChild; - const parent = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) - const isMergeable = newNode.className.startsWith(STANDARD_OUTPUT_CLASS_NAME) || - newNode.className.startsWith(ERROR_OUTPUT_CLASS_NAME) - - if (isMergeable && parent.lastChild && parent.lastChild.className === newNode.className) { - parent.lastChild.textContent += newNode.textContent - } else { - parent.appendChild(newNode) + if (template.content.childElementCount !== 1) { // synchronous mode + this.appendMultipleNodesToDOM(parent, template.content.childNodes) + } else { // streaming + this.appendOneNodeToDOM(parent, template.content.firstChild) } } diff --git a/src/utils.js b/src/utils.js index 82ba9eab..52d43746 100644 --- a/src/utils.js +++ b/src/utils.js @@ -133,6 +133,23 @@ export function unEscapeString(string) { return unEscapedString } +/** + * convert all `&lt;` and `&gt;` to `<` and `>` + * @param string + * @returns {*} + */ +export function convertToHtmlTag(string) { + const tagsToReplace = { + "<": "&lt;", + ">": "&gt;", + }; + let unEscapedString = string; + Object.keys(tagsToReplace).forEach(function (key) { + unEscapedString = unEscapedString.replace(new RegExp(tagsToReplace[key], 'g'), key) + }); + return unEscapedString +} + /** * Getting count of lines * @param string diff --git a/src/view/output-view.js b/src/view/output-view.js index f5e48004..92582fdb 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -1,4 +1,4 @@ -import {arrayFrom, convertToHtmlTag, escapeBrackets} from "../utils"; +import {arrayFrom, convertToHtmlTag, escapeBrackets, processingHtmlBrackets} from "../utils"; import isEmptyObject from "is-empty-object" import escapeHtml from "escape-html" @@ -7,6 +7,8 @@ const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; const NO_TEST_FOUND = "No test methods were found"; +const ANGLE_BRACKETS_LEFT_HTML = "<"; +const ANGLE_BRACKETS_RIGHT_HTML = ">"; const TEST_STATUS = { FAIL : { value: "FAIL", text: "Fail" }, @@ -21,12 +23,12 @@ const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' `✅ Don't forget to attach code to the issue\n`; export function processJVMStdout(output, theme) { - const processedOutput = escapeBrackets(output); + const processedOutput = escapeHtml(output); return `${processedOutput}` } export function createErrorText(output, theme) { - const processedOutput = escapeBrackets(output); + const processedOutput = escapeHtml(output); return `${processedOutput}` } @@ -37,6 +39,16 @@ export function processJVMStderr(output, theme) { return createErrorText(output, theme) } +export function processBatchJVMOutput(output, theme) { + let processedOutput = escapeBrackets(output); // don't need to escape `&` + return processedOutput + .split(BUG_FLAG).join(BUG_REPORT_MESSAGE) + .split(`${ANGLE_BRACKETS_LEFT_HTML}outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) + .split(`${ANGLE_BRACKETS_LEFT_HTML}/outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("") + .split(`${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) + .split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(""); +} + export function processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) { if (testResults.testsRun === 0) { return createErrorText(NO_TEST_FOUND, theme) @@ -49,30 +61,37 @@ export function processJUnitTotalResults(testResults, theme, onTestPassed, onTes return `
Total test time: ${testResults.totalTime}s
` } -export function processJUnitTestResult(testRunInfo, testResults) { +export function processJUnitTestResult(testRunInfo, testResults, needToEscape) { let output = ""; testResults.testsRun++ testResults.totalTime += testRunInfo.executionTime / 1000 switch (testRunInfo.status) { case TEST_STATUS.FAIL.value: testResults.success = false; - output = buildOutputTestLine(TEST_STATUS.FAIL.text, testRunInfo.methodName, testRunInfo.comparisonFailure.message); + output = buildOutputTestLine(TEST_STATUS.FAIL.text, testRunInfo.methodName, testRunInfo.comparisonFailure.message, needToEscape); break; case TEST_STATUS.ERROR.value: testResults.success = false; - output = buildOutputTestLine(TEST_STATUS.ERROR.text, testRunInfo.methodName, testRunInfo.exception.message); + output = buildOutputTestLine(TEST_STATUS.ERROR.text, testRunInfo.methodName, testRunInfo.exception.message, needToEscape); break; case TEST_STATUS.PASSED.value: - output = buildOutputTestLine(TEST_STATUS.PASSED.text, testRunInfo.methodName, ""); + output = buildOutputTestLine(TEST_STATUS.PASSED.text, testRunInfo.methodName, "", needToEscape); } return output; } -function buildOutputTestLine(status, method, message) { +function buildOutputTestLine(status, method, message, needToEscape) { + let escapedMessage; + if (needToEscape) { + escapedMessage = escapeHtml(message) + } else { + escapedMessage = convertToHtmlTag(message) // synchronous mode escapes some text on the server side + } + return `
-
${status}: ${method}${message ? ': ' + escapeBrackets(message) : ''}
+
${status}: ${method}${message ? ': ' + escapedMessage : ''}
`; } diff --git a/src/webdemo-api.js b/src/webdemo-api.js index 6cec2cbe..38417716 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -11,8 +11,11 @@ import { processJUnitTestResult, processJUnitTotalResults, processJVMStdout, - processJVMStderr, createErrorText + processJVMStderr, + createErrorText, + processBatchJVMOutput } from "./view/output-view"; +import {arrayFrom} from "./utils"; /** * @typedef {Object} KotlinVersion @@ -69,7 +72,7 @@ export default class WebDemoApi { } /** - * Request on execute Kotlin code. + * Request to execute Kotlin code. * * @param code - string * @param compilerVersion - string kotlin compiler @@ -82,56 +85,11 @@ export default class WebDemoApi { * @param callback - a callback for output chunks */ static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { - const testResults = { - testsRun: 0, - totalTime: 0, - success: true + if (API_URLS.COMPILE_ASYNC) { + executeKotlinCodeStreaming(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) + } else { + executeKotlinCodeSync(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) } - executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { - let output; - if (result.errorText) { - output = createErrorText(result.errorText, theme) - } else if (platform === TargetPlatform.JUNIT) { - output = processJUnitTotalResults(testResults, onTestPassed, onTestFailed) - } - callback({ - waitingForOutput: false, - output: output - }) - }, result => { - const data = result.data - - let errorsAndWarnings - let errors = [] - if (data.hasOwnProperty('errors')) { - errorsAndWarnings = flatten(Object.values(data.errors)); - errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); - } - - let output; - if (errors.length > 0) { - output = processErrors(errors, theme); - } else if (data.hasOwnProperty('errStream')) { - output = processJVMStderr(data.errStream, theme) - } else if (data.hasOwnProperty('outStream')) { - output = processJVMStdout(data.outStream) - } else if (data.hasOwnProperty('testResult') && platform === TargetPlatform.JUNIT) { - output = processJUnitTestResult(data.testResult, testResults) - } - - let exception; - if (data.hasOwnProperty('exception')) { - exception = findSecurityException(data.exception); - exception.causes = getExceptionCauses(exception); - exception.cause = undefined; - } - callback({ - waitingForOutput: true, - errors: errorsAndWarnings, - output: output, - exception: exception - }) - }) } /** @@ -166,6 +124,112 @@ export default class WebDemoApi { } } +function processJUnitResults(data, theme, onTestPassed, onTestFailed) { // use for synchronous output only + const testResults = { + testsRun: 0, + totalTime: 0, + success: true + } + let output = "" + for (let testClass in data) { + arrayFrom(data[testClass]).forEach(testResult => { + output += processJUnitTestResult(testResult, testResults, false) + }) + } + output += processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) + return output +} + +function executeKotlinCodeSync(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + executeCode(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies).then(function (data) { + let output = ""; + let errorsAndWarnings = flatten(Object.values(data.errors)); + let errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + if (errors.length > 0) { + output = processErrors(errors, theme); + } else { + switch (platform) { + case TargetPlatform.JAVA: + if (data.text) output = processBatchJVMOutput(data.text, theme); + break; + case TargetPlatform.JUNIT: + if (data.testResults || !data.text) { + output = processJUnitResults(data.testResults, theme, onTestPassed, onTestFailed) + } else { + output = processBatchJVMOutput(data.text, theme); + } + break; + } + } + let exceptions = null; + if (data.exception != null) { + exceptions = findSecurityException(data.exception); + exceptions.causes = getExceptionCauses(exceptions); + exceptions.cause = undefined; + } + callback({ + waitingForOutput: false, + errors: errorsAndWarnings, + output: output, + exception: exceptions + }) + }) +} + + +function executeKotlinCodeStreaming(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + const testResults = { + testsRun: 0, + totalTime: 0, + success: true + } + executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { + let output; + if (result.errorText) { + output = createErrorText(result.errorText, theme) + } else if (platform === TargetPlatform.JUNIT) { + output = processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) + } + callback({ + waitingForOutput: false, + output: output + }) + }, result => { + const data = result.data + + let errorsAndWarnings + let errors = [] + if (data.hasOwnProperty('errors')) { + errorsAndWarnings = flatten(Object.values(data.errors)); + errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + } + + let output; + if (errors.length > 0) { + output = processErrors(errors, theme); + } else if (data.hasOwnProperty('errStream')) { + output = processJVMStderr(data.errStream, theme) + } else if (data.hasOwnProperty('outStream')) { + output = processJVMStdout(data.outStream) + } else if (data.hasOwnProperty('testResult') && platform === TargetPlatform.JUNIT) { + output = processJUnitTestResult(data.testResult, testResults, true) + } + + let exception; + if (data.hasOwnProperty('exception')) { + exception = findSecurityException(data.exception); + exception.causes = getExceptionCauses(exception); + exception.cause = undefined; + } + callback({ + waitingForOutput: true, + errors: errorsAndWarnings, + output: output, + exception: exception + }) + }) +} + function createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { const files = [buildFileObject(code, DEFAULT_FILE_NAME)] .concat(hiddenDependencies.map((file, index) => buildFileObject(file, `hiddenDependency${index}.kt`))); diff --git a/webpack.config.js b/webpack.config.js index 19e8cd16..bfbd04e8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ module.exports = (params = {}) => { const isServer = process.argv[1].includes('webpack-dev-server'); const libraryName = 'KotlinPlayground'; const webDemoUrl = params.webDemoUrl || 'https://try.kotlinlang.org'; + const asyncServerUrl = params.asyncServerUrl; const examplesPath = isServer ? '' : 'examples/'; const config = { @@ -75,6 +76,7 @@ module.exports = (params = {}) => { new webpack.DefinePlugin({ __WEBDEMO_URL__: JSON.stringify(webDemoUrl), + __ASYNC_SERVER_URL__: JSON.stringify(asyncServerUrl), __IS_PRODUCTION__: isProduction, __LIBRARY_NAME__: JSON.stringify(libraryName), 'process.env': { From 8ef53c5a40e34a24e50763e631e514ada11ff6f2 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 24 May 2020 02:08:02 +0300 Subject: [PATCH 07/10] add onOutputAddedToDom callback --- README.md | 2 ++ src/executable-code/executable-fragment.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index aef24655..db5b3f42 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ playground('.selector', options) - `onOpenConsole` — Is called after the console's opened. +- `onOutputAddedToDom` - Is called after the output is added to DOM. + - `getJsCode(code)` — Is called after compilation Kotlin to JS. Use for target platform `js`. _code_ — converted JS code from Kotlin. diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index 23d50a3e..92058230 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -260,6 +260,10 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { onExceptionClick: this.onExceptionClick.bind(this) }) } + + if ((stateUpdate.output || stateUpdate.exception) && this.state.onOutputAddedToDom) { + this.state.onOutputAddedToDom() + } } markPlaceHolders() { From f05220660efc09e325ef902658463eadebd3b523 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 24 May 2020 02:15:01 +0300 Subject: [PATCH 08/10] remove is-empty-object --- package.json | 1 - src/view/output-view.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index a02ccdc0..dd9d6e7e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "flatten": "^1.0.2", "github-markdown-css": "^3.0.1", "html-webpack-plugin": "^3.2.0", - "is-empty-object": "^1.1.1", "jsonpipe": "2.2.0", "markdown-it": "^8.4.2", "markdown-it-highlightjs": "^3.0.0", diff --git a/src/view/output-view.js b/src/view/output-view.js index 92582fdb..ca070e33 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -1,5 +1,4 @@ -import {arrayFrom, convertToHtmlTag, escapeBrackets, processingHtmlBrackets} from "../utils"; -import isEmptyObject from "is-empty-object" +import {convertToHtmlTag, escapeBrackets} from "../utils"; import escapeHtml from "escape-html" From 33e3289aa9cdb27caae91f02b85e9619764bd8be Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Sun, 24 May 2020 02:17:43 +0300 Subject: [PATCH 09/10] fix lockfile --- yarn.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2aaa60de..7a469274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3430,10 +3430,6 @@ is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" -is-empty-object@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-empty-object/-/is-empty-object-1.1.1.tgz#86d5d4d5c5229cea31ec2772f528bf5efc519f23" - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" From 63a046f5da51e88ce9ae059db61fb0ddaa609162 Mon Sep 17 00:00:00 2001 From: Konstantin Lyubort Date: Mon, 8 Jun 2020 01:02:54 +0300 Subject: [PATCH 10/10] rename onOutputAddedToDom to onOutputUpdate --- README.md | 2 +- src/executable-code/executable-fragment.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index db5b3f42..ee5e3b2d 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ playground('.selector', options) - `onOpenConsole` — Is called after the console's opened. -- `onOutputAddedToDom` - Is called after the output is added to DOM. +- `onOutputUpdate` - Is called after the output is added to DOM. - `getJsCode(code)` — Is called after compilation Kotlin to JS. Use for target platform `js`. _code_ — converted JS code from Kotlin. diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index 92058230..a8c9948a 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -261,8 +261,8 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { }) } - if ((stateUpdate.output || stateUpdate.exception) && this.state.onOutputAddedToDom) { - this.state.onOutputAddedToDom() + if ((stateUpdate.output || stateUpdate.exception) && this.state.onOutputUpdate) { + this.state.onOutputUpdate() } }