@@ -199,6 +199,78 @@ function retrieveAttempt() {
199
199
return result ;
200
200
}
201
201
202
+ const attemptIdPattern = / ^ a t t e m p t ( \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
+
202
274
/**
203
275
* Check the document's user input "attempt" to see if matches "correct".
204
276
* Then set "grade" in document depending on that answer.
@@ -226,27 +298,34 @@ function runCheck() {
226
298
// This makes it easy to detect someone simply copying a final result.
227
299
correctStamp = document . getElementById ( 'correctStamp' ) ;
228
300
if ( correctStamp ) {
229
- let timeStamp = ( new Date ( ) ) . toISOString ( ) ;
230
- let uuid = crypto . randomUUID ( ) ;
231
- correctStamp . innerHTML = `${ timeStamp } ${ uuid } ` ;
301
+ correctStamp . innerHTML = makeStamp ( ) ;
232
302
}
233
303
234
304
// Use a timeout so the underlying page will *re-render* before the
235
305
// alert shows. If we don't do this, the alert would be confusing
236
306
// because the underlying page would show that it wasn't completed.
237
307
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 ) ;
239
315
} , 100 ) ;
240
316
}
241
317
}
242
318
243
319
/** Return the best-matching hint string given an attempt.
244
320
* @attempt - array of strings of attempt to give hints on
245
321
*/
246
- function findHint ( attempt ) {
322
+ function findHint ( attempt , validIndexes = undefined ) {
247
323
// Find a matching hint (matches present and NOT absent)
248
324
for ( hint of hints ) {
249
- if ( ( ! hint . presentRe ||
325
+ if (
326
+ ( ( validIndexes === undefined ) ||
327
+ ( validIndexes . includes ( hint . index ) ) ) &&
328
+ ( ! hint . presentRe ||
250
329
hint . presentRe . test ( attempt [ hint . index ] ) ) &&
251
330
( ! hint . absentRe ||
252
331
! hint . absentRe . test ( attempt [ hint . index ] ) ) ) {
@@ -257,19 +336,24 @@ function findHint(attempt) {
257
336
}
258
337
259
338
/** 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}`);
261
342
let attempt = retrieveAttempt ( ) ;
262
343
if ( calcMatch ( attempt , correctRe ) ) {
263
344
alert ( 'The answer is already correct!' ) ;
264
345
} else if ( ! hints ) {
265
346
alert ( 'Sorry, there are no hints for this lab.' ) ;
266
347
} else {
267
- alert ( findHint ( attempt ) ) ;
348
+ let validIndexes = findIndexes ( e . target . form ) ;
349
+ alert ( findHint ( attempt , validIndexes ) ) ;
268
350
}
269
351
}
270
352
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 } ` ) ;
273
357
}
274
358
275
359
/**
@@ -279,8 +363,8 @@ function showAnswer() {
279
363
* had correctly answered it, and we reset, then we need to show
280
364
* the visual indicators that it's no longer correctly answered.
281
365
*/
282
- function resetForm ( ) {
283
- form = document . getElementById ( 'lab' ) ;
366
+ function resetForm ( e ) {
367
+ form = e . target . form ;
284
368
form . reset ( ) ;
285
369
runCheck ( ) ;
286
370
}
@@ -445,7 +529,7 @@ function runSelftest() {
445
529
let testAttempt = expected . slice ( ) ; // shallow copy of expected
446
530
testAttempt [ hint . index ] = example ;
447
531
// What hint does our new testAttempt give?
448
- actualHint = findHint ( testAttempt ) ;
532
+ actualHint = findHint ( testAttempt , [ hint . index ] ) ;
449
533
if ( actualHint != hint . text ) {
450
534
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 ) } ` ) ;
451
535
} ;
@@ -517,23 +601,20 @@ function initPage() {
517
601
attempt . oninput = runCheck ;
518
602
current ++ ;
519
603
}
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 ) ; } ) ;
523
606
if ( ! hintButton . title ) {
524
607
hintButton . title = 'Provide a hint given current attempt.' ;
525
- }
608
+ }
526
609
}
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 ) ; } ) ;
530
612
if ( ! resetButton . title ) {
531
613
resetButton . title = 'Reset initial state (throwing away current attempt).' ;
532
614
}
533
615
}
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 ) ; } ) ;
537
618
if ( ! giveUpButton . title ) {
538
619
giveUpButton . title = 'Give up and show an answer.' ;
539
620
}
0 commit comments