diff --git a/README.md b/README.md
index aef24655..ee5e3b2d 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,8 @@ playground('.selector', options)
- `onOpenConsole` — Is called after the console's opened.
+- `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/package.json b/package.json
index b3d3714d..dd9d6e7e 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"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",
"monkberry": "4.0.8",
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 b52e71ab..a8c9948a 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"
@@ -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",
@@ -51,9 +54,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 +129,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 +191,81 @@ export default class ExecutableFragment extends ExecutableCodeTemplate {
}
}
+ removeAllOutputNodes() {
+ const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0)
+ if (outputNode) {
+ outputNode.innerHTML = ""
+ }
+ }
+
+ applyStateUpdate(stateUpdate) {
+ 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.assign(this.state, filteredStateUpdate, {
+ isShouldBeFolded: this.isShouldBeFolded && filteredStateUpdate.isFoldedButton
+ })
+ }
+
+ 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();
+ if (template.content.childElementCount !== 1) { // synchronous mode
+ this.appendMultipleNodesToDOM(parent, template.content.childNodes)
+ } else { // streaming
+ this.appendOneNodeToDOM(parent, template.content.firstChild)
+ }
+ }
+
+ if (stateUpdate.exception) {
+ const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0)
+ const exceptionView = Monkberry.render(Exception, outputNode, {
+ 'directives': directives
+ })
+ exceptionView.update({
+ ...stateUpdate.exception,
+ onExceptionClick: this.onExceptionClick.bind(this)
+ })
+ }
+
+ if ((stateUpdate.output || stateUpdate.exception) && this.state.onOutputUpdate) {
+ this.state.onOutputUpdate()
+ }
+ }
+
markPlaceHolders() {
let taskRanges = this.getTaskRanges();
this.codemirror.setValue(this.codemirror.getValue()
@@ -226,11 +311,11 @@ 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());
- this.update({output: "", openConsole: false, exception: null});
+ this.update({output: null, openConsole: false, exception: null});
if (onCloseConsole) onCloseConsole();
}
@@ -248,6 +333,9 @@ export default class ExecutableFragment extends ExecutableCodeTemplate {
return
}
this.update({
+ errors: null,
+ output: null,
+ exception: null,
waitingForOutput: true,
openConsole: false
});
@@ -261,18 +349,26 @@ export default class ExecutableFragment extends ExecutableCodeTemplate {
theme,
hiddenDependencies,
onTestPassed,
- onTestFailed).then(
+ onTestFailed,
state => {
- state.waitingForOutput = false;
- if (state.output || state.exception) {
+ if (state.waitingForOutput) {
+ this.update(state)
+ return
+ }
+ // 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 (hasOutput || hasException) {
state.openConsole = true;
- } else {
- if (onCloseConsole) onCloseConsole();
+ } else if (onCloseConsole) {
+ onCloseConsole()
}
- if ((state.errors.length > 0 || state.exception) && onError) onError();
+ if ((hasErrors || hasException) && onError) onError();
this.update(state);
- },
- () => this.update({waitingForOutput: false})
+ }
)
} else {
this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe());
@@ -357,7 +453,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..52d43746 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 = {
"<": "<",
">": ">"
@@ -134,7 +134,7 @@ export function unEscapeString(string) {
}
/**
- * convert all `<` and `>` to `<` and `>`
+ * convert all `<` and `>` to `<` and `>`
* @param string
* @returns {*}
*/
diff --git a/src/view/output-view.js b/src/view/output-view.js
index c3b85959..ca070e33 100644
--- a/src/view/output-view.js
+++ b/src/view/output-view.js
@@ -1,12 +1,11 @@
-import {arrayFrom, convertToHtmlTag, processingHtmlBrackets} from "../utils";
-import isEmptyObject from "is-empty-object"
+import {convertToHtmlTag, escapeBrackets} from "../utils";
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 ANGLE_BRACKETS_LEFT_HTML = "<";
const ANGLE_BRACKETS_RIGHT_HTML = ">";
@@ -16,14 +15,31 @@ 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` +
- `Please click here to submit it ` +
+const BUG_FLAG = 'BUG'
+const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' +
+ `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${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 `&`
+export function processJVMStdout(output, theme) {
+ const processedOutput = escapeHtml(output);
+ return `${processedOutput}`
+}
+
+export function createErrorText(output, theme) {
+ const processedOutput = escapeHtml(output);
+ return `${processedOutput}`
+}
+
+export function processJVMStderr(output, theme) {
+ if (output === BUG_FLAG) {
+ output = BUG_REPORT_MESSAGE
+ }
+ 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(``)
@@ -32,37 +48,49 @@ export function processJVMOutput(output, theme) {
.split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("");
}
-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 processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) {
+ if (testResults.testsRun === 0) {
+ return createErrorText(NO_TEST_FOUND, theme)
}
- if (passed && onTestPassed) onTestPassed();
- if (!passed && onTestFailed) onTestFailed();
- let testTime = `Total test time: ${totalTime}s
`;
- return testTime + result;
+ if (testResults.success) {
+ if (onTestPassed) onTestPassed()
+ } else {
+ if (onTestFailed) onTestFailed()
+ }
+ return `Total test time: ${testResults.totalTime}s
`
+}
+
+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, needToEscape);
+ break;
+ case TEST_STATUS.ERROR.value:
+ testResults.success = false;
+ 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, "", 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 ? ': ' + convertToHtmlTag(message) : ''}
+
${status}: ${method}${message ? ': ' + escapedMessage : ''}
`;
}
diff --git a/src/webdemo-api.js b/src/webdemo-api.js
index 7eb5a03d..38417716 100644
--- a/src/webdemo-api.js
+++ b/src/webdemo-api.js
@@ -3,13 +3,19 @@ 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,
+ createErrorText,
+ processBatchJVMOutput
} from "./view/output-view";
+import {arrayFrom} from "./utils";
/**
* @typedef {Object} KotlinVersion
@@ -66,47 +72,24 @@ export default class WebDemoApi {
}
/**
- * Request on execute Kotlin code.
+ * Request to 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");
- 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;
- }
- }
- let exceptions = null;
- if (data.exception != null) {
- exceptions = findSecurityException(data.exception);
- exceptions.causes = getExceptionCauses(exceptions);
- exceptions.cause = undefined;
- }
- return {
- errors: errorsAndWarnings,
- output: output,
- exception: exceptions
- }
- })
+ static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) {
+ 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)
+ }
}
/**
@@ -141,7 +124,113 @@ export default class WebDemoApi {
}
}
-function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, options) {
+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`)));
const projectJson = JSON.stringify({
@@ -164,6 +253,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 +268,33 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep
}).then(response => response.json())
}
+function executeCodeStreaming(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, onDone, onData) {
+ const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies)
+
+ let xmlHttpRequest;
+ xmlHttpRequest = jsonpipe.flow(url + targetPlatform.id, {
+ success: data => {
+ onData({'data': data})
+ },
+ complete: () => {
+ 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: {
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ 'Enable-Streaming': 'true'
+ },
+ data: body.toString(),
+ withCredentials: false
+ });
+}
+
/**
*
* Build file object.
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': {
diff --git a/yarn.lock b/yarn.lock
index 2f58577e..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"
@@ -3683,6 +3679,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"