mirror of https://github.com/dirtbags/moth.git
Compare commits
4 Commits
c4788acaa2
...
a7c2ee0022
Author | SHA1 | Date |
---|---|---|
Neale Pickett | a7c2ee0022 | |
Neale Pickett | 2f7fba2dff | |
Neale Pickett | 285c101bc6 | |
Neale Pickett | 3e629c6859 |
36
LICENSE.md
36
LICENSE.md
|
@ -129,10 +129,36 @@ Both came with the following license:
|
|||
> OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
|
||||
Javascript MD5 Library
|
||||
======================
|
||||
Go Fonts
|
||||
=======
|
||||
|
||||
Obtained from <https://github.com/blueimp/JavaScript-MD5>, which says:
|
||||
The Go fonts were obtained from
|
||||
https://go.googlesource.com/image
|
||||
|
||||
> The JavaScript MD5 script is released under the
|
||||
> [MIT license](http://www.opensource.org/licenses/MIT).
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Boop!
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
authors:
|
||||
- neale
|
||||
answers:
|
||||
- 146
|
||||
attachments:
|
||||
- boop.txt
|
||||
---
|
||||
|
||||
Some puzzles can have embedded code.
|
||||
|
||||
Your theme may turn this into a full in-browser development environment!
|
||||
|
||||
```python
|
||||
print(open("boop.txt").read())
|
||||
setanswer(0x58 + 58)
|
||||
```
|
|
@ -1,12 +1,47 @@
|
|||
/* Color palette: http://paletton.com/#uid=33x0u0klrl-4ON9dhtKtAdqMQ4T */
|
||||
|
||||
:root {
|
||||
--bg: #010e19;
|
||||
--bg-main: #000d;
|
||||
--heading: #cb2408cc;
|
||||
--bg-heading1: #cb240844;
|
||||
--fg-link: #b9cbd8;
|
||||
--bg-input: #ccc4;
|
||||
--bg-input-hover: #8884;
|
||||
--bg-notification: #ac8f3944;
|
||||
--bg-error: #f00;
|
||||
--fg-error: white;
|
||||
--bg-category: #ccc4;
|
||||
--bg-input-invalid: #800;
|
||||
--fg-input-invalid: white;
|
||||
--bg-mothball: #ccc;
|
||||
--bg-debug: #cccc;
|
||||
--fg-debug: black;
|
||||
--bg-toast: #333;
|
||||
--fg-toast: #eee;
|
||||
--box-toast: #0b0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
/* We uses the alpha channel to apply hue tinting to elements, to get a
|
||||
* similar effect in light or dark mode. That means there aren't a whole lot of
|
||||
* things to change between light and dark mode.
|
||||
*/
|
||||
:root {
|
||||
--bg: #b9cbd8;
|
||||
--fg: black;
|
||||
--bg-main: #fffd;
|
||||
--fg-link: #092b45;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #010e19 url("bg.png") center fixed;
|
||||
background: var(--bg) url("bg.png") center fixed;
|
||||
background-size: cover;
|
||||
background-blend-mode: soft-light;
|
||||
background-color: #010e19;
|
||||
color: #edd488;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
canvas.wallpaper {
|
||||
position: fixed;
|
||||
|
@ -24,20 +59,20 @@ main {
|
|||
margin: 1em auto;
|
||||
padding: 1px 3px;
|
||||
border-radius: 5px;
|
||||
background: #000d;
|
||||
background: var(--bg-main);
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #cb2408cc;
|
||||
color: var(--heading);
|
||||
}
|
||||
h1 {
|
||||
background: #cb240844;
|
||||
background: var(--bg-heading1);
|
||||
padding: 3px;
|
||||
}
|
||||
p {
|
||||
margin: 1em 0em;
|
||||
}
|
||||
a:any-link {
|
||||
color: #b9cbd8;
|
||||
color: var(--fg-link);
|
||||
}
|
||||
form, pre {
|
||||
margin: 1em;
|
||||
|
@ -49,11 +84,11 @@ input, select {
|
|||
max-width: 30em;
|
||||
}
|
||||
input {
|
||||
background-color: #ccc4;
|
||||
background-color: var(--bg-input);
|
||||
color: inherit;
|
||||
}
|
||||
input:hover {
|
||||
background-color: #8884;
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
input:active {
|
||||
background-color: inherit;
|
||||
|
@ -63,11 +98,11 @@ input:active {
|
|||
border-radius: 8px;
|
||||
}
|
||||
.notification {
|
||||
background: #ac8f3944;
|
||||
background: var(--bg-notification);
|
||||
}
|
||||
.error {
|
||||
background: red;
|
||||
color: white;
|
||||
background: var(--bg-error);
|
||||
color: var(--fg-error);
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
|
@ -76,7 +111,7 @@ input:active {
|
|||
/** Puzzles list */
|
||||
.category {
|
||||
margin: 5px 0;
|
||||
background: #ccc4;
|
||||
background: var(--bg-category);
|
||||
}
|
||||
.category h2 {
|
||||
margin: 0 0.2em;
|
||||
|
@ -101,7 +136,7 @@ nav li, .category li {
|
|||
float: right;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
background: #ccc;
|
||||
background: var(--bg-mothball);
|
||||
padding: 4px 8px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
@ -115,8 +150,8 @@ nav li, .category li {
|
|||
max-width: 100%;
|
||||
}
|
||||
input:invalid {
|
||||
background-color: #800;
|
||||
color: white;
|
||||
background-color: var(--bg-input-invalid);
|
||||
color: var(--fg-input-invalid);
|
||||
}
|
||||
.answer_ok {
|
||||
cursor: help;
|
||||
|
@ -128,8 +163,8 @@ input:invalid {
|
|||
padding: 1em;
|
||||
border-radius: 10px;
|
||||
margin: 2em auto;
|
||||
background: #cccc;
|
||||
color: black;
|
||||
background: var(--bg-debug);
|
||||
color: var(--fg-debug);
|
||||
}
|
||||
.debug dt {
|
||||
font-weight: bold;
|
||||
|
@ -173,28 +208,11 @@ li[draggable] {
|
|||
padding: 0.2em 2em;
|
||||
animation: fadeIn ease 1s;
|
||||
margin: 2px auto;
|
||||
background: #333;
|
||||
color: #eee;
|
||||
box-shadow: 0px 0px 8px 0px #0b0;
|
||||
background: var(--bg-toast);
|
||||
color: var(--fg-toast);
|
||||
box-shadow: 0px 0px 8px 0px var(--box-toast);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
/* We uses the alpha channel to apply hue tinting to elements, to get a
|
||||
* similar effect in light or dark mode. That means there aren't a whole lot of
|
||||
* things to change between light and dark mode.
|
||||
*/
|
||||
body {
|
||||
background-color: #b9cbd8;
|
||||
color: black;
|
||||
}
|
||||
main {
|
||||
background-color: #fffd;
|
||||
}
|
||||
a:any-link {
|
||||
color: #092b45;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"TrackSolved": true,
|
||||
"Titles": false,
|
||||
"PuzzleList": {
|
||||
"TrackSolved": true,
|
||||
"Titles": false,
|
||||
"": 0
|
||||
},
|
||||
"Puzzle": {
|
||||
"SyntaxHighlighting": true,
|
||||
"": 0
|
||||
},
|
||||
"Scoreboard": {
|
||||
"DisplayServerURLWhenEnabled": true,
|
||||
"ShowCategoryLeaders": true,
|
||||
|
@ -8,7 +15,7 @@
|
|||
"ReplayFPS": 6,
|
||||
"ReplayDurationMS": 2000,
|
||||
"NoScoresHtml": "<div class='notification'><h2>~ no scores ~</h2></div>",
|
||||
"": ""
|
||||
"": 0
|
||||
},
|
||||
"Messages": "<!-- Messages can go here (HTML) -->",
|
||||
"": "this is here so you don't have to remember to take the comma off the last item"
|
||||
|
|
Binary file not shown.
|
@ -88,7 +88,7 @@ class App {
|
|||
// Update elements with data-track-solved
|
||||
for (let e of document.querySelectorAll("[data-track-solved]")) {
|
||||
// Only display if data-track-solved is the same as config.trackSolved
|
||||
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.TrackSolved)
|
||||
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.PuzzleList?.TrackSolved)
|
||||
}
|
||||
|
||||
for (let e of document.querySelectorAll(".login")) {
|
||||
|
@ -152,10 +152,10 @@ class App {
|
|||
a.href = url
|
||||
a.target = "_blank"
|
||||
|
||||
if (this.config.TrackSolved) {
|
||||
if (this.config.PuzzleList?.TrackSolved) {
|
||||
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
||||
}
|
||||
if (this.config.Titles) {
|
||||
if (this.config.PuzzleList?.Titles) {
|
||||
this.loadTitle(puzzle, i)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
@font-face {
|
||||
font-family: "Go";
|
||||
src: url("fonts/Go-Regular.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Go-Mono";
|
||||
src: url("fonts/Go-Mono.ttf");
|
||||
}
|
||||
|
||||
/** Workspace
|
||||
*
|
||||
* Tools for this puzzle: shows up in content.
|
||||
* Right now this is just a Python interpreter.
|
||||
*/
|
||||
.workspace {
|
||||
background-color: rgba(255, 240, 220, 0.3);
|
||||
white-space: normal;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.output {
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
margin: 0.5em 0;
|
||||
padding: 0.5em;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 3em;
|
||||
max-height: 24em;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.output, .editor {
|
||||
font-family: Go, "source code pro", consolas, monospace;
|
||||
}
|
||||
|
||||
.fixed .output, .fixed .editor {
|
||||
font-family: "source code pro", consolas, monospace;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.controls .status {
|
||||
font-size: 9pt;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.stdout,
|
||||
.stderr,
|
||||
.stdinfo,
|
||||
.traceback {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.stderr {
|
||||
color: #f88;
|
||||
}
|
||||
.traceback {
|
||||
background-color: #222;
|
||||
}
|
||||
.stdinfo {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor {
|
||||
border: 1px solid black;
|
||||
overflow-y: scroll;
|
||||
max-height: 24em;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
font-size: 12pt;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
.editor .linenos {
|
||||
background-color: #eee;
|
||||
white-space: pre;
|
||||
min-width: 2em;
|
||||
padding: 0 4px;
|
||||
text-align: right;
|
||||
height: fit-content;
|
||||
}
|
||||
.editor .text {
|
||||
background-color: #fff;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
white-space: nowrap;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
padding: 0 4px;
|
||||
height: fit-content;
|
||||
min-height: 8em;
|
||||
}
|
||||
|
||||
/* Some things that crop up in puzzles */
|
||||
[draggable] {
|
||||
padding-left: 1em;
|
||||
background-image: url(../images/drag-handle.svg);
|
||||
background-position: 0 center;
|
||||
background-size: 1em 1em;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
margin: 2px 0px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
[draggable].over,
|
||||
[draggable].moving {
|
||||
background-color: rgba(127, 127, 127, 0.5);
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
<meta charset="utf-8">
|
||||
<link rel="icon" href="luna-moth.svg">
|
||||
<link rel="stylesheet" href="basic.css">
|
||||
<link rel="stylesheet" href="puzzle.css">
|
||||
<script src="background.mjs" type="module" async></script>
|
||||
<script src="puzzle.mjs" type="module" async></script>
|
||||
</head>
|
||||
|
@ -30,5 +31,23 @@
|
|||
</main>
|
||||
<div class="debug" class="notification"></div>
|
||||
<div class="toasts"></div>
|
||||
<template id="workspace">
|
||||
<div class="editor">
|
||||
<div class="linenos"></div>
|
||||
<div class="text"></div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="run">Run</button>
|
||||
<button class="font" title="Switch in and out of monospace font">Font</button>
|
||||
<span class="status">Execution time: 0.03s</span>
|
||||
<button class="revert" title="Reset code to original">Revert</button>
|
||||
</div>
|
||||
<div class="output">
|
||||
<div class="stdout"></div>
|
||||
<div class="stderr"></div>
|
||||
<div class="traceback"></div>
|
||||
<div class="stdinfo"></div>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import * as moth from "./moth.mjs"
|
||||
import * as common from "./common.mjs"
|
||||
import * as workspace from "./workspace/workspace.mjs"
|
||||
|
||||
const server = new moth.Server(".")
|
||||
|
||||
|
@ -129,7 +130,7 @@ function writeObject(e, obj) {
|
|||
* @param {string} category
|
||||
* @param {number} points
|
||||
*/
|
||||
async function loadPuzzle(category, points) {
|
||||
async function loadPuzzle(category, points) {
|
||||
console.groupCollapsed("Loading puzzle:", category, points)
|
||||
let contentBase = new URL(`content/${category}/${points}/`, common.BaseURL)
|
||||
|
||||
|
@ -179,15 +180,41 @@ async function loadPuzzle(category, points) {
|
|||
}
|
||||
|
||||
console.info("Listing attached files...")
|
||||
let attachmentUrls = []
|
||||
for (let fn of (puzzle.Attachments || [])) {
|
||||
let li = document.createElement("li")
|
||||
let a = document.createElement("a")
|
||||
a.href = new URL(fn, contentBase)
|
||||
let url = new URL(fn, contentBase)
|
||||
attachmentUrls.push(url)
|
||||
a.href = url
|
||||
a.innerText = fn
|
||||
li.appendChild(a)
|
||||
document.getElementById("files").appendChild(li)
|
||||
}
|
||||
|
||||
let codeBlocks = document.querySelectorAll("code[class^=language-]")
|
||||
for (let i = 0; i < codeBlocks.length; i++) {
|
||||
console.info(`Loading workspace ${i}...`)
|
||||
let codeBlock = codeBlocks[i]
|
||||
let language = "unknown"
|
||||
let sourceCode = codeBlock.textContent
|
||||
for (let c of codeBlock.classList) {
|
||||
let parts = c.split("-")
|
||||
if ((parts.length == 2) && parts[0].startsWith("lang")) {
|
||||
language = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
let id = category + "#" + points + "#" + i
|
||||
let element = document.createElement("div")
|
||||
let template = document.querySelector("template#workspace")
|
||||
element.classList.add("workspace")
|
||||
element.appendChild(template.content.cloneNode(true))
|
||||
element.workspace = new workspace.Workspace(element, id, sourceCode, language, attachmentUrls)
|
||||
|
||||
// Now swap it in for the pre
|
||||
codeBlock.parentElement.replaceWith(element)
|
||||
}
|
||||
|
||||
console.info("Filling debug information...")
|
||||
for (let e of document.querySelectorAll(".debug")) {
|
||||
|
@ -199,7 +226,7 @@ async function loadPuzzle(category, points) {
|
|||
}
|
||||
|
||||
window.app.puzzle = puzzle
|
||||
console.info("window.app.puzzle =", window.app.puzzle)
|
||||
console.info("window.app.puzzle:", window.app.puzzle)
|
||||
|
||||
console.groupEnd()
|
||||
|
||||
|
@ -220,6 +247,9 @@ async function init() {
|
|||
// There isn't a more graceful way to "unload" scripts attached to the current puzzle
|
||||
window.addEventListener("hashchange", () => location.reload())
|
||||
|
||||
// Workspaces may trigger a "this is the answer" event
|
||||
document.addEventListener("setAnswer", e => SetAnswer(e.detail.value))
|
||||
|
||||
// Make all links absolute, because we're going to be changing the base URL
|
||||
for (let e of document.querySelectorAll("[href]")) {
|
||||
e.href = new URL(e.href, common.BaseURL)
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import * as pyodide from "https://cdn.jsdelivr.net/npm/pyodide@0.25.1/pyodide.mjs" // v0.16.1 known good
|
||||
|
||||
const HOME = "/home/web_user"
|
||||
|
||||
async function createInstance() {
|
||||
let instance = await pyodide.loadPyodide()
|
||||
instance.runPython("import sys")
|
||||
self.postMessage({type: "loaded"})
|
||||
return instance
|
||||
}
|
||||
const initialized = createInstance()
|
||||
|
||||
class Buffer {
|
||||
constructor() {
|
||||
this.buf = []
|
||||
}
|
||||
|
||||
write(s) {
|
||||
this.buf.push(s)
|
||||
}
|
||||
|
||||
value() {
|
||||
return this.buf.join("")
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(event) {
|
||||
let data = event.data
|
||||
|
||||
let instance = await initialized
|
||||
let fs = instance._module.FS
|
||||
|
||||
let ret = {
|
||||
result: null,
|
||||
answer: null,
|
||||
stdout: null,
|
||||
stderr: null,
|
||||
traceback: null,
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "nop":
|
||||
// You might want to do nothing in order to display to the user that a run can now be handled
|
||||
break
|
||||
case "run":
|
||||
let sys = instance.globals.get("sys")
|
||||
sys.stdout = new Buffer()
|
||||
sys.stderr = new Buffer()
|
||||
instance.globals.set("setanswer", (s) => {ret.answer = s})
|
||||
|
||||
try {
|
||||
ret.result = await instance.runPythonAsync(data.code)
|
||||
} catch (err) {
|
||||
ret.traceback = err
|
||||
}
|
||||
ret.stdout = sys.stdout.value()
|
||||
ret.stderr = sys.stderr.value()
|
||||
break
|
||||
case "wget":
|
||||
let url = data.url
|
||||
let dir = data.directory || fs.cwd()
|
||||
let filename = url.split("/").pop()
|
||||
let path = dir + "/" + filename
|
||||
|
||||
if (fs.analyzePath(path).exists) {
|
||||
fs.unlink(path)
|
||||
}
|
||||
fs.createLazyFile(dir, filename, url, true, false)
|
||||
break
|
||||
default:
|
||||
ret.result = "Unknown message type: " + data.type
|
||||
break
|
||||
}
|
||||
if (data.channel) {
|
||||
data.channel.postMessage(ret)
|
||||
}
|
||||
}
|
||||
self.addEventListener("message", e => handleMessage(e))
|
|
@ -0,0 +1,214 @@
|
|||
import {Toast} from "../common.mjs"
|
||||
import "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"
|
||||
|
||||
var workers = {}
|
||||
|
||||
// loadWorker returns an existing worker if one exists, otherwise, it starts a new worker
|
||||
function loadWorker(language) {
|
||||
let worker = workers[language]
|
||||
if (!worker) {
|
||||
let url = new URL(language + ".mjs", import.meta.url)
|
||||
worker = new Worker(url, {
|
||||
type: "module",
|
||||
})
|
||||
console.info("Loading worker", url, worker)
|
||||
workers[language] = worker
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
export class Workspace {
|
||||
/**
|
||||
*
|
||||
* @param element {HTMLElement} Element to populate with the workspace
|
||||
* @param id {string} A unique identifier of this workspace
|
||||
* @param code {string} The "pristine" source code for this workspace
|
||||
* @param language {string} The language for this workspace
|
||||
* @param attachmentUrls {URL[]} List of attachment URLs
|
||||
*/
|
||||
constructor(element, id, code, language, attachmentUrls) {
|
||||
this.element = element
|
||||
this.originalCode = code
|
||||
this.language = language
|
||||
this.attachmentUrls = attachmentUrls
|
||||
this.storageKey = "code:" + id
|
||||
|
||||
// Get our document and window
|
||||
this.document = this.element.ownerDocument
|
||||
this.window = this.document.defaultView
|
||||
|
||||
// Load user modifications, if there are any
|
||||
this.code = localStorage[this.storageKey] || this.originalCode
|
||||
|
||||
this.status = this.element.querySelector(".status")
|
||||
this.linenos = this.element.querySelector(".editor .linenos")
|
||||
this.editor = this.element.querySelector(".editor .text")
|
||||
this.stdout = this.element.querySelector(".stdout")
|
||||
this.stderr = this.element.querySelector(".stderr")
|
||||
this.traceback = this.element.querySelector(".traceback")
|
||||
this.stdinfo = this.element.querySelector(".stdinfo")
|
||||
this.runButton = this.element.querySelector("button.run")
|
||||
this.revertButton = this.element.querySelector("button.revert")
|
||||
this.fontButton = this.element.querySelector("button.font")
|
||||
|
||||
this.runButton.disabled = true
|
||||
|
||||
// Load in the editor
|
||||
this.editor.classList.add("language-" + language)
|
||||
import("https://cdn.jsdelivr.net/npm/codejar@4.2.0").then((module) => this.editorReady(module))
|
||||
|
||||
// Load the interpreter
|
||||
this.initLanguage(language)
|
||||
|
||||
this.runButton.addEventListener("click", () => this.run())
|
||||
this.revertButton.addEventListener("click", () => this.revert())
|
||||
this.fontButton.addEventListener("click", () => this.font())
|
||||
}
|
||||
|
||||
async initLanguage(language) {
|
||||
let start = performance.now()
|
||||
this.status.textContent = "Initializing..."
|
||||
this.status.appendChild(document.createElement("progress"))
|
||||
this.worker = loadWorker(language)
|
||||
await this.workerReady()
|
||||
|
||||
let runtime = performance.now() - start
|
||||
let duration = new Date(runtime).toISOString().slice(11, -1)
|
||||
this.status.textContent = "Loaded in " + duration
|
||||
this.runButton.disabled = false
|
||||
|
||||
for (let a of this.attachmentUrls) {
|
||||
let filename = a.pathname.split("/").pop()
|
||||
this.workerWget(a)
|
||||
.then(ret => {
|
||||
this.stdinfo.appendChild(this.document.createElement("div")).textContent = "Downloaded " + filename
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
workerMessage(message) {
|
||||
let chan = new MessageChannel()
|
||||
message.channel = chan.port2
|
||||
this.worker.postMessage(message, [chan.port2])
|
||||
let p = new Promise(
|
||||
(resolve, reject) => {
|
||||
chan.port1.addEventListener("message", e => resolve(e.data), {once: true})
|
||||
}
|
||||
)
|
||||
chan.port1.start()
|
||||
return p
|
||||
}
|
||||
|
||||
workerReady() {
|
||||
return this.workerMessage({type: "nop"})
|
||||
}
|
||||
|
||||
workerWget(url) {
|
||||
return this.workerMessage({
|
||||
type: "wget",
|
||||
url: url.href || url,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* highlight provides a code highlighter for CodeJar
|
||||
*
|
||||
* It calls Prism.highlightElement, then updates line numbers
|
||||
*/
|
||||
highlight(editor) {
|
||||
if (Prism) {
|
||||
// Sometimes it loads slowly
|
||||
Prism.highlightElement(editor)
|
||||
} else {
|
||||
console.warn("No highlighter!", Prism, this.window.document.scripts)
|
||||
}
|
||||
|
||||
// Create a line numbers column
|
||||
if (true) {
|
||||
const code = editor.textContent || ""
|
||||
const lines = code.split("\n")
|
||||
let linesCount = lines.length
|
||||
if (lines[linesCount-1]) {
|
||||
linesCount += 1
|
||||
}
|
||||
|
||||
let ltxt = ""
|
||||
for (let i = 1; i < linesCount; i++) {
|
||||
ltxt += i + "\n"
|
||||
}
|
||||
this.linenos.textContent = ltxt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the editor has imported
|
||||
*
|
||||
*/
|
||||
editorReady(module) {
|
||||
this.jar = module.CodeJar(this.editor, (editor) => this.highlight(editor), {window: this.window})
|
||||
this.jar.updateCode(this.code)
|
||||
switch (this.language) {
|
||||
case "python":
|
||||
this.jar.updateOptions({
|
||||
tab: " ",
|
||||
indentOn: /:$/,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setAnswer(answer) {
|
||||
let evt = new CustomEvent("setAnswer", {detail: {value: answer}, bubbles: true, cancelable: true})
|
||||
this.element.dispatchEvent(evt)
|
||||
|
||||
this.stdinfo.appendChild(this.document.createTextNode("Set answer to "))
|
||||
this.stdinfo.appendChild(this.document.createElement("code")).textContent = answer
|
||||
}
|
||||
|
||||
async run() {
|
||||
let start = performance.now()
|
||||
this.runButton.disabled = true
|
||||
this.status.textContent = "Running..."
|
||||
|
||||
// Save first. Always save first.
|
||||
let program = this.jar.toString()
|
||||
if (program != this.originalCode) {
|
||||
localStorage[this.storageKey] = program
|
||||
}
|
||||
|
||||
let result = await this.workerMessage({
|
||||
type: "run",
|
||||
code: program,
|
||||
})
|
||||
|
||||
this.stdout.textContent = result.stdout
|
||||
this.stderr.textContent = result.stderr
|
||||
this.traceback.textContent = result.traceback
|
||||
while (this.stdinfo.firstChild) this.stdinfo.firstChild.remove()
|
||||
if (result.answer) {
|
||||
this.setAnswer(result.answer)
|
||||
}
|
||||
|
||||
let runtime = performance.now() - start
|
||||
let duration = new Date(runtime).toISOString().slice(11, -1)
|
||||
this.status.textContent = "Ran in " + duration
|
||||
this.runButton.disabled = false
|
||||
}
|
||||
|
||||
revert() {
|
||||
let currentCode = this.jar.toString()
|
||||
let savedCode = localStorage[this.storageKey]
|
||||
if ((currentCode == this.originalCode) && savedCode) {
|
||||
this.jar.updateCode(savedCode)
|
||||
Toast("Re-loaded saved code")
|
||||
} else {
|
||||
this.jar.updateCode(this.originalCode)
|
||||
Toast("Reverted to original code")
|
||||
}
|
||||
}
|
||||
|
||||
font(force) {
|
||||
this.element.classList.toggle("fixed", force)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue