UNPKG

@dodona/papyros

Version:

Scratchpad for multiple programming languages in the browser.

172 lines 7.46 kB
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