mirror of https://github.com/dirtbags/moth.git
Compare commits
No commits in common. "a7c2ee00221d9cf6c3e24a039a23cf5e8d80ce74" and "c4788acaa2a5fe326a9911661f08d36ad27103db" have entirely different histories.
a7c2ee0022
...
c4788acaa2
36
LICENSE.md
36
LICENSE.md
|
@ -129,36 +129,10 @@ Both came with the following license:
|
||||||
> OTHER DEALINGS IN THE FONT SOFTWARE.
|
> OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
Go Fonts
|
Javascript MD5 Library
|
||||||
=======
|
======================
|
||||||
|
|
||||||
The Go fonts were obtained from
|
Obtained from <https://github.com/blueimp/JavaScript-MD5>, which says:
|
||||||
https://go.googlesource.com/image
|
|
||||||
|
|
||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
> The JavaScript MD5 script is released under the
|
||||||
|
> [MIT license](http://www.opensource.org/licenses/MIT).
|
||||||
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.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Boop!
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
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,47 +1,12 @@
|
||||||
/* Color palette: http://paletton.com/#uid=33x0u0klrl-4ON9dhtKtAdqMQ4T */
|
/* 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 {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
background: var(--bg) url("bg.png") center fixed;
|
background: #010e19 url("bg.png") center fixed;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-blend-mode: soft-light;
|
background-blend-mode: soft-light;
|
||||||
background-color: var(--bg);
|
background-color: #010e19;
|
||||||
color: var(--fg);
|
color: #edd488;
|
||||||
}
|
}
|
||||||
canvas.wallpaper {
|
canvas.wallpaper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -59,20 +24,20 @@ main {
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--bg-main);
|
background: #000d;
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: var(--heading);
|
color: #cb2408cc;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
background: var(--bg-heading1);
|
background: #cb240844;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
}
|
}
|
||||||
a:any-link {
|
a:any-link {
|
||||||
color: var(--fg-link);
|
color: #b9cbd8;
|
||||||
}
|
}
|
||||||
form, pre {
|
form, pre {
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
|
@ -84,11 +49,11 @@ input, select {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
background-color: var(--bg-input);
|
background-color: #ccc4;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
input:hover {
|
input:hover {
|
||||||
background-color: var(--bg-input-hover);
|
background-color: #8884;
|
||||||
}
|
}
|
||||||
input:active {
|
input:active {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
@ -98,11 +63,11 @@ input:active {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.notification {
|
.notification {
|
||||||
background: var(--bg-notification);
|
background: #ac8f3944;
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
background: var(--bg-error);
|
background: red;
|
||||||
color: var(--fg-error);
|
color: white;
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -111,7 +76,7 @@ input:active {
|
||||||
/** Puzzles list */
|
/** Puzzles list */
|
||||||
.category {
|
.category {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
background: var(--bg-category);
|
background: #ccc4;
|
||||||
}
|
}
|
||||||
.category h2 {
|
.category h2 {
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
@ -136,7 +101,7 @@ nav li, .category li {
|
||||||
float: right;
|
float: right;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--bg-mothball);
|
background: #ccc;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
@ -150,8 +115,8 @@ nav li, .category li {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
input:invalid {
|
input:invalid {
|
||||||
background-color: var(--bg-input-invalid);
|
background-color: #800;
|
||||||
color: var(--fg-input-invalid);
|
color: white;
|
||||||
}
|
}
|
||||||
.answer_ok {
|
.answer_ok {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
|
@ -163,8 +128,8 @@ input:invalid {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
background: var(--bg-debug);
|
background: #cccc;
|
||||||
color: var(--fg-debug);
|
color: black;
|
||||||
}
|
}
|
||||||
.debug dt {
|
.debug dt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -208,11 +173,28 @@ li[draggable] {
|
||||||
padding: 0.2em 2em;
|
padding: 0.2em 2em;
|
||||||
animation: fadeIn ease 1s;
|
animation: fadeIn ease 1s;
|
||||||
margin: 2px auto;
|
margin: 2px auto;
|
||||||
background: var(--bg-toast);
|
background: #333;
|
||||||
color: var(--fg-toast);
|
color: #eee;
|
||||||
box-shadow: 0px 0px 8px 0px var(--box-toast);
|
box-shadow: 0px 0px 8px 0px #0b0;
|
||||||
}
|
}
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
0% { opacity: 0; }
|
0% { opacity: 0; }
|
||||||
100% { opacity: 1; }
|
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,13 +1,6 @@
|
||||||
{
|
{
|
||||||
"PuzzleList": {
|
|
||||||
"TrackSolved": true,
|
"TrackSolved": true,
|
||||||
"Titles": false,
|
"Titles": false,
|
||||||
"": 0
|
|
||||||
},
|
|
||||||
"Puzzle": {
|
|
||||||
"SyntaxHighlighting": true,
|
|
||||||
"": 0
|
|
||||||
},
|
|
||||||
"Scoreboard": {
|
"Scoreboard": {
|
||||||
"DisplayServerURLWhenEnabled": true,
|
"DisplayServerURLWhenEnabled": true,
|
||||||
"ShowCategoryLeaders": true,
|
"ShowCategoryLeaders": true,
|
||||||
|
@ -15,7 +8,7 @@
|
||||||
"ReplayFPS": 6,
|
"ReplayFPS": 6,
|
||||||
"ReplayDurationMS": 2000,
|
"ReplayDurationMS": 2000,
|
||||||
"NoScoresHtml": "<div class='notification'><h2>~ no scores ~</h2></div>",
|
"NoScoresHtml": "<div class='notification'><h2>~ no scores ~</h2></div>",
|
||||||
"": 0
|
"": ""
|
||||||
},
|
},
|
||||||
"Messages": "<!-- Messages can go here (HTML) -->",
|
"Messages": "<!-- Messages can go here (HTML) -->",
|
||||||
"": "this is here so you don't have to remember to take the comma off the last item"
|
"": "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
|
// Update elements with data-track-solved
|
||||||
for (let e of document.querySelectorAll("[data-track-solved]")) {
|
for (let e of document.querySelectorAll("[data-track-solved]")) {
|
||||||
// Only display if data-track-solved is the same as config.trackSolved
|
// Only display if data-track-solved is the same as config.trackSolved
|
||||||
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.PuzzleList?.TrackSolved)
|
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.TrackSolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let e of document.querySelectorAll(".login")) {
|
for (let e of document.querySelectorAll(".login")) {
|
||||||
|
@ -152,10 +152,10 @@ class App {
|
||||||
a.href = url
|
a.href = url
|
||||||
a.target = "_blank"
|
a.target = "_blank"
|
||||||
|
|
||||||
if (this.config.PuzzleList?.TrackSolved) {
|
if (this.config.TrackSolved) {
|
||||||
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
||||||
}
|
}
|
||||||
if (this.config.PuzzleList?.Titles) {
|
if (this.config.Titles) {
|
||||||
this.loadTitle(puzzle, i)
|
this.loadTitle(puzzle, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
113
theme/puzzle.css
113
theme/puzzle.css
|
@ -1,113 +0,0 @@
|
||||||
@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,7 +6,6 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="icon" href="luna-moth.svg">
|
<link rel="icon" href="luna-moth.svg">
|
||||||
<link rel="stylesheet" href="basic.css">
|
<link rel="stylesheet" href="basic.css">
|
||||||
<link rel="stylesheet" href="puzzle.css">
|
|
||||||
<script src="background.mjs" type="module" async></script>
|
<script src="background.mjs" type="module" async></script>
|
||||||
<script src="puzzle.mjs" type="module" async></script>
|
<script src="puzzle.mjs" type="module" async></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -31,23 +30,5 @@
|
||||||
</main>
|
</main>
|
||||||
<div class="debug" class="notification"></div>
|
<div class="debug" class="notification"></div>
|
||||||
<div class="toasts"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
import * as moth from "./moth.mjs"
|
import * as moth from "./moth.mjs"
|
||||||
import * as common from "./common.mjs"
|
import * as common from "./common.mjs"
|
||||||
import * as workspace from "./workspace/workspace.mjs"
|
|
||||||
|
|
||||||
const server = new moth.Server(".")
|
const server = new moth.Server(".")
|
||||||
|
|
||||||
|
@ -180,41 +179,15 @@ async function loadPuzzle(category, points) {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info("Listing attached files...")
|
console.info("Listing attached files...")
|
||||||
let attachmentUrls = []
|
|
||||||
for (let fn of (puzzle.Attachments || [])) {
|
for (let fn of (puzzle.Attachments || [])) {
|
||||||
let li = document.createElement("li")
|
let li = document.createElement("li")
|
||||||
let a = document.createElement("a")
|
let a = document.createElement("a")
|
||||||
let url = new URL(fn, contentBase)
|
a.href = new URL(fn, contentBase)
|
||||||
attachmentUrls.push(url)
|
|
||||||
a.href = url
|
|
||||||
a.innerText = fn
|
a.innerText = fn
|
||||||
li.appendChild(a)
|
li.appendChild(a)
|
||||||
document.getElementById("files").appendChild(li)
|
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...")
|
console.info("Filling debug information...")
|
||||||
for (let e of document.querySelectorAll(".debug")) {
|
for (let e of document.querySelectorAll(".debug")) {
|
||||||
|
@ -226,7 +199,7 @@ async function loadPuzzle(category, points) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.app.puzzle = puzzle
|
window.app.puzzle = puzzle
|
||||||
console.info("window.app.puzzle:", window.app.puzzle)
|
console.info("window.app.puzzle =", window.app.puzzle)
|
||||||
|
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
|
|
||||||
|
@ -247,9 +220,6 @@ async function init() {
|
||||||
// There isn't a more graceful way to "unload" scripts attached to the current puzzle
|
// There isn't a more graceful way to "unload" scripts attached to the current puzzle
|
||||||
window.addEventListener("hashchange", () => location.reload())
|
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
|
// Make all links absolute, because we're going to be changing the base URL
|
||||||
for (let e of document.querySelectorAll("[href]")) {
|
for (let e of document.querySelectorAll("[href]")) {
|
||||||
e.href = new URL(e.href, common.BaseURL)
|
e.href = new URL(e.href, common.BaseURL)
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
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))
|
|
|
@ -1,214 +0,0 @@
|
||||||
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