@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
172 lines • 7.46 kB
JavaScript
import escapeHTML from "escape-html";
import { BackendEventType } from "./BackendEvent";
import { BackendManager } from "./BackendManager";
import { renderInCircle } from "./util/HTMLShapes";
import { getElement, parseData, t } from "./util/Util";
import { LogType, papyrosLog } from "./util/Logging";
import { Renderable, renderWithOptions } from "./util/Rendering";
import { OUTPUT_AREA_ID, OUTPUT_OVERFLOW_ID } from "./Constants";
/**
* Component for displaying code output or errors to the user
*/
export class OutputManager extends Renderable {
set debugMode(value) {
this.outputArea.classList.toggle("papyros-debug", value);
this._debugMode = value;
}
constructor() {
super();
this._debugMode = false;
this.content = [];
this.overflown = false;
this.downloadCallback = null;
BackendManager.subscribe(BackendEventType.Start, () => this.reset());
BackendManager.subscribe(BackendEventType.Output, e => this.showOutput(e));
BackendManager.subscribe(BackendEventType.Error, e => this.showError(e));
BackendManager.subscribe(BackendEventType.End, () => this.onRunEnd());
BackendManager.subscribe(BackendEventType.FrameChange, e => {
const outputsToHighlight = e.data.outputs;
const outputElements = this.outputArea.children;
for (let i = 0; i < outputElements.length; i++) {
const outputElement = outputElements[i];
outputElement.classList.toggle("papyros-highlight-debugged", i < outputsToHighlight);
}
});
}
/**
* Retrieve the parent element containing all output parts
*/
get outputArea() {
return getElement(OUTPUT_AREA_ID);
}
/**
* Render an element in the next position of the output area
* @param {string} html Safe string version of the next child to render
* @param {boolean} isNewElement Whether this a newly generated element
*/
renderNextElement(html, isNewElement = true) {
if (isNewElement) { // Only save new ones to prevent duplicating
this.content.push(html);
}
this.outputArea.insertAdjacentHTML("beforeend", html);
// Ensure overflowElement is at the bottom
const overflowElement = document.getElementById(OUTPUT_OVERFLOW_ID);
if (overflowElement) {
this.outputArea.append(overflowElement);
}
// Scroll to bottom to show latest output
this.outputArea.scrollTop = this.outputArea.scrollHeight;
}
/**
* Convert a piece of text to a span element for displaying
* @param {string} text The text content for the span
* @param {boolean} ignoreEmpty Whether to remove empty lines in the text
* @param {string} className Optional class name for the span
* @return {string} String version of the created span
*/
spanify(text, ignoreEmpty = false, className = "") {
let spanText = text;
if (spanText.includes("\n") && spanText !== "\n") {
spanText = spanText.split("\n")
.filter(line => !ignoreEmpty || line.trim().length > 0)
.join("\n");
}
return `<span class="${className}">${escapeHTML(spanText)}</span>`;
}
/**
* Display output to the user, based on its content type
* @param {BackendEvent} output Event containing the output data
*/
showOutput(output) {
const data = parseData(output.data, output.contentType);
if (output.contentType && output.contentType.startsWith("img")) {
this.renderNextElement(`<img src="data:${output.contentType}, ${data}"></img>`);
}
else {
this.renderNextElement(this.spanify(data, false));
}
}
/**
* Display to the user that overflow has occurred, limiting the shown output
* @param {function():void | null} downloadCallback Optional callback to download overflow
*/
onOverflow(downloadCallback) {
this.overflown = true;
this.downloadCallback = downloadCallback;
if (document.getElementById(OUTPUT_OVERFLOW_ID) == null) {
this.renderNextElement(`
<div id=${OUTPUT_OVERFLOW_ID}><span class="_tw-text-red-500
_tw-text-bold">${t("Papyros.output_overflow")}</span>
<a class="hover:_tw-cursor-pointer _tw-text-blue-500"
hidden>${t("Papyros.output_overflow_download")}</a>
</div>`, false);
}
const overflowDiv = getElement(OUTPUT_OVERFLOW_ID);
if (this.downloadCallback !== null) {
const overflowLink = overflowDiv.lastElementChild;
overflowLink.hidden = false;
overflowLink.addEventListener("click", this.downloadCallback);
}
}
/**
* Display an error to the user
* @param {BackendEvent} error Event containing the error data
*/
showError(error) {
let errorHTML = "";
const errorData = parseData(error.data, error.contentType);
papyrosLog(LogType.Debug, "Showing error: ", errorData);
if (typeof (errorData) === "string") {
errorHTML = this.spanify(errorData, false, "_tw-text-red-500");
}
else {
const errorObject = errorData;
let shortTraceback = (errorObject.where || "").trim();
// Prepend a bit of indentation, so every part has indentation
if (shortTraceback) {
shortTraceback = this.spanify(" " + shortTraceback, true, "where");
}
errorHTML += "<div class=\"_tw-text-red-500 _tw-text-bold\">";
const infoQM = renderInCircle("?", escapeHTML(errorObject.info), "_tw-text-blue-500 _tw-border-blue-500 dark:_tw-text-dark-mode-blue dark:_tw-border-dark-mode-blue");
const tracebackEM = renderInCircle("!", escapeHTML(errorObject.traceback), "_tw-text-red-500 _tw-border-red-500");
errorHTML += `${infoQM}${errorObject.name} traceback:${tracebackEM}\n`;
errorHTML += shortTraceback;
errorHTML += "</div>\n";
if (errorObject.what) {
errorHTML += this.spanify(errorObject.what.trim(), true, "what") + "\n";
}
if (errorObject.why) {
errorHTML += this.spanify(errorObject.why.trim(), true, "why") + "\n";
}
}
this.renderNextElement(errorHTML);
}
_render(options) {
renderWithOptions(options, `
<div id=${OUTPUT_AREA_ID} class="_tw-border-2 _tw-w-full _tw-min-h-1/4
_tw-max-h-3/5 _tw-overflow-auto papyros-font-family
_tw-py-1 _tw-px-2 _tw-whitespace-pre _tw-rounded-lg dark:_tw-border-dark-mode-content
with-placeholder ${this._debugMode ? "papyros-debug" : ""}" data-placeholder="${t("Papyros.output_placeholder")}"></div>
`);
// Restore previously rendered items
this.content.forEach(html => this.renderNextElement(html, false));
if (this.overflown) {
this.onOverflow(this.downloadCallback);
}
}
/**
* Clear the contents of the output area
*/
reset() {
this.content = [];
this.overflown = false;
this.downloadCallback = null;
this.render();
}
onRunEnd() {
if (this.outputArea.childElementCount === 0) {
this.outputArea.setAttribute("data-placeholder", t("Papyros.no_output"));
}
}
}
//# sourceMappingURL=OutputManager.js.map