@@ -18,19 +18,25 @@ let user_gave_up = false; // True if user ever gave up before user solved it
1818
1919let startTime = Date . now ( ) ; // Time this lab started.
2020let 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. */
2628let changedInputSinceHint = true ;
2729
2830let BACKQUOTE = "`" ; // Make it easy to use `${BACKQUOTE}`
2931let 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. */
3234let lang = "en" ;
3335
36+ /**
37+ * Resources for localizations, in particular, for translations.
38+ * This intentionally mimics the format of library i18next.
39+ */
3440const 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 */
95102function 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+ */
108119function 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");
118129myAssert ( 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+ */
122136function 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+ */
132147function 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="...">. */
142157function 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+ */
169186let preprocessRegexes = [
170187 // Remove end-of-line characters (\n and \r)
171188 [ / [ \n \r ] + / g, '' ] ,
@@ -217,9 +234,9 @@ function escapeHTML(unsafe) {
217234 . replace ( / \' / g, "'" ) ) ;
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 */
234252function 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 */
270288function 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 */
295313function 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 */
320338function 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 */
337355function 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 */
351369function 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
386409const attemptIdPattern = / ^ a t t e m p t ( \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 */
543566function 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). */
629647function 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 */
707745function processInfo ( configurationInfo ) {
708746 const allowedInfoFields = new Set ( [
@@ -887,6 +925,7 @@ function setupInfo() {
887925 } ;
888926}
889927
928+ /** Initialize the whole HTML page */
890929function initPage ( ) {
891930 // Set current locale
892931 lang = determine_locale ( ) ;
0 commit comments