Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
99 changes: 82 additions & 17 deletions src/executable-code/executable-fragment.js
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -181,6 +188,57 @@ 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do it after filter and code should be simpler after that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I placed this peace of code after filtration but could not come up with any idea how to make it simpler. We must distinguish null here because passing null as stateUpdate.errors tells the function to remove previous errors in the state. Therefore if is necessary here.

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 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) {
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()
Expand Down Expand Up @@ -226,11 +284,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();
}

Expand All @@ -248,6 +306,9 @@ export default class ExecutableFragment extends ExecutableCodeTemplate {
return
}
this.update({
errors: null,
output: null,
exception: null,
waitingForOutput: true,
openConsole: false
});
Expand All @@ -261,18 +322,22 @@ 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
}
// 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());
Expand Down Expand Up @@ -357,7 +422,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);

Expand Down
23 changes: 5 additions & 18 deletions src/executable-code/executable-fragment.monk
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{% import Exception from './exception' %}

<div class="executable-fragment-wrapper">
<div class="executable-fragment {{ theme }}">
{% if (!highlightOnly) %}
Expand All @@ -18,26 +16,15 @@
{% if (openConsole) %}
<div class="console-close {{ theme }}" :onclick={{ this.onConsoleCloseButtonEnter.bind(this) }}></div>
{% endif %}
{% if output || exception %}
<div class="output-wrapper {{ theme }}">
<div class="code-output"></div>
</div>
{% endif %}
{% if (waitingForOutput) %}
<div class="output-wrapper {{ theme }}">
<div class="loader {{ theme }}"></div>
</div>
{% else %}
{% if (output && output != "") || exception %}
<div class="output-wrapper {{ theme }}">
<div class="code-output">
{% unsafe output %}

{% if exception %}
<Exception
{{...exception}}
originalException={{ true }}
onExceptionClick={{ this.onExceptionClick.bind(this) }}
/>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/js-executor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()}";
Expand Down Expand Up @@ -37,7 +37,7 @@ export default class JsExecutor {
if (loadedScripts === jsLibs.size + 2) {
try {
const output = this.iframe.contentWindow.eval(jsCode);
return output ? `<span class="standard-output ${theme}">${processingHtmlBrackets(output)}</span>` : "";
return output ? `<span class="standard-output ${theme}">${escapeBrackets(output)}</span>` : "";
} catch (e) {
if (onError) onError();
let exceptionOutput = showJsException(e);
Expand Down
19 changes: 1 addition & 18 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function insertAfter(newNode, referenceNode) {
* @param string
* @returns {*}
*/
export function processingHtmlBrackets(string) {
export function escapeBrackets(string) {
const tagsToReplace = {
"&lt;": "<",
"&gt;": ">"
Expand Down Expand Up @@ -133,23 +133,6 @@ export function unEscapeString(string) {
return unEscapedString
}

/**
* convert all `<` and `>` to `&lt;` and `&gt;`
* @param string
* @returns {*}
*/
export function convertToHtmlTag(string) {
const tagsToReplace = {
"&lt;": "&amp;lt;",
"&gt;": "&amp;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
Expand Down
84 changes: 45 additions & 39 deletions src/view/output-view.js
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -7,62 +7,68 @@ 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 = "&lt;";
const ANGLE_BRACKETS_RIGHT_HTML = "&gt;";

const TEST_STATUS = {
FAIL : { value: "FAIL", text: "Fail" },
ERROR: { value: "ERROR", text: "Error" },
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 <a href=http://kotl.in/issue target=_blank>here<a> 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 ` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is happened with link? :(

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging DOM nodes turns into text all inner elements so the link will not be clickable.
I thought that this case with BUG_FLAG is too rare and not really important to add specific behaviour for it.
Is it possible to merge nodes without destroying children?

`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(`<span class="standard-output ${theme}">`)
.split(`${ANGLE_BRACKETS_LEFT_HTML}/outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("</span>")
.split(`${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(`<span class="error-output ${theme}">`)
.split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("</span>");
export function processJVMStdout(output, theme) {
const processedOutput = escapeBrackets(output);
return `<span class="standard-output ${theme}">${processedOutput}</span>`
}

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) {
output = BUG_REPORT_MESSAGE
}
const processedOutput = escapeBrackets(output);
return `<span class="error-output ${theme}">${processedOutput}</span>`
}

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 `<div class="test-time">Total test time: ${testResults.totalTime}s</div>`
}

export function processJUnitTestResult(testRunInfo, testResults) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lyubortk actually i don't understand what is happend with test output. Can you explain me differences in this output. It's too much changes not clear for me :(

Copy link
Author

@lyubortk lyubortk May 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to rewrite that because each test result must be handled separately with streaming.

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 = `<div class="test-time">Total test time: ${totalTime}s</div>`;
return testTime + result;
return output;
}

function buildOutputTestLine(status, method, message) {
return `
<div class="console-block">
<span class="console-icon ${status.toLocaleLowerCase()}"></span>
<div class="test-${status.toLocaleLowerCase()}">${status}: ${method}${message ? ': ' + convertToHtmlTag(message) : ''}</div>
<div class="test-${status.toLocaleLowerCase()}">${status}: ${method}${message ? ': ' + escapeBrackets(message) : ''}</div>
</div>
`;
}
Expand Down
Loading