Neale Pickett
·
2025-03-10
pupate.mjs
1import {Toast} from "./toast.mjs"
2
3const confettiPromise = import("https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm")
4 .catch(err => {}) // It's okay if there's no confetti.
5const hljsPromise = import("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js")
6 .catch(err => {}) // It's okay if the code samples don't look like fruit salad.
7
8const Millisecond = 1
9const Second = 1000 * Millisecond
10const Minute = 60 * Second
11const Hour = 60 * Minute
12
13
14async function RenderCategoryList(server) {
15 let resp = await fetch("-/state/current.json", {cache: "no-cache"})
16 let state = await resp.json()
17
18 document.title = "Categories"
19 let topElement = document.querySelector(".categorylist")
20 let categories = topElement.appendChild(document.createElement("div"))
21 categories.classList.add("categories")
22
23 RenderErrors(topElement, state)
24
25 for (let catid in state.Puzzles) {
26 let div = categories.appendChild(document.createElement("div"))
27 let title = div.appendChild(document.createElement("h1"))
28 let titleLink = title.appendChild(document.createElement("a"))
29 let desc = div.appendChild(document.createElement("p"))
30 let puzzles = div.appendChild(document.createElement("ul"))
31
32 div.classList.add("category")
33 titleLink.textContent = catid
34 titleLink.href = (`#${catid}`)
35 puzzles.classList.add("puzzles")
36
37 fetch(`-/puzzles/${catid}/index.html`, {cache: "no-cache"})
38 .then(resp => resp.text())
39 .then(html => {
40 let page = (new DOMParser()).parseFromString(html, "text/html")
41 titleLink.textContent = page.title || catid
42 desc.textContent = page.querySelector("meta[name='moth description']")?.content || ""
43 })
44
45 for (let puzzleid of state.Puzzles[catid]) {
46 let li = puzzles.appendChild(document.createElement("li"))
47 let a = li.appendChild(document.createElement("a"))
48 a.textContent = puzzleid
49 a.href = `#${catid}#${puzzleid}`
50 }
51 }
52
53 topElement.classList.remove("hidden")
54}
55
56function RenderErrors(topElement, state) {
57 for (let errors of topElement.querySelectorAll(".errors")) {
58 errors.replaceChildren()
59 if (state.Errors.length == 0) {
60 errors.classList.add("hidden")
61 continue
62 }
63 errors.classList.remove("hidden")
64 errors.appendChild(document.createElement("summary")).textContent = `${state.Errors.length} errors`
65
66 for (let err of state.Errors) {
67 errors.appendChild(document.createElement("pre")).textContent = err
68 }
69 }
70}
71
72async function RenderCategory(catid) {
73 let resp = await fetch("-/state/current.json", {cache: "no-cache"})
74 let state = await resp.json()
75
76 document.title = catid
77 let topElement = document.querySelector(".category")
78
79 RenderErrors(topElement, state)
80
81 let title = topElement.querySelector("slot[name=Title]")
82 title.classList.add("title")
83 title.textContent = document.title
84
85 let desc = topElement.appendChild(document.createElement("p"))
86 let puzzlesContainer = topElement.querySelector("slot[name=Puzzles]")
87 let puzzles = puzzlesContainer.appendChild(document.createElement("dl"))
88
89 fetch(`-/puzzles/${catid}/index.html`, {cache: "no-cache"})
90 .then(resp => resp.text())
91 .then(html => {
92 let page = (new DOMParser()).parseFromString(html, "text/html")
93 document.title = page.title || catid
94 title.textContent = page.title || catid
95 for (let slot of topElement.querySelectorAll("slot[name=Description]")) {
96 slot.textContent = page.querySelector("meta[name='moth description']")?.content || ""
97 }
98 for (let slot of topElement.querySelectorAll("slot[name=Content]")) {
99 slot.replaceChildren(...page.body.childNodes)
100 }
101 })
102
103 for (let puzzleid of state.Puzzles[catid]) {
104 let dt = puzzles.appendChild(document.createElement("dt"))
105 let a = dt.appendChild(document.createElement("a"))
106 a.textContent = puzzleid
107 a.href = `#${catid}#${puzzleid}`
108
109 let dd = puzzles.appendChild(document.createElement("dd"))
110 fetch(`-/puzzles/${catid}/${puzzleid}/index.html`, {cache: "no-cache"})
111 .then(resp => resp.text())
112 .then(html => {
113 let page = (new DOMParser()).parseFromString(html, "text/html")
114 a.textContent = `${puzzleid} - ${page.title}`
115 let summary = page.querySelector("meta[name='moth debug summary]")?.content
116 if (summary) {
117 dd.textContent = summary
118 dd.classList.add("summary")
119 }
120 })
121 }
122
123 topElement.classList.remove("hidden")
124}
125
126async function RenderPuzzle(cat, puzzleid) {
127 let topElement = document.querySelector(".puzzle")
128 let pageURL = new URL(`-/puzzles/${cat}/${puzzleid}/index.html`, location)
129 let page
130
131 try {
132 let resp = await fetch(pageURL, {cache: "no-cache"})
133 let html = await resp.text()
134 page = (new DOMParser()).parseFromString(html, "text/html")
135 }
136 catch (err) {
137 console.error(err)
138 if (err.Payload) {
139 RenderError(`${err.Status} ${err.StatusText}`)
140 RenderError(err.Payload)
141 } else {
142 RenderError(err)
143 }
144 return
145 }
146
147 document.title = page.title
148
149 let answers = []
150 for (let meta of page.querySelectorAll("meta[name='moth debug answer']")) {
151 answers.push(meta.content)
152 }
153
154 let answerhashes = []
155 for (let meta of page.querySelectorAll("meta[name='moth answerhash']")) {
156 answerhashes.push(meta.content)
157 }
158
159 document.addEventListener("setAnswer", event => {
160 for (let el of document.querySelectorAll("input[name=answer]")) {
161 el.value = event.detail.value
162 el.dispatchEvent(new Event("input"))
163 }
164 })
165
166 for (let form of document.querySelectorAll("form.answer")) {
167 for (let input of form.querySelectorAll("input[name=answer]")) {
168 input.addEventListener("input", async event => {
169 let msgUint8 = new TextEncoder().encode(input.value)
170 let hashBuffer = await crypto.subtle.digest("SHA-1", msgUint8)
171 let hashArray = Array.from(new Uint8Array(hashBuffer))
172 let hexits = hashArray.map(b => b.toString(16).padStart(2, "0")).join("")
173 let hash = hexits.slice(0, 4)
174
175 let correct = false
176 for (let ah of answerhashes) {
177 if (ah == hash) {
178 correct = true
179 }
180 }
181 form.classList.toggle("match", correct)
182 form.classList.toggle("no-match", !correct)
183 })
184 let answerPattern = page.querySelector("meta[name='moth answerpattern']")?.content
185 if (answerPattern) {
186 input.pattern = answerPattern
187 }
188 }
189
190 form.addEventListener("submit", event => {
191 event.preventDefault()
192 let submittedAnswer = form.elements.namedItem("answer").value
193 if (answers.length == 0) {
194 Toast("No answers: did you run pupate with -unsafe?")
195 }
196 for (let answer of answers) {
197 if (submittedAnswer == answer) {
198 Toast("Correct answer")
199 confettiPromise
200 .then(c => {
201 c.default({
202 disableForReducedMotion: true,
203 })
204 })
205 .catch(err => {})
206 return
207 }
208 }
209 Toast("Wrong answer")
210 })
211 }
212
213 for (let script of page.head.querySelectorAll("script[src]")) {
214 // One day I would like to understand why I can't just append the script to document.head.
215 let ns = document.head.appendChild(document.createElement("script"))
216 ns.src = new URL(script.getAttribute("src"), pageURL)
217 ns.type = script.type
218 }
219
220 for (let slot of topElement.querySelectorAll("slot[name=Title]")) {
221 slot.textContent = page.title
222 }
223 for (let slot of topElement.querySelectorAll("slot[name=Question]")) {
224 let question = page.querySelector("meta[name='moth question']")?.content
225 if (question) {
226 slot.textContent = question
227 }
228 }
229 for (let slot of topElement.querySelectorAll("slot[name=Content]")) {
230 slot.replaceChildren(...page.body.childNodes)
231 }
232 for (let slot of topElement.querySelectorAll("slot[name=Authors]")) {
233 slot.replaceChildren()
234 for (let authorMeta of page.querySelectorAll("meta[name=author]")) {
235 slot.appendChild(document.createElement("li")).textContent = authorMeta.content
236 }
237 }
238 for (let slot of topElement.querySelectorAll("slot[name=Attachments]")) {
239 slot.replaceChildren()
240 for (let attachmentMeta of page.querySelectorAll("meta[name='moth attachment']")) {
241 let li = slot.appendChild(document.createElement("li"))
242 let a = li.appendChild(document.createElement("a"))
243 a.textContent = attachmentMeta.content
244 a.href = new URL(attachmentMeta.content, pageURL)
245 }
246 }
247 for (let slot of topElement.querySelectorAll("slot[name=Debug]")) {
248 slot.replaceChildren()
249 let values = {}
250 for (let meta of page.querySelectorAll("meta[name~=moth]")) {
251 let k = meta.name.replace("moth", "").trim()
252 let ul = values[k]
253 if (!ul) {
254 slot.appendChild(document.createElement("dt")).textContent = k
255 let dd = slot.appendChild(document.createElement("dd"))
256 ul = dd.appendChild(document.createElement("ul"))
257 values[k] = ul
258 }
259 ul.appendChild(document.createElement('li')).textContent = meta.content
260 }
261 slot.parentElement.classList.remove("hidden")
262 }
263
264 // Do after everything else has been fetched!
265 let base = document.querySelector("base") || document.head.appendChild(document.createElement("base"))
266 base.href = new URL("./", pageURL)
267 topElement.classList.remove("hidden")
268
269 hljsPromise.then(hljs => {
270 for (let pre of document.querySelectorAll("code[class|=language]"))
271 hljs.default.highlightAll()
272 })
273}
274
275async function RenderError(message) {
276 let topElement = document.querySelector(".error")
277 topElement.classList.remove("hidden")
278 topElement.appendChild(document.createElement("p")).textContent = message
279}
280
281function init() {
282 window.addEventListener("hashchange", () => {location.reload()})
283 let hashparts = location.hash.split("#")
284 switch (hashparts.length) {
285 case 0:
286 case 1:
287 RenderCategoryList()
288 break
289 case 2:
290 RenderCategory(hashparts[1])
291 break
292 case 3:
293 RenderPuzzle(hashparts[1], hashparts[2])
294 break
295 default:
296 RenderError("Cannot parse this URL!")
297 break
298 }
299
300 for (let el of document.querySelectorAll(".loading")) {
301 el.classList.add("hidden")
302 }
303}
304
305
306if (document.readyState === "loading") {
307 document.addEventListener("DOMContentLoaded", init)
308} else {
309 init()
310}