Neale Pickett
·
2025-02-27
sctr.mjs
1import {Toast} from "./toast.mjs"
2import * as frontmatter from "./frontmatter.mjs"
3
4const confettiPromise = import("https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/+esm")
5 .catch(e => {})
6const commonmarkPromise = import("https://cdn.jsdelivr.net/npm/commonmark@0.31.1/+esm")
7 .catch(e => {})
8
9const Millisecond = 1
10const Second = 1000 * Millisecond
11const Minute = 60 * Second
12const Hour = 60 * Minute
13
14// fill slot elements
15function fill(element, obj) {
16 for (let slot of element.querySelectorAll("slot")) {
17 let name = slot.name
18 let val = obj[name]
19
20 if (name == "modules") {
21 let modulesDoc = new DocumentFragment()
22 for (let module of val) {
23 let li = modulesDoc.appendChild(document.createElement("li"))
24 let a = li.appendChild(document.createElement("a"))
25 a.textContent = `${module.step} - ${module.title}`
26 a.href = `${location.hash}#${module.step}`
27 }
28 slot.replaceWith(modulesDoc)
29 } else if (name == "content") {
30 slot.parentElement.innerHTML = val
31 } else if (Array.isArray(val)) {
32 let doc = new DocumentFragment()
33 for (let item of val) {
34 let li = doc.appendChild(document.createElement("li"))
35 if (slot.dataset.links !== undefined) {
36 let a = li.appendChild(document.createElement("a"))
37 a.textContent = item
38 a.href = item
39 } else {
40 li.textContent = item
41 }
42
43 }
44 slot.replaceWith(doc)
45 } else if (val) {
46 slot.replaceWith(new Text(val))
47 } else {
48 // Leave the default value
49 }
50 }
51}
52
53// Use the MOTH API endpoint to return a list of training IDs.
54async function TrainingIDs() {
55 let resp = await fetch(
56 "-/state/current.json",
57 {
58 cache: "no-cache",
59 },
60 )
61 let obj = await resp.json()
62 return Object.keys(obj.Puzzles)
63}
64
65// Fetch a training description.
66async function Training(trainingID) {
67 let resp = await fetch(
68 `-/puzzles/${trainingID}/training.json`,
69 {
70 cache: "no-cache",
71 },
72 )
73 let training = await resp.json()
74 training.ID = trainingID
75 return training
76}
77
78// Render trainings
79async function RenderAllTrainings() {
80 let trainingsElement = document.querySelector(".traininglist")
81
82 let list = trainingsElement.appendChild(document.createElement("dl"))
83 for (let trainingID of await TrainingIDs()) {
84 let training = await Training(trainingID)
85
86 let dt = list.appendChild(document.createElement("dt"))
87 let a = dt.appendChild(document.createElement("a"))
88 a.textContent = training.title
89 a.href = `#${trainingID}`
90 list.appendChild(document.createElement("dd")).textContent = training.description
91 }
92
93 trainingsElement.classList.remove("hidden")
94}
95
96async function RenderTraining(trainingID) {
97 let trainingElement = document.querySelector(".training")
98
99 let training = await Training(trainingID)
100
101 fill(trainingElement, training)
102 document.title = training.title
103
104 trainingElement.classList.remove("hidden")
105}
106
107async function RenderModule(trainingID, step) {
108 let moduleElement = document.querySelector(".module")
109
110 let training = await Training(trainingID)
111 let module
112 for (let mod of training.modules) {
113 if (mod.step == step) {
114 module = mod
115 }
116 }
117 module.question = module.knowledge_check.question
118
119 let moduleURL = new URL(`/-/puzzles/${trainingID}/${module.path}`, location)
120 let baseURL = new URL(".", moduleURL)
121
122 let resp = await fetch(moduleURL)
123 if (!resp.ok) {
124 RenderError(`${moduleURL}: ${resp.status} ${resp.statusText}`)
125 return
126 }
127 let text = await resp.text()
128
129 // Convert markdown
130 let page = new frontmatter.Doc(text)
131 let urlpfx = page.urlpfx()
132 let content = page.content.replaceAll(urlpfx, "")
133
134 // Render as a pre for speed
135 let pre = document.createElement("pre")
136 pre.textContent = content
137 module.content = pre.outerHTML
138 fill(moduleElement, module)
139
140 // Once we have a parser (which may be immediately),
141 // replace the pre element.
142 commonmarkPromise
143 .then(commonmark => {
144 let parser = new commonmark.Parser()
145 let parsed = parser.parse(content)
146 let writer = new commonmark.HtmlRenderer()
147 document.querySelector(".content").innerHTML = writer.render(parsed)
148 })
149 .catch(err => {
150 Toast("Unable to load markdown parser")
151 })
152
153 document.title = module.title
154
155 // Do after everything else has been fetched!
156 let base = document.querySelector("base") || document.head.appendChild(document.createElement("base"))
157 base.href = baseURL
158
159 document.querySelector("form.answer").addEventListener("submit", e => answerSubmit(e, module.knowledge_check.answers))
160
161 moduleElement.classList.remove("hidden")
162}
163
164function answerSubmit(e, answers) {
165 e.preventDefault()
166 let form = e.target
167 let submittedAnswer = form.elements.namedItem("answer").value
168
169 for (let correct of answers) {
170 if (submittedAnswer == correct) {
171 Toast("Correct answer")
172 confettiPromise
173 .then(c => {
174 c.default({
175 disableForReducedMotion: true,
176 })
177 })
178 .catch(err => {})
179 return
180 }
181 }
182 Toast("Wrong answer")
183}
184
185async function RenderError(message) {
186 let topElement = document.querySelector(".error")
187 topElement.classList.remove("hidden")
188 topElement.appendChild(document.createElement("p")).textContent = message
189}
190
191
192async function init() {
193 window.addEventListener("hashchange", () => location.reload())
194
195 let hashparts = location.hash.split("#")
196 let promise
197 switch (hashparts.length) {
198 case 1:
199 await RenderAllTrainings()
200 break
201 case 2:
202 await RenderTraining(hashparts[1])
203 break
204 case 3:
205 await RenderModule(hashparts[1], hashparts[2])
206 break
207 default:
208 await RenderError("Cannot parse this URL!")
209 break
210 }
211
212 for (let el of document.querySelectorAll(".loading")) {
213 el.classList.add("hidden")
214 }
215}
216
217init()