@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
267 lines • 12.7 kB
JavaScript
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