pupate

No description provided
git clone https://git.woozle.org/neale/pupate.git

pupate / variants / default / web
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}