Skip to content

Commit bb1a408

Browse files
Merge pull request #786 from ossf/hint_all_correct
Once it's all correct, asking for a hint verifies this
2 parents 4a77e39 + 3c50c6f commit bb1a408

File tree

1 file changed

+121
-82
lines changed

1 file changed

+121
-82
lines changed

docs/labs/checker.js

Lines changed: 121 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,25 @@ let user_gave_up = false; // True if user ever gave up before user solved it
1818

1919
let startTime = Date.now(); // Time this lab started.
2020
let lastHintTime = null; // Last time we showed a hint.
21+
let lastHintTarget = null; // Last hint button user used.
2122

22-
// Has the input changed since we showed a hint?
23-
// We track this so people can re-see a hint they've already seen.
24-
// This initial value of "true" forces users to wait a delay time before
25-
// they are allowed to see their first hint on an unchanged page.
23+
/**
24+
* True iff the input has changed since we showed a hint.
25+
* We track this so people can re-see a hint they've already seen.
26+
* This initial value of "true" forces users to wait a delay time before
27+
* they are allowed to see their first hint on an unchanged page. */
2628
let changedInputSinceHint = true;
2729

2830
let BACKQUOTE = "`"; // Make it easy to use `${BACKQUOTE}`
2931
let DOLLAR = "$"; // Make it easy to use `${DOLLAR}`
3032

31-
// Current language. Guess English until we learn otherwise.
33+
/** Current language. Guess English until we learn otherwise. */
3234
let lang = "en";
3335

36+
/**
37+
* Resources for localizations, in particular, for translations.
38+
* This intentionally mimics the format of library i18next.
39+
*/
3440
const resources = {
3541
en: {
3642
translation: {
@@ -89,7 +95,8 @@ const resources = {
8995
},
9096
};
9197

92-
/** Provide an "assert" (JavaScript doesn't have one built-in).
98+
/**
99+
* Provide an "assert" function (JavaScript doesn't have one built-in).
93100
* This one uses "Error" to provide a stack trace.
94101
*/
95102
function myAssert(condition, message) {
@@ -98,13 +105,17 @@ function myAssert(condition, message) {
98105
}
99106
}
100107

101-
// Format a string, replacing {NUM} with item NUM.
102-
// We use this function to simplify internationalization.
103-
// Use as: myFormat("Demo {0} result", ["Name"])
104-
// https://www.geeksforgeeks.org/what-are-the-equivalent-of-printf-string-format-in-javascript/
105-
// This is *not* set as a property on String; if we did that,
106-
// we'd modify the global namespace, possibly messing up something
107-
// already there.
108+
/**
109+
* Format a string, replacing {NUM} with item NUM.
110+
* We use this function to simplify internationalization.
111+
* Use as: myFormat("Demo {0} result", ["Name"]). See:
112+
* https://www.geeksforgeeks.org/what-are-the-equivalent-of-printf-string-format-in-javascript/
113+
* This is *not* set as a property on String; if we did that,
114+
* we'd modify the global namespace, possibly messing up something
115+
* already there.
116+
* @param s {string} - string to format, where {NUM} is to be replaced
117+
* @param replacements {Array} - Array of strings for replacements
118+
*/
108119
function myFormat(s, replacements) {
109120
return s.replace(/{(\d+)}/g, function (match, number) {
110121
return typeof replacements[number] != 'undefined'
@@ -118,7 +129,10 @@ myAssert(myFormat("Hello", []) === "Hello");
118129
myAssert(myFormat("Hello {0}, are you {1}?", ["friend", "well"]) ===
119130
"Hello friend, are you well?");
120131

121-
// Retrieve translation for given key from resources.
132+
/** Retrieve translation for given key from resources.
133+
* @param key {string} - key to be retrieved
134+
* @returns {string} - translated key for the current local `lang`
135+
*/
122136
function t(key) {
123137
let result = resources[lang]['translation'][key];
124138

@@ -128,7 +142,8 @@ function t(key) {
128142
return result;
129143
}
130144

131-
// Retrieve translation from object for given field
145+
/** Retrieve translation from object for given field
146+
*/
132147
function retrieve_t(obj, field) {
133148
let result = obj[field + "_" + lang];
134149

@@ -138,7 +153,7 @@ function retrieve_t(obj, field) {
138153
return result;
139154
}
140155

141-
// Determine language of document. Set with <html lang="...">.
156+
/** Return language of document. Set with <html lang="...">. */
142157
function determine_locale() {
143158
let lang = document.documentElement.lang;
144159
if (!lang) {
@@ -147,25 +162,27 @@ function determine_locale() {
147162
return lang;
148163
}
149164

150-
// This array contains the default pattern preprocessing commands, in order.
151-
// We process every pattern through these (in order) to create a final regex
152-
// to be used to match a pattern.
153-
//
154-
// We preprocess regexes to (1) simplify the pattern language and
155-
// (2) optimize performance.
156-
// Each item in this array has two elements:
157-
// a regex and its replacement string on match.
158-
// Yes, these preprocess patterns are regexes that process regexes.
159-
//
160-
// People can instead define their *own* sequence of
161-
// preprocessing commands, to make their language easier to handle
162-
// (e.g., Python). Do this by setting `info.preprocessing`.
163-
// Its format is a sequence of arrays, each element is an array of
164-
// 2 or 3 strings of form pattern, replacementString [, flags]
165-
//
166-
// Our default pattern preprocessing commands include some optimizations;
167-
// we want people to get rapid feedback even with complex correct patterns.
168-
//
165+
/**
166+
* Array that containing the pattern preprocessing commands, in order.
167+
* We set it to its default value to start with.
168+
* We process every pattern through these (in order) to create a final regex
169+
* to be used to match a pattern.
170+
*
171+
* We preprocess regexes to (1) simplify the pattern language and
172+
* (2) optimize performance.
173+
* Each item in this array has two elements:
174+
* a regex and its replacement string on match.
175+
* Yes, these preprocess patterns are regexes that process regexes.
176+
*
177+
* People can instead define their *own* sequence of
178+
* preprocessing commands, to make their language easier to handle
179+
* (e.g., Python). Do this by setting `info.preprocessing`.
180+
* Its format is a sequence of arrays, each element is an array of
181+
* 2 or 3 strings of form pattern, replacementString [, flags]
182+
*
183+
* Our default pattern preprocessing commands include some optimizations;
184+
* we want people to get rapid feedback even with complex correct patterns.
185+
*/
169186
let preprocessRegexes = [
170187
// Remove end-of-line characters (\n and \r)
171188
[/[\n\r]+/g, ''],
@@ -217,9 +234,9 @@ function escapeHTML(unsafe) {
217234
.replace(/\'/g, "&#039;"));
218235
}
219236

220-
/* Compute Set difference lhs \ rhs.
221-
* @lhs - Set to start with
222-
* @rhs - Set to remove from the lhs
237+
/** Compute Set difference lhs \ rhs.
238+
* @param lhs - Set to start with
239+
* @param rhs - Set to remove from the lhs
223240
* Set difference is in Firefox nightly, but is not yet released.
224241
* So we compute it ourselves. This is equivalent to lhs.difference(rhs)
225242
*/
@@ -229,16 +246,17 @@ function setDifference(lhs, rhs) {
229246
return new Set(result);
230247
}
231248

232-
/* Return differences between two objects
249+
/** Return differences between two objects
250+
* (this is useful for debugging)
233251
*/
234252
function objectDiff(obj1, obj2) {
235253
let diff = {};
236-
254+
237255
function compare(obj1, obj2, path = '') {
238256
for (const key in obj1) {
239257
if (obj1.hasOwnProperty(key)) {
240258
const newPath = path ? `${path}.${key}` : key;
241-
259+
242260
if (!obj2.hasOwnProperty(key)) {
243261
diff[newPath] = [obj1[key], undefined];
244262
} else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
@@ -248,23 +266,23 @@ function objectDiff(obj1, obj2) {
248266
}
249267
}
250268
}
251-
269+
252270
for (const key in obj2) {
253271
if (obj2.hasOwnProperty(key) && !obj1.hasOwnProperty(key)) {
254272
const newPath = path ? `${path}.${key}` : key;
255273
diff[newPath] = [undefined, obj2[key]];
256274
}
257275
}
258276
}
259-
277+
260278
compare(obj1, obj2);
261279
return diff;
262280
}
263281

264-
/*
282+
/**
265283
* Show debug output in debug region and maybe via alert box
266-
* @debugOutput - the debug information to show
267-
* @alwaysAlert - if true, ALWAYS show an alert
284+
* @param debugOutput - the debug information to show
285+
* @param alwaysAlert - if true, ALWAYS show an alert
268286
* This does *not* raise or re-raise an exception; it may be just informative.
269287
*/
270288
function showDebugOutput(debugOutput, alwaysAlert = true) {
@@ -289,8 +307,8 @@ function showDebugOutput(debugOutput, alwaysAlert = true) {
289307
* Given take a regex string, preprocess it (using our array of
290308
* definitions and preprocessing regexes),
291309
* and return a processed regex as a String.
292-
* @regexString - String to be converted into a compiled Regex
293-
* @fullMatch - require full match (insert "^" at beginning, "$" at end).
310+
* @param {string} regexString - String to be converted into a compiled Regex
311+
* @param {Boolean} fullMatch - require full match ("^" at start, "$" at end)
294312
*/
295313
function processRegexToString(regexString, fullMatch = true) {
296314
// Replace all definitions. This makes regexes much easier to use,
@@ -313,9 +331,9 @@ function processRegexToString(regexString, fullMatch = true) {
313331
/**
314332
* Given take a regex string, preprocess it (using our array of
315333
* preprocessing regexes), and return a final compiled Regexp.
316-
* @regexString - String to be converted into a compiled Regexp
317-
* @description - Description of @regexString's purpose (for error reports)
318-
* @fullMatch - require full match (insert "^" at beginning, "$" at end).
334+
* @param {String} regexString - String to be converted into a compiled Regexp
335+
* @param description - Description of its purpose (for error reports)
336+
* @param fullMatch - require full match? (insert "^" at start, "$" at end).
319337
*/
320338
function processRegex(regexString, description, fullMatch = true) {
321339
let processedRegexString = processRegexToString(regexString, fullMatch);
@@ -330,9 +348,9 @@ function processRegex(regexString, description, fullMatch = true) {
330348
}
331349
}
332350

333-
/*
351+
/**
334352
* Determine if preprocessing produces the expected final regex answer.
335-
* @example - 2-element array. LHS is to be processed, RHS is expected result
353+
* @param example - 2-element array [to be processed, expected result]
336354
*/
337355
function validProcessing(example) {
338356
let [unProcessed, expectedProcessed] = example;
@@ -344,27 +362,32 @@ function validProcessing(example) {
344362

345363
/**
346364
* Return true iff the indexed attempt matches the indexed correct.
347-
* @attempt - Array of strings that might be correct
348-
* @index - Integer index (0+)
349-
* @correct - Array of compiled regexes describing correct answer
365+
* @param attempt - Array of strings that might be correct
366+
* @param index - Integer index (0+)
367+
* @param correct - Array of compiled regexes describing correct answer
350368
*/
351369
function calcOneMatch(attempt, index = 0, correct = correctRe) {
352370
return correct[index].test(attempt[index]);
353371
}
354372

355373
/**
356374
* Return true iff all of attempt matches all of correct.
357-
* @attempt - Array of strings that might be correct
358-
* @correct - Array of compiled regexes describing correct answer
375+
* @param attempt - Array of strings that might be correct
376+
* @param correct - Array of compiled regexes describing correct answer
377+
* @param validIndexes - Array of indexes to check (default: all indexes)
359378
*/
360-
function calcMatch(attempt, correct = correctRe) {
379+
function calcMatch(attempt, correct = correctRe, validIndexes = null) {
361380
if (!correct) { // Defensive test, should never happen.
362381
alert('Error: Internal failure, correct value not defined or empty.');
363382
return false;
364383
}
365384
for (let i = 0; i < correct.length; i++) {
366-
// If we find a failure, return false immediately (short circuit)
367-
if (!calcOneMatch(attempt, i, correctRe)) return false;
385+
if (validIndexes == null || validIndexes.includes(i)) {
386+
// If we find a failure, return false immediately (short circuit)
387+
if (!calcOneMatch(attempt, i, correctRe)) {
388+
return false;
389+
}
390+
}
368391
}
369392
// Everything passed.
370393
return true;
@@ -385,8 +408,8 @@ function retrieveAttempt() {
385408

386409
const attemptIdPattern = /^attempt(\d+)$/;
387410

388-
/*
389-
* Given Node @form in document, return array of indexes of input/textareas
411+
/**
412+
* Given Node form in document, return array of indexes of input/textareas
390413
* that are relevant for that form.
391414
* The values retrieved are *input* field indexes (`inputIndexes`),
392415
* starting at 0 for the first user input.
@@ -538,7 +561,7 @@ function runCheck() {
538561
}
539562

540563
/** Return the best-matching hint string given an attempt.
541-
* @attempt - array of strings of attempt to give hints on
564+
* @param attempt - array of strings of attempt to give hints on
542565
*/
543566
function findHint(attempt, validIndexes = undefined) {
544567
// Find a matching hint (matches present and NOT absent)
@@ -557,20 +580,10 @@ function findHint(attempt, validIndexes = undefined) {
557580
}
558581

559582
/** Show a hint to the user. */
560-
function showHint(e) {
561-
// Get data-indexes value using e.target.dataset.indexes
562-
// alert(`Form id = ${e.target.form.id}`);
563-
let attempt = retrieveAttempt();
564-
if (calcMatch(attempt, correctRe)) {
565-
alert(t('already_correct'));
566-
} else if (!hints) {
567-
alert(t('no_hints'));
568-
} else {
569-
// Use *precalculated* input field indexes to work around
570-
// problem in Chrome translator.
571-
let validIndexes = e.target.dataset.inputIndexes;
572-
alert(findHint(attempt, validIndexes));
573-
}
583+
function showHint(e, attempt, validIndexes) {
584+
// Use *precalculated* input field indexes to work around
585+
// problem in Chrome translator.
586+
alert(findHint(attempt, validIndexes));
574587
}
575588

576589
/** Show the answer to the user */
@@ -625,18 +638,42 @@ function maybeShowAnswer(e) {
625638
}
626639
}
627640

641+
/** Return true iff target is same hint button as last time & no edits. */
642+
function sameHint(target) {
643+
return (target == lastHintTarget) && !changedInputSinceHint;
644+
}
645+
628646
/** Maybe show a hint to the user (depending on timer). */
629647
function maybeShowHint(e) {
648+
// If there are no hints, just say so without delay.
649+
if (!hints || hints.length === 0) {
650+
alert(t('no_hints'));
651+
return;
652+
}
653+
654+
// Confirm correct answer if it is, and don't cause a penalty or delay.
655+
// For "hint" we only consider the answers for THIS form.
656+
let attempt = retrieveAttempt();
657+
let formIndexes = JSON.parse(e.target.dataset.inputIndexes);
658+
if (calcMatch(attempt, correctRe, formIndexes)) {
659+
alert(t('already_correct'));
660+
return;
661+
}
662+
663+
// Answer is not correct. Determine how much time has passed.
630664
let elapsedTime = elapsedTimeSinceClue();
665+
666+
// Reply if the minimum delay time has passed.
631667
// Only enforce delay timer if changedInputSinceHint is true. That way,
632668
// people can re-see a previously-seen hint as long as they
633669
// have not changed anything since seeing the hint.
634-
if (changedInputSinceHint && (elapsedTime < HINT_DELAY_TIME)) {
670+
if ((elapsedTime < HINT_DELAY_TIME) && !sameHint(e.target)) {
635671
alert(myFormat(t('try_harder_hint'), [HINT_DELAY_TIME.toString()]));
636672
} else {
637673
lastHintTime = Date.now(); // Set new delay time start
674+
lastHintTarget = e.target; // Set last hint button used
638675
changedInputSinceHint = false; // Allow redisplay of hint
639-
showHint(e);
676+
showHint(e, attempt, formIndexes);
640677
}
641678
}
642679

@@ -700,9 +737,10 @@ function processHints(requestedHints) {
700737
}
701738

702739
/** Set global values based on other than "correct" and "expected" values.
703-
* The correct and expected values may come from elsewhere, but we have to set up the
740+
* The correct and expected values may come from elsewhere,
741+
* but we have to set up the
704742
* info-based values first, because info can change how those are interpreted.
705-
* @configurationInfo: Data to use
743+
* @param configurationInfo - Data to use
706744
*/
707745
function processInfo(configurationInfo) {
708746
const allowedInfoFields = new Set([
@@ -887,6 +925,7 @@ function setupInfo() {
887925
};
888926
}
889927

928+
/** Initialize the whole HTML page */
890929
function initPage() {
891930
// Set current locale
892931
lang = determine_locale();

0 commit comments

Comments
 (0)