Skip to content

Commit

Permalink
Create/implement separate hello.js for hello.html config
Browse files Browse the repository at this point in the history
Currently configuration is mostly in a YAML file embedded in the
lab's HTML. This means that the lab configuration cannot be shared
between locales (translations), which is annoying.

We can't easily load YAML or JSON from a separate file when
running or testing locally. However, we *can* easily load a
separate file if it's in JavaScript format.

This switches the configuration of lab hello.html so it uses the
configuration in hello.js.

Signed-off-by: David A. Wheeler <[email protected]>
  • Loading branch information
david-a-wheeler committed Jan 26, 2025
1 parent a04d174 commit 442c032
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 104 deletions.
43 changes: 31 additions & 12 deletions docs/labs/checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ let page_definitions = {}; // Definitions used when preprocessing regexes
let user_solved = false; // True if user has *ever* solved it on this load
let user_gave_up = false; // True if user ever gave up before user solved it

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

// 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.
Expand Down Expand Up @@ -456,10 +459,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);

Expand All @@ -473,9 +476,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']);
Expand Down Expand Up @@ -513,14 +522,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)
Expand Down Expand Up @@ -593,17 +602,26 @@ 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);
};

// Set global values *except* correct and expected arrays
processInfo(info);

// Set global correct and expected arrays
let current = 0;
while (true) {
Expand Down Expand Up @@ -642,7 +660,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();
Expand Down
24 changes: 24 additions & 0 deletions docs/labs/create_checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Modify the `expected0` section to have a sample expected answer, and
See [input1.html](input1.html) and [input2.html](input2.html)
for examples.

*NOTE*: We are transitioning to using separate `.js` files to simplify
translations. E.g., `input1.html` will have a corresponding `input1.js`
with configuration information that is shared between translations.
That transition hasn't completed yet.

Whenever a lab is loaded it automatically runs all embedded self-tests.
At the least, it checks that the initial attempted answer does
*not* satisfy the correct answer pattern, while the example expected answer
Expand Down Expand Up @@ -746,6 +751,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.
Expand Down
93 changes: 1 addition & 92 deletions docs/labs/hello.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<link rel="stylesheet" href="checker.css">
<script src="js-yaml.min.js"></script>
<script src="checker.js"></script>
<script src="hello.js"></script>
<link rel="license" href="https://creativecommons.org/licenses/by/4.0/">

<!-- See create_labs.md for how to create your own lab! -->
Expand All @@ -21,98 +22,6 @@
\s* console \. log \( (["'`])Hello,\x20world!\1 \) ; \s*
</script>

<script id="info" type="application/yaml">
---
hints:
- absent: |-
^ console \. log \(
text: ' Please use the form console.log("...");'
examples:
- - ""
- - "foo"
- present: "Goodbye"
text: "You need to change the text Goodbye to something else."
examples:
- - 'console.log("Goodbye.");'
- present: "hello"
text: "Please capitalize Hello."
examples:
- - 'console.log("hello.");'
- present: "World"
text: "Please lowercase world."
examples:
- - 'console.log("Hello, World!");'
- present: "Hello[^,]"
text: "Put a comma immediately after Hello."
examples:
- - 'console.log("Hello world.");'
- present: "Hello"
absent: "[Ww]orld"
text: "There's a Hello, but you need to also mention the world."
examples:
- - 'console.log("Hello, ");'
- present: |-
world[^\!]
text: "Put an exclamation point immediately after world."
examples:
- - 'console.log("Hello, world.");'
- present: 'Hello,\s*world!'
absent: 'Hello,\x20world!'
text: "You need exactly one space between 'Hello,' and 'world!'"
examples:
- - 'console.log("Hello, world!");'
- present: |-
^ console \. log \( Hello
text: |-
You must quote constant strings using ", ', or `
examples:
- - 'console.log(Hello, world'
- - 'console.log( Hello, world'
- absent: " ; $"
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.
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!") ; '
# ADVANCED use - define our own preprocessing commands.
# I suggest using "|-" (stripping the trailing newlines)
# preprocessing:
# -
# - |-
# [\n\r]+
# - ""
# -
# - |-
# (\\s\*)?\s+(\\s\*)?
# - "\\s*"
# Here are tests for default preprocessing. You do not need this in
# every lab, but it demonstrates how to do it.
preprocessingTests:
-
- |-
\s* console \. log \( (["'`])Hello,\x20world!\1 \) ; \s*
- |-
\s*console\s*\.\s*log\s*\(\s*(["'`])Hello,\x20world!\1\s*\)\s*;\s*
-
- |-
\s* foo \s+ bar \\string\\ \s*
- |-
\s*foo\s+bar\s*\\string\\\s*
# debug: true
</script>
<!--
-->

</head>
<body>
<!-- For GitHub Pages formatting: -->
Expand Down
104 changes: 104 additions & 0 deletions docs/labs/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
info =
{
"hints": [
{
"absent": String.raw`^ console \. log \(`,
"text": " Please use the form console.log(\"...\");",
"examples": [
[ "" ],
[ "foo" ]
]
},
{
"present": "Goodbye",
"text": "You need to change the text Goodbye to something else.",
"examples": [
[ "console.log(\"Goodbye.\");" ]
]
},
{
"present": "hello",
"text": "Please capitalize Hello.",
"examples": [
[ "console.log(\"hello.\");" ]
]
},
{
"present": "World",
"text": "Please lowercase world.",
"examples": [
[ "console.log(\"Hello, World!\");" ]
]
},
{
"present": "Hello[^,]",
"text": "Put a comma immediately after Hello.",
"examples": [
[ "console.log(\"Hello world.\");" ]
]
},
{
"present": "Hello",
"absent": "[Ww]orld",
"text": "There's a Hello, but you need to also mention the world.",
"examples": [
[ "console.log(\"Hello, \");" ]
]
},
{
"present": String.raw`world[^\!]`,
"text": "Put an exclamation point immediately after 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!'",
"examples": [
[ "console.log(\"Hello, world!\");" ]
]
},
{
"present": String.raw`^ console \. log \( Hello`,
"text": "You must quote constant strings using \", ', or `",
"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.\n",
"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. You do not need this in
// every lab, but it 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*`
]

]
};

0 comments on commit 442c032

Please sign in to comment.