Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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=`;
},
Expand Down
130 changes: 113 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 All @@ -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",
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.onOutputAddedToDom) {
this.state.onOutputAddedToDom()
}
}

markPlaceHolders() {
let taskRanges = this.getTaskRanges();
this.codemirror.setValue(this.codemirror.getValue()
Expand Down Expand Up @@ -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();
}

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

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
4 changes: 2 additions & 2 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 @@ -134,7 +134,7 @@ export function unEscapeString(string) {
}

/**
* convert all `<` and `>` to `&lt;` and `&gt;`
* convert all `&amp;lt;` and `&amp;gt;` to `&lt;` and `&gt;`
* @param string
* @returns {*}
*/
Expand Down
Loading