diff --git a/docs/labs/checker.js b/docs/labs/checker.js index d22830e5..7cd6aad5 100644 --- a/docs/labs/checker.js +++ b/docs/labs/checker.js @@ -9,6 +9,7 @@ let correctRe = []; // Array of compiled regex of correct answer let expected = []; // Array of an expected (correct) answer let info = {}; // General info +let info2 = {}; // Transitional info - if it exists, compare to info let hints = []; // Array of hint objects let page_definitions = {}; // Definitions used when preprocessing regexes @@ -17,6 +18,9 @@ let user_gave_up = false; // True if user ever gave up before user solved it let startTime = Date.now(); +let BACKQUOTE = "`"; // Make it easy to use `${BACKQUOTE}` +let DOLLAR = "$"; // Make it easy to use `${DOLLAR}` + // Current language let lang; @@ -91,7 +95,6 @@ function determine_locale() { return lang; } - // This array contains the default pattern preprocessing commands, in order. // We process every pattern through these (in order) to create a final regex // to be used to match a pattern. @@ -174,6 +177,38 @@ function setDifference(lhs, rhs) { return new Set(result); } +/* Return differences between two objects + */ +function objectDiff(obj1, obj2) { + let diff = {}; + + function compare(obj1, obj2, path = '') { + for (const key in obj1) { + if (obj1.hasOwnProperty(key)) { + const newPath = path ? `${path}.${key}` : key; + + if (!obj2.hasOwnProperty(key)) { + diff[newPath] = [obj1[key], undefined]; + } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { + compare(obj1[key], obj2[key], newPath); + } else if (obj1[key] !== obj2[key]) { + diff[newPath] = [obj1[key], obj2[key]]; + } + } + } + + for (const key in obj2) { + if (obj2.hasOwnProperty(key) && !obj1.hasOwnProperty(key)) { + const newPath = path ? `${path}.${key}` : key; + diff[newPath] = [undefined, obj2[key]]; + } + } + } + + compare(obj1, obj2); + return diff; +} + /* * Show debug output in debug region and maybe via alert box * @debugOutput - the debug information to show @@ -546,10 +581,10 @@ function processHints(requestedHints) { return compiledHints; } -/** Set global values based on info. +/** Load and parse YAML data, return result to be placed in "info". * @info: String with YAML (including JSON) data to use */ -function processInfo(configurationInfo) { +function processYamlToInfo(configurationInfo) { // This would only allow JSON, but then we don't need to load YAML lib: // let parsedJson = JSON.parse(configurationInfo); @@ -563,9 +598,15 @@ function processInfo(configurationInfo) { throw e; // Rethrow, so containing browser also gets exception } - // Set global variable - info = parsedData; + return parsedData; +} +/** Set global values based on other than "correct" and "expected" values. + * The correct and expected values may come from elsewhere, but we have to set up the + * info-based values first, because info can change how those are interpreted. + * @info: String with YAML (including JSON) data to use + */ +function processInfo(configurationInfo) { const allowedInfoFields = new Set([ 'hints', 'successes', 'failures', 'correct', 'expected', 'definitions', 'preprocessing', 'preprocessingTests', 'debug']); @@ -603,14 +644,14 @@ function processInfo(configurationInfo) { }; // Set up hints - if (parsedData && parsedData.hints) { - hints = processHints(parsedData.hints); + if (info && info.hints) { + hints = processHints(info.hints); }; } /** * Run a simple selftest. - * Run loadData *before* calling this, to set up globals like correctRe. + * Run setupInfo *before* calling this, to set up globals like correctRe. * This ensures that: * - the initial attempt is incorrect (as expected) * - the expected value is correct (as expected) @@ -683,17 +724,36 @@ function runSelftest() { } /** - * Load data from HTML page and initialize our local variables from it. + * Load "info" data and set up all other variables that depend on "info". + * The "info" data includes the regex preprocessing steps, hints, etc. */ -function loadData() { - // If there is info (e.g., hints), load it & set up global variable hints. +function setupInfo() { // We must load info *first*, because it can affect how other things // (like pattern preprocessing) is handled. + + // Deprecated approach: Load embedded "info" data in YAML file. + // If there is "info" data embedded in the HTML (e.g., hints), + // load it & set up global variable hints. let infoElement = document.getElementById('info'); if (infoElement) { - processInfo(infoElement.textContent); + let configurationYamlText = infoElement.textContent; + // Set global variable "info" + info = processYamlToInfo(configurationYamlText); }; + // If an "info2" exists, report any differences between it and "info". + // This makes it safer to change how info is recorded. + if (Object.keys(info2).length > 0) { + let differences = objectDiff(info, info2); + if (Object.keys(differences).length > 0) { + alert(`ERROR: info2 exists, but info and info2 differ: ${JSON.stringify(differences)}`); + } + }; + + + // Set global values *except* correct and expected arrays + processInfo(info); + // Set global correct and expected arrays let current = 0; while (true) { @@ -732,7 +792,8 @@ function loadData() { } function initPage() { - loadData(); + // Use configuration info to set up all relevant global values. + setupInfo(); // Run a selftest on page load, to prevent later problems runSelftest(); diff --git a/docs/labs/create_checker.md b/docs/labs/create_checker.md index e6b1aeca..ac1b5e77 100644 --- a/docs/labs/create_checker.md +++ b/docs/labs/create_checker.md @@ -92,6 +92,19 @@ under the `docs/labs` directory. Simply fork the repository, add your proposed lab in the `docs/labs` directory, and create a pull request. +### Transitioning away from YAML + +Configuration data was originally in an embedded YAML file. +We are transitioning to using separate `.js` files to simplify +translations and eliminate the need for the YAML library. +E.g., `input1.html` will have a corresponding `input1.js` +with configuration information that is shared between translations. +That transition hasn't completed yet. + +To help, you can create a JavaScript file that sets info2 +instead of info. The checker will automatically report +any differences between the two values. + ### Quick aside: script tag requirements Data about the lab is embedded in the HTML in a @@ -746,6 +759,25 @@ different markers for the text of various locales. E.g., `text` would be English, and `text_jp` would its Japanese translation. We'd love feedback on this idea. +## Conversion of YAML to JavaScript files + +We have used embedded YAML in the HTML files for configuration. +However, this creates a problem for translations: You want different +HTML files for each translation (locale), yet the embedded YAML can't be shared. + +We have decided to move away from YAML for configuration to +a lab-specific JavaScript file. That file can be loaded when the HTML +is loaded, even when the HTML is loaded locally. + +You can start this conversion using the `yq` tool: + +~~~~sh +yq eval hello.yaml -o=json -P > hello.js +~~~~ + +Prepend the result with `configurationInfo =` and suffix with `;`. +Now load the JavaScript as a script (after the main library). + ## Potential future directions Below are notes about potential future directions. diff --git a/docs/labs/hello.html b/docs/labs/hello.html index d85665c3..a90417f2 100644 --- a/docs/labs/hello.html +++ b/docs/labs/hello.html @@ -7,6 +7,7 @@ + @@ -21,98 +22,6 @@ \s* console \. log \( (["'`])Hello,\x20world!\1 \) ; \s* - - - diff --git a/docs/labs/hello.js b/docs/labs/hello.js new file mode 100644 index 00000000..2b7b151b --- /dev/null +++ b/docs/labs/hello.js @@ -0,0 +1,92 @@ +info = +{ + hints: [ + { + absent: String.raw`^ console \. log \(`, + text: "Please use the form console.log(...);", + text_ja: "console.log(...); の形式を使用してください。", + examples: [ [ "" ], [ "foo" ] + ] + }, + { + present: "Goodbye", + text: "You need to change the text Goodbye to something else.", + text_ja: "「Goodbye」というテキストを別の文字に変更する必要があります.", + examples: [ [ "console.log(\"Goodbye.\");" ] ] + }, + { + present: "hello", + text: "Please capitalize Hello.", + text_ja: "Hello は大文字で入力してください。.", + examples: [ [ "console.log(\"hello.\");" ] ] + }, + { + present: "World", + text: "Please lowercase world.", + text_ja: "world という単語を小文字にしてください。", + examples: [ [ "console.log(\"Hello, World!\");" ] ] + }, + { + present: "Hello[^,]", + text: "Put a comma immediately after Hello.", + text_ja: "Hello の直後にカンマを入れます", + examples: [ [ "console.log(\"Hello world.\");" ] ] + }, + { + present: "Hello", + absent: "[Ww]orld", + text: "There's a Hello, but you need to also mention the world.", + text_ja: "「Hello」がありますが、「world」という単語にも言及する必要があります。", + examples: [ [ "console.log(\"Hello, \");" ] ] + }, + { + present: String.raw`world[^\!]`, + text: "Put an exclamation point immediately after world.", + text_ja: "world の直後に感嘆符を置きます。", + examples: [ [ "console.log(\"Hello, world.\");" ] ] + }, + { + present: String.raw`Hello,\s*world!`, + absent: String.raw`Hello,\x20world!`, + text: "You need exactly one space between 'Hello,' and 'world!'", + text_ja: "「Hello」と「world!」の間にはスペースが 1 つだけ必要です。", + examples: [ [ "console.log(\"Hello, world!\");" ] ] + }, + { + present: String.raw`^ console \. log \( Hello`, + text: "You must quote constant strings using \", ', or `", + text_ja: "定数文字列は \"、'、または ` を使用して引用符で囲む必要があります。", + examples: [ [ "console.log(Hello, world" ], [ "console.log( Hello, world" ] ] + }, + { + absent: String.raw` ; $`, + text: "Please end this statement with a semicolon. JavaScript does not require a semicolon in this case, but usually when modifying source code you should follow the style of the current code.", + text_ja: "このステートメントはセミコロンで終了してください。この場合、JavaScript ではセミコロンは必要ありませんが、通常、ソース コードを変更する場合は、現在のコードのスタイルに従う必要があります。", + examples: [ [ " console.log(\"Hello, world!\") " ] ] + } + ], + successes: [ + [ " console . log( \"Hello, world!\" ) ; " ], + [ " console . log( 'Hello, world!' ) ; " ], + [ " console . log( `Hello, world!` ) ; " ] + ], + failures: [ + [ " console . log( Hello, world! ) ; " ], + [ " console . log(\"hello, world!\") ; " ] + ], + // All regexes are preprocessed by a set of rules. You can replace them with your + // own set. You can completely eliminate them by providing an empty set of rules: + // "preprocessing" : [ ], + // Here are tests for default preprocessing. + // Don't do this in every lab. This merely demonstrates how to create tests for your preprocessing. + "preprocessingTests": [ + [ + String.raw`\s* console \. log \( (["'${BACKQUOTE}])Hello,\x20world!\1 \) ; \s*`, + String.raw`\s*console\s*\.\s*log\s*\(\s*(["'${BACKQUOTE}])Hello,\x20world!\1\s*\)\s*;\s*` + ], + [ + String.raw`\s* foo \s+ bar \\string\\ \s*`, + String.raw`\s*foo\s+bar\s*\\string\\\s*` + ] + ] +};