Skip to content

Commit 22012a3

Browse files
Merge pull request #441 from ossf/lab_extension
Lab extension
2 parents 56d758c + 91ec38d commit 22012a3

File tree

7 files changed

+562
-36
lines changed

7 files changed

+562
-36
lines changed

docs/labs/checker.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ pre input, pre textarea {
1616
.displayNone {
1717
display: none;
1818
}
19+
20+
table, th, td {
21+
border: 1px solid black;
22+
}
23+

docs/labs/checker.js

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,78 @@ function retrieveAttempt() {
199199
return result;
200200
}
201201

202+
const attemptIdPattern = /^attempt(\d+)$/;
203+
204+
/*
205+
* Given Node @form in document, return array of indexes of input/textareas
206+
*/
207+
function findIndexes(form) {
208+
let inputs = form.querySelectorAll(
209+
"input[type='text']:not(:read-only),textarea:not(:read-only)");
210+
if (!inputs) {
211+
// Shouldn't happen. Reaching this means the current form has no inputs.
212+
// We'll do a "reasonable thing" - act as if all is in scope.
213+
return correctRe.map((_, i) => i);
214+
} else {
215+
let result = [];
216+
// Turn "approach0", "approach1" into [0, 1].
217+
for (input of inputs) {
218+
// alert(`findIndexes: ${input.id}`);
219+
let matchResult = input.id.match(attemptIdPattern);
220+
if (matchResult) {
221+
let index = Number(matchResult[1]);
222+
result.push(index);
223+
}
224+
}
225+
// alert(`findIndexes = ${result}`);
226+
return result;
227+
}
228+
}
229+
230+
/** Compute cyrb53 non-cryptographic hash */
231+
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
232+
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
233+
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
234+
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
235+
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
236+
// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
237+
const cyrb64 = (str, seed = 0) => {
238+
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
239+
for(let i = 0, ch; i < str.length; i++) {
240+
ch = str.charCodeAt(i);
241+
h1 = Math.imul(h1 ^ ch, 2654435761);
242+
h2 = Math.imul(h2 ^ ch, 1597334677);
243+
}
244+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
245+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
246+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
247+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
248+
// For a single 53-bit numeric return value we could return
249+
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
250+
// but we instead return the full 64-bit value:
251+
return [h2>>>0, h1>>>0];
252+
};
253+
254+
// An improved, *insecure* 64-bit hash that's short, fast, and has no dependencies.
255+
// Output is always 14 characters.
256+
// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
257+
const cyrb64Hash = (str, seed = 0) => {
258+
const [h2, h1] = cyrb64(str, seed);
259+
return h2.toString(36).padStart(7, '0') + h1.toString(36).padStart(7, '0');
260+
}
261+
262+
/** Create a stamp to indicate completion. */
263+
function makeStamp() {
264+
let timeStamp = (new Date()).toISOString();
265+
let uuid = crypto.randomUUID();
266+
let resultBeginning = `${timeStamp} ${uuid}`;
267+
// Browsers have a SHA-256 cryptographic hash available, but *only*
268+
// when they're in a "secure state". We don't need the hash to be
269+
// cryptographic, since the data is clearly in view. Use a simple one.
270+
let hash = cyrb64Hash(resultBeginning);
271+
return `${resultBeginning} ${hash}`;
272+
}
273+
202274
/**
203275
* Check the document's user input "attempt" to see if matches "correct".
204276
* Then set "grade" in document depending on that answer.
@@ -226,27 +298,34 @@ function runCheck() {
226298
// This makes it easy to detect someone simply copying a final result.
227299
correctStamp = document.getElementById('correctStamp');
228300
if (correctStamp) {
229-
let timeStamp = (new Date()).toISOString();
230-
let uuid = crypto.randomUUID();
231-
correctStamp.innerHTML = `${timeStamp} ${uuid}`;
301+
correctStamp.innerHTML = makeStamp();
232302
}
233303

234304
// Use a timeout so the underlying page will *re-render* before the
235305
// alert shows. If we don't do this, the alert would be confusing
236306
// because the underlying page would show that it wasn't completed.
237307
setTimeout(function() {
238-
alert('Congratulations! Your answer is correct!');
308+
let congrats_text;
309+
if (correctRe.length > 1) {
310+
congrats_text = 'Great work! All your answers are correct!';
311+
} else {
312+
congrats_text = 'Congratulations! Your answer is correct!';
313+
}
314+
alert(congrats_text);
239315
}, 100);
240316
}
241317
}
242318

243319
/** Return the best-matching hint string given an attempt.
244320
* @attempt - array of strings of attempt to give hints on
245321
*/
246-
function findHint(attempt) {
322+
function findHint(attempt, validIndexes = undefined) {
247323
// Find a matching hint (matches present and NOT absent)
248324
for (hint of hints) {
249-
if ((!hint.presentRe ||
325+
if (
326+
((validIndexes === undefined) ||
327+
(validIndexes.includes(hint.index))) &&
328+
(!hint.presentRe ||
250329
hint.presentRe.test(attempt[hint.index])) &&
251330
(!hint.absentRe ||
252331
!hint.absentRe.test(attempt[hint.index]))) {
@@ -257,19 +336,24 @@ function findHint(attempt) {
257336
}
258337

259338
/** Show a hint to the user. */
260-
function showHint() {
339+
function showHint(e) {
340+
// Get data-indexes value using e.target.dataset.indexes
341+
// alert(`Form id = ${e.target.form.id}`);
261342
let attempt = retrieveAttempt();
262343
if (calcMatch(attempt, correctRe)) {
263344
alert('The answer is already correct!');
264345
} else if (!hints) {
265346
alert('Sorry, there are no hints for this lab.');
266347
} else {
267-
alert(findHint(attempt));
348+
let validIndexes = findIndexes(e.target.form);
349+
alert(findHint(attempt, validIndexes));
268350
}
269351
}
270352

271-
function showAnswer() {
272-
alert(`We were expecting an answer like this:\n${expected.join('\n\n')}`);
353+
function showAnswer(e) {
354+
let formIndexes = findIndexes(e.target.form); // Indexes in this form
355+
let goodAnswer = formIndexes.map(i => expected[i]).join('\n\n');
356+
alert(`We were expecting an answer like this:\n${goodAnswer}`);
273357
}
274358

275359
/**
@@ -279,8 +363,8 @@ function showAnswer() {
279363
* had correctly answered it, and we reset, then we need to show
280364
* the visual indicators that it's no longer correctly answered.
281365
*/
282-
function resetForm() {
283-
form = document.getElementById('lab');
366+
function resetForm(e) {
367+
form = e.target.form;
284368
form.reset();
285369
runCheck();
286370
}
@@ -445,7 +529,7 @@ function runSelftest() {
445529
let testAttempt = expected.slice(); // shallow copy of expected
446530
testAttempt[hint.index] = example;
447531
// What hint does our new testAttempt give?
448-
actualHint = findHint(testAttempt);
532+
actualHint = findHint(testAttempt, [hint.index]);
449533
if (actualHint != hint.text) {
450534
alert(`Lab Error: Unexpected hint!\n\nExample:\n${example}\n\nExpected hint:\n${hint.text}\n\nProduced hint:\n${actualHint}\n\nExpected (passing example)=${JSON.stringify(expected)}\n\ntestAttempt=${JSON.stringify(testAttempt)}\nFailing hint=${JSON.stringify(hint)}`);
451535
};
@@ -517,23 +601,20 @@ function initPage() {
517601
attempt.oninput = runCheck;
518602
current++;
519603
}
520-
hintButton = document.getElementById('hintButton');
521-
if (hintButton) {
522-
hintButton.onclick = (() => showHint());
604+
for (let hintButton of document.querySelectorAll("button.hintButton")) {
605+
hintButton.addEventListener('click', (e) => { showHint(e); });
523606
if (!hintButton.title) {
524607
hintButton.title = 'Provide a hint given current attempt.';
525-
}
608+
}
526609
}
527-
resetButton = document.getElementById('resetButton');
528-
if (resetButton) {
529-
resetButton.onclick = (() => resetForm());
610+
for (let resetButton of document.querySelectorAll("button.resetButton")) {
611+
resetButton.addEventListener('click', (e) => { resetForm(e); });
530612
if (!resetButton.title) {
531613
resetButton.title = 'Reset initial state (throwing away current attempt).';
532614
}
533615
}
534-
giveUpButton = document.getElementById('giveUpButton');
535-
if (giveUpButton) {
536-
giveUpButton.onclick = (() => showAnswer());
616+
for (let giveUpButton of document.querySelectorAll("button.giveUpButton")) {
617+
giveUpButton.addEventListener('click', (e) => { showAnswer(e); });
537618
if (!giveUpButton.title) {
538619
giveUpButton.title = 'Give up and show an answer.';
539620
}

docs/labs/csp1.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,9 @@ <h2>Interactive Lab (<span id="grade"></span>)</h2>
405405

406406
app.listen(3000);
407407
</code></pre>
408-
<button type="button" id="hintButton">Hint</button>
409-
<button type="button" id="resetButton">Reset</button>
410-
<button type="button" id="giveUpButton">Give up</button>
408+
<button type="button" class="hintButton">Hint</button>
409+
<button type="button" class="resetButton">Reset</button>
410+
<button type="button" class="giveUpButton">Give up</button>
411411
<br><br>
412412
<pre id="correctStamp"></pre>
413413
<textarea id="debugData" class="displayNone" rows="20" cols="70" readonly>

docs/labs/hello.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ <h2>Interactive Lab (<span id="grade"></span>)</h2>
166166
<input id="attempt0" type="text" size="70" spellcheck="false"
167167
value='console.log("Goodbye.");'>
168168
</code></pre>
169-
<button type="button" id="hintButton">Hint</button>
170-
<button type="button" id="resetButton">Reset</button>
171-
<button type="button" id="giveUpButton">Give up</button>
169+
<button type="button" class="hintButton">Hint</button>
170+
<button type="button" class="resetButton">Reset</button>
171+
<button type="button" class="giveUpButton">Give up</button>
172172
<br><br>
173173
<pre id="correctStamp"></pre>
174174
<textarea id="debugData" class="displayNone" rows="20" cols="70" readonly>

docs/labs/input1.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,10 @@ <h2>Interactive Lab (<span id="grade"></span>)</h2>
222222
res.status(422).send(`Invalid input`);
223223
})
224224
</code></pre>
225-
<button type="button" id="hintButton">Hint</button>
226-
<button type="button" id="resetButton">Reset</button>
227-
<button type="button" id="giveUpButton">Give up</button>
228-
<br><br>
225+
<button type="button" class="hintButton">Hint</button>
226+
<button type="button" class="resetButton">Reset</button>
227+
<button type="button" class="giveUpButton">Give up</button>
228+
<br><br><!-- These go in the last form if there's more than one: -->
229229
<pre id="correctStamp"></pre>
230230
<textarea id="debugData" class="displayNone" rows="20" cols="70" readonly>
231231
</textarea>

docs/labs/input2.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,9 @@ <h2>Interactive Lab (<span id="grade"></span>)</h2>
213213
res.status(422).send(`Invalid input`);
214214
})
215215
</code></pre>
216-
<button type="button" id="hintButton">Hint</button>
217-
<button type="button" id="resetButton">Reset</button>
218-
<button type="button" id="giveUpButton">Give up</button>
216+
<button type="button" class="hintButton">Hint</button>
217+
<button type="button" class="resetButton">Reset</button>
218+
<button type="button" class="giveUpButton">Give up</button>
219219
<br><br>
220220
<pre id="correctStamp"></pre>
221221
<textarea id="debugData" class="displayNone" rows="20" cols="70" readonly>

0 commit comments

Comments
 (0)