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.
|
> 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
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
> [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.
|
||||||
|
|
|
@ -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 */
|
/* 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: #010e19 url("bg.png") center fixed;
|
background: var(--bg) url("bg.png") center fixed;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-blend-mode: soft-light;
|
background-blend-mode: soft-light;
|
||||||
background-color: #010e19;
|
background-color: var(--bg);
|
||||||
color: #edd488;
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
canvas.wallpaper {
|
canvas.wallpaper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -24,20 +59,20 @@ main {
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #000d;
|
background: var(--bg-main);
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #cb2408cc;
|
color: var(--heading);
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
background: #cb240844;
|
background: var(--bg-heading1);
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
}
|
}
|
||||||
a:any-link {
|
a:any-link {
|
||||||
color: #b9cbd8;
|
color: var(--fg-link);
|
||||||
}
|
}
|
||||||
form, pre {
|
form, pre {
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
|
@ -49,11 +84,11 @@ input, select {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
background-color: #ccc4;
|
background-color: var(--bg-input);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
input:hover {
|
input:hover {
|
||||||
background-color: #8884;
|
background-color: var(--bg-input-hover);
|
||||||
}
|
}
|
||||||
input:active {
|
input:active {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
@ -63,11 +98,11 @@ input:active {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.notification {
|
.notification {
|
||||||
background: #ac8f3944;
|
background: var(--bg-notification);
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
background: red;
|
background: var(--bg-error);
|
||||||
color: white;
|
color: var(--fg-error);
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -76,7 +111,7 @@ input:active {
|
||||||
/** Puzzles list */
|
/** Puzzles list */
|
||||||
.category {
|
.category {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
background: #ccc4;
|
background: var(--bg-category);
|
||||||
}
|
}
|
||||||
.category h2 {
|
.category h2 {
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
@ -101,7 +136,7 @@ nav li, .category li {
|
||||||
float: right;
|
float: right;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #ccc;
|
background: var(--bg-mothball);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
@ -115,8 +150,8 @@ nav li, .category li {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
input:invalid {
|
input:invalid {
|
||||||
background-color: #800;
|
background-color: var(--bg-input-invalid);
|
||||||
color: white;
|
color: var(--fg-input-invalid);
|
||||||
}
|
}
|
||||||
.answer_ok {
|
.answer_ok {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
|
@ -128,8 +163,8 @@ input:invalid {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
background: #cccc;
|
background: var(--bg-debug);
|
||||||
color: black;
|
color: var(--fg-debug);
|
||||||
}
|
}
|
||||||
.debug dt {
|
.debug dt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -173,28 +208,11 @@ 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: #333;
|
background: var(--bg-toast);
|
||||||
color: #eee;
|
color: var(--fg-toast);
|
||||||
box-shadow: 0px 0px 8px 0px #0b0;
|
box-shadow: 0px 0px 8px 0px var(--box-toast);
|
||||||
}
|
}
|
||||||
@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,6 +1,13 @@
|
||||||
{
|
{
|
||||||
"TrackSolved": true,
|
"PuzzleList": {
|
||||||
"Titles": false,
|
"TrackSolved": true,
|
||||||
|
"Titles": false,
|
||||||
|
"": 0
|
||||||
|
},
|
||||||
|
"Puzzle": {
|
||||||
|
"SyntaxHighlighting": true,
|
||||||
|
"": 0
|
||||||
|
},
|
||||||
"Scoreboard": {
|
"Scoreboard": {
|
||||||
"DisplayServerURLWhenEnabled": true,
|
"DisplayServerURLWhenEnabled": true,
|
||||||
"ShowCategoryLeaders": true,
|
"ShowCategoryLeaders": true,
|
||||||
|
@ -8,7 +15,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.TrackSolved)
|
e.classList.toggle("hidden", common.Truthy(e.dataset.trackSolved) != this.config.PuzzleList?.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.TrackSolved) {
|
if (this.config.PuzzleList?.TrackSolved) {
|
||||||
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
a.classList.toggle("solved", this.state.IsSolved(puzzle))
|
||||||
}
|
}
|
||||||
if (this.config.Titles) {
|
if (this.config.PuzzleList?.Titles) {
|
||||||
this.loadTitle(puzzle, i)
|
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">
|
<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>
|
||||||
|
@ -30,5 +31,23 @@
|
||||||
</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,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
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(".")
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ function writeObject(e, obj) {
|
||||||
* @param {string} category
|
* @param {string} category
|
||||||
* @param {number} points
|
* @param {number} points
|
||||||
*/
|
*/
|
||||||
async function loadPuzzle(category, points) {
|
async function loadPuzzle(category, points) {
|
||||||
console.groupCollapsed("Loading puzzle:", category, points)
|
console.groupCollapsed("Loading puzzle:", category, points)
|
||||||
let contentBase = new URL(`content/${category}/${points}/`, common.BaseURL)
|
let contentBase = new URL(`content/${category}/${points}/`, common.BaseURL)
|
||||||
|
|
||||||
|
@ -179,15 +180,41 @@ 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")
|
||||||
a.href = new URL(fn, contentBase)
|
let url = 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")) {
|
||||||
|
@ -199,7 +226,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()
|
||||||
|
|
||||||
|
@ -220,6 +247,9 @@ 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)
|
||||||
|
|
|
@ -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