UNPKG

@dodona/papyros

Version:

Scratchpad for multiple programming languages in the browser.

267 lines 12.7 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { DARK_MODE_TOGGLE_ID, EDITOR_WRAPPER_ID, EXAMPLE_SELECT_ID, INPUT_AREA_WRAPPER_ID, LOCALE_SELECT_ID, MAIN_APP_ID, OUTPUT_AREA_WRAPPER_ID, PANEL_WRAPPER_ID, PROGRAMMING_LANGUAGE_SELECT_ID } from "./Constants"; import { ProgrammingLanguage } from "./ProgrammingLanguage"; import { t, getLocales, removeSelection, addListener, getElement, cleanCurrentUrl, i18n } from "./util/Util"; import { CodeRunner } from "./CodeRunner"; import { getCodeForExample, getExampleNames } from "./examples/Examples"; import { makeChannel } from "sync-message"; import { BackendManager } from "./BackendManager"; import { appendClasses, Renderable, renderLabel, renderSelect, renderSelectOptions, renderWithOptions } from "./util/Rendering"; const LANGUAGE_MAP = new Map([ ["python", ProgrammingLanguage.Python], ["javascript", ProgrammingLanguage.JavaScript] ]); /** * Class that manages multiple components to form a coding scratchpad */ export class Papyros extends Renderable { /** * Construct a new Papyros instance * @param {PapyrosConfig} config Properties to configure this instance */ constructor(config) { super(); this.config = config; i18n.locale = config.locale; this.codeRunner = new CodeRunner(config.programmingLanguage, config.inputMode); } /** * @return {RunState} The current state of the user's code */ getState() { return this.codeRunner.getState(); } /** * Launch this instance of Papyros, making it ready to run code * @return {Promise<Papyros>} Promise of launching, chainable */ launch() { return __awaiter(this, void 0, void 0, function* () { if (!(yield this.configureInput())) { alert(t("Papyros.service_worker_error")); } else { try { yield this.codeRunner.start(); } catch (_a) { if (confirm(t("Papyros.launch_error"))) { return this.launch(); } } } return this; }); } /** * Set the used programming language to the given one to allow editing and running code * @param {ProgrammingLanguage} programmingLanguage The language to use */ setProgrammingLanguage(programmingLanguage) { return __awaiter(this, void 0, void 0, function* () { this.config.programmingLanguage = programmingLanguage; yield this.codeRunner.setProgrammingLanguage(programmingLanguage); }); } /** * @param {string} locale The locale to use */ setLocale(locale) { if (locale !== this.config.locale) { this.config.locale = locale; i18n.locale = locale; this.render(); } } /** * @param {boolean} darkMode Whether to use dark mode */ setDarkMode(darkMode) { if (darkMode !== this.renderOptions.darkMode) { this.renderOptions.darkMode = darkMode; this.render(); } } /** * @param {string} code The code to use in the editor */ setCode(code) { this.codeRunner.editor.setText(code); } /** * @return {string} The currently written code */ getCode() { return this.codeRunner.editor.getText(); } /** * Configure how user input is handled within Papyros * By default, we will try to use SharedArrayBuffers * If this option is not available, the optional arguments in the channelOptions config are used * They are needed to register a service worker to handle communication between threads * @return {Promise<boolean>} Promise of configuring input */ configureInput() { return __awaiter(this, void 0, void 0, function* () { var _a; if (typeof SharedArrayBuffer === "undefined") { if (!((_a = this.config.channelOptions) === null || _a === void 0 ? void 0 : _a.serviceWorkerName) || !("serviceWorker" in navigator)) { return false; } const serviceWorkerRoot = this.config.channelOptions.root || cleanCurrentUrl(true); const { serviceWorkerName } = this.config.channelOptions; this.config.channelOptions.scope = serviceWorkerRoot; const serviceWorkerUrl = serviceWorkerRoot + serviceWorkerName; try { yield navigator.serviceWorker.register(serviceWorkerUrl, { scope: "/" }); BackendManager.channel = makeChannel({ serviceWorker: this.config.channelOptions }); } catch (_b) { return false; } } else { BackendManager.channel = makeChannel({ atomics: Object.assign({}, this.config.channelOptions) }); } return true; }); } _render(renderOptions) { // Set default values for each option for (const [option, defaultParentId] of [ ["inputOptions", INPUT_AREA_WRAPPER_ID], ["statusPanelOptions", PANEL_WRAPPER_ID], ["codeEditorOptions", EDITOR_WRAPPER_ID], ["outputOptions", OUTPUT_AREA_WRAPPER_ID], ["standAloneOptions", MAIN_APP_ID] ]) { const elementOptions = renderOptions[option] || {}; elementOptions.darkMode = renderOptions.darkMode; appendClasses(elementOptions, "tailwind"); renderOptions[option] = Object.assign({ parentElementId: defaultParentId }, elementOptions); } if (this.config.standAlone) { const { locale, programmingLanguage } = this.config; const programmingLanguageSelect = renderSelect(PROGRAMMING_LANGUAGE_SELECT_ID, new Array(...LANGUAGE_MAP.values()), l => t(`Papyros.programming_languages.${l}`), programmingLanguage, t("Papyros.programming_language")); const exampleSelect = renderSelect(EXAMPLE_SELECT_ID, getExampleNames(programmingLanguage), name => name, this.config.example, t("Papyros.examples")); const locales = [locale, ...getLocales().filter(l => l != locale)]; const toggleIconClasses = renderOptions.darkMode ? "mdi-toggle-switch _tw-text-[#FF8F00]" : "mdi-toggle-switch-off _tw-text-white"; const navOptions = ` <div class="_tw-flex _tw-flex-row-reverse dark:_tw-text-white _tw-items-center"> <!-- row-reverse to start at the right, so put elements in order of display --> <i id=${DARK_MODE_TOGGLE_ID} class="mdi ${toggleIconClasses} hover:_tw-cursor-pointer _tw-text-4xl"></i> <p class="_tw-text-white">${t("Papyros.dark_mode")}</p> ${renderSelect(LOCALE_SELECT_ID, locales, l => t(`Papyros.locales.${l}`), locale)} <i class="mdi mdi-web _tw-text-4xl _tw-text-white"></i> </div> `; const navBar = ` <div class="_tw-bg-blue-500 _tw-text-white _tw-text-lg _tw-p-4 _tw-grid _tw-grid-cols-8 _tw-items-center _tw-max-h-1/5 dark:_tw-bg-dark-mode-blue"> <div class="_tw-col-span-6 _tw-text-4xl _tw-font-medium"> ${t("Papyros.Papyros")} </div> <div class="_tw-col-span-2 _tw-text-black"> ${navOptions} </div> </div> `; const header = ` <!-- Header --> <div class="_tw-flex _tw-flex-row _tw-items-center"> ${programmingLanguageSelect} ${exampleSelect} </div>`; renderWithOptions(renderOptions.standAloneOptions, ` <div id="${MAIN_APP_ID}" class="_tw-min-h-screen _tw-h-full dark:_tw-text-white dark:_tw-bg-dark-mode-bg" style="margin-bottom: -40px; padding-bottom: 20px"> ${navBar} <div class="_tw-m-10"> ${header} <!--Body of the application--> <div class="_tw-grid _tw-grid-cols-2 _tw-gap-4 _tw-box-border"> <!-- Code section--> <div> ${renderLabel(t("Papyros.code"), renderOptions.codeEditorOptions.parentElementId)} <div id="${renderOptions.codeEditorOptions.parentElementId}"></div> <div id="${renderOptions.statusPanelOptions.parentElementId}"></div> </div> <!-- User input and output section--> <div> ${renderLabel(t("Papyros.output"), renderOptions.outputOptions.parentElementId)} <div id="${renderOptions.outputOptions.parentElementId}"></div> ${renderLabel(t("Papyros.input"), renderOptions.inputOptions.parentElementId)} <div id="${renderOptions.inputOptions.parentElementId}"></div> </div> </div> <!-- Debugging section--> <div id="${renderOptions.traceOptions.parentElementId}" ></div> </div> </div> `); addListener(PROGRAMMING_LANGUAGE_SELECT_ID, pl => { this.setProgrammingLanguage(pl); getElement(EXAMPLE_SELECT_ID).innerHTML = renderSelectOptions(getExampleNames(pl), name => name); removeSelection(EXAMPLE_SELECT_ID); this.config.example = undefined; // Modify search query params without reloading page history.pushState(null, "", `?locale=${i18n.locale}&language=${pl}`); }, "change", "value"); addListener(LOCALE_SELECT_ID, locale => { // Modify search query params without reloading page history.pushState(null, "", `?locale=${locale}&language=${this.codeRunner.getProgrammingLanguage()}`); this.setLocale(locale); }, "change", "value"); addListener(EXAMPLE_SELECT_ID, (name) => __awaiter(this, void 0, void 0, function* () { this.config.example = name; const code = getCodeForExample(this.codeRunner.getProgrammingLanguage(), name); yield this.codeRunner.reset(); this.setCode(code); }), "change", "value"); // If example is null, it removes the selection getElement(EXAMPLE_SELECT_ID).value = this.config.example; addListener(DARK_MODE_TOGGLE_ID, () => { this.setDarkMode(!renderOptions.darkMode); }, "click"); } this.codeRunner.render({ statusPanelOptions: renderOptions.statusPanelOptions, inputOptions: renderOptions.inputOptions, codeEditorOptions: renderOptions.codeEditorOptions, outputOptions: renderOptions.outputOptions, traceOptions: renderOptions.traceOptions, }); } /** * Add a button to the status panel within Papyros * @param {ButtonOptions} options Options to render the button with * @param {function} onClick Listener for click events on the button */ addButton(options, onClick) { this.codeRunner.addButton(options, onClick); } /** * @param {ProgrammingLanguage} language The language to check * @return {boolean} Whether Papyros supports this language by default */ static supportsProgrammingLanguage(language) { return Papyros.toProgrammingLanguage(language) !== undefined; } /** * Convert a string to a ProgrammingLanguage * @param {string} language The language to convert * @return {ProgrammingLanguage | undefined} The ProgrammingLanguage, or undefined if not supported */ static toProgrammingLanguage(language) { return LANGUAGE_MAP.get(language.toLowerCase()); } } //# sourceMappingURL=Papyros.js.map