Skip to content

Commit dc0f6c9

Browse files
committed
Improve Validation Regexes
1 parent 813f304 commit dc0f6c9

9 files changed

Lines changed: 304 additions & 27 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Then open `http://localhost:8000`.
2121

2222
## Examples
2323

24-
All tutorial content is in [`tutorial-data.js`](tutorial-data.js). The interface renders the lessons from that single configuration object.
24+
All tutorial content is in [`tutorial-data.js`](tutorial-data.js). The interface renders the lessons from that single configuration object. Reusable exercise checks are centralized in [`validation-regexes.js`](validation-regexes.js).
2525

2626
Each lesson contains:
2727

app.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { tutorial } from "./tutorial-data.js";
2+
13
(function () {
24
"use strict";
35

4-
const content = window.LIQUID_JAVA_TUTORIAL;
6+
const content = tutorial;
57
const steps = [
68
{ id: "welcome", shortTitle: "Welcome" },
79
...content.lessons.map((lesson) => ({ id: lesson.id, shortTitle: lesson.shortTitle })),
810
];
9-
const appScriptUrl = new URL(document.currentScript.src);
11+
const appScriptUrl = new URL(import.meta.url);
1012
const basePath = appScriptUrl.pathname.slice(0, appScriptUrl.pathname.lastIndexOf("/") + 1);
1113

1214
const blankState = () => ({

index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
<script src="vendor/codemirror-closebrackets.js"></script>
6464
<script src="vendor/prism.js"></script>
6565
<script src="vendor/prism-java.min.js"></script>
66-
<script src="tutorial-data.js"></script>
67-
<script src="app.js"></script>
66+
<script type="module" src="app.js"></script>
6867
</body>
6968
</html>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"vendor:syntax": "node scripts/vendor-syntax.mjs",
88
"dev": "npm run vendor:syntax && node scripts/dev-server.mjs",
99
"build": "npm run vendor:syntax && node scripts/build-site.mjs",
10-
"check": "node --check app.js && node --check tutorial-data.js && node --check scripts/dev-server.mjs && node --check scripts/build-site.mjs && node scripts/validate-content.mjs"
10+
"test": "node --test validation-regexes.test.js",
11+
"check": "node --check app.js && node --check tutorial-data.js && node --check validation-regexes.js && node --check scripts/dev-server.mjs && node --check scripts/build-site.mjs && node --test validation-regexes.test.js && node scripts/validate-content.mjs"
1112
},
1213
"dependencies": {
1314
"codemirror": "5.65.21",

scripts/build-site.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const staticEntries = [
99
"index.html",
1010
"app.js",
1111
"tutorial-data.js",
12+
"validation-regexes.js",
1213
"styles.css",
1314
"images",
1415
"vendor",

scripts/validate-content.mjs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { access, readFile } from "node:fs/promises";
1+
import { access } from "node:fs/promises";
22
import { dirname, resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
4-
import vm from "node:vm";
4+
import { tutorial } from "../tutorial-data.js";
55

66
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7-
const source = await readFile(resolve(root, "tutorial-data.js"), "utf8");
8-
const sandbox = { window: {} };
9-
10-
vm.runInNewContext(source, sandbox, { filename: "tutorial-data.js" });
11-
12-
const tutorial = sandbox.window.LIQUID_JAVA_TUTORIAL;
137
const failures = [];
148

159
if (!tutorial?.lessons?.length) {

tutorial-data.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { validationRegexes } from "./validation-regexes.js";
2+
13
/*
24
* TUTORIAL CONTENT LIVES HERE
35
* ---------------------------
@@ -7,7 +9,7 @@
79
* Exercise checks are JavaScript regular expressions stored as strings. They are
810
* intentionally tolerant of spacing so learners can format Java naturally.
911
*/
10-
window.LIQUID_JAVA_TUTORIAL = {
12+
export const tutorial = {
1113
meta: {
1214
title: "LiquidJava Interactive Tutorial",
1315
eyebrow: "A hands-on interactive tutorial",
@@ -56,11 +58,11 @@ public class RGB {
5658
}`,
5759
checks: [
5860
{
59-
pattern: "@Refinement\\s*\\(\\s*\"(?:(?:_|red)\\s*>=\\s*0\\s*&&\\s*(?:_|red)\\s*<=\\s*255|0\\s*<=\\s*(?:_|red)\\s*&&\\s*(?:_|red)\\s*<=\\s*255)\"\\s*\\)",
61+
pattern: validationRegexes.rgb.refinement,
6062
message: "Add a boolean refinement that includes every integer from 0 to 255.",
6163
},
6264
{
63-
pattern: "int\\s+red\\s*=\\s*(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\s*;",
65+
pattern: validationRegexes.rgb.assignment,
6466
message: "Choose a red-channel value between 0 and 255.",
6567
},
6668
],
@@ -137,11 +139,11 @@ public class Midpoint {
137139
}`,
138140
checks: [
139141
{
140-
pattern: "@Refinement\\s*\\(\\s*\"(?:_\\s*>=\\s*low|low\\s*<=\\s*_)\\s*&&\\s*_\\s*<=\\s*high\"\\s*\\)\\s*public\\s+static\\s+int\\s+midpoint",
142+
pattern: validationRegexes.midpoint.result,
141143
message: "Refine the return value so it stays between low and high.",
142144
},
143145
{
144-
pattern: "@Refinement\\s*\\(\\s*\"_\\s*<=\\s*high\"\\s*\\)\\s*int\\s+low",
146+
pattern: validationRegexes.midpoint.bounds,
145147
message: "Require low to be less than or equal to high.",
146148
},
147149
],
@@ -250,19 +252,19 @@ public interface SocketRefinements {
250252
}`,
251253
checks: [
252254
{
253-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"unconnected\\(this\\)\"\\s*,\\s*to\\s*=\\s*\"bound\\(this\\)\"\\s*\\)\\s*public\\s+void\\s+bind",
255+
pattern: validationRegexes.socket.bind,
254256
message: "bind should move the socket from unconnected to bound.",
255257
},
256258
{
257-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"bound\\(this\\)\"\\s*,\\s*to\\s*=\\s*\"connected\\(this\\)\"\\s*\\)\\s*public\\s+void\\s+connect",
259+
pattern: validationRegexes.socket.connect,
258260
message: "connect should move the socket from bound to connected.",
259261
},
260262
{
261-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"connected\\(this\\)\"\\s*(?:,\\s*to\\s*=\\s*\"connected\\(this\\)\"\\s*)?\\)\\s*public\\s+void\\s+sendUrgentData",
263+
pattern: validationRegexes.socket.sendUrgentData,
262264
message: "sendUrgentData should require a connected socket and leave it connected.",
263265
},
264266
{
265-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"!closed\\(this\\)\"\\s*,\\s*to\\s*=\\s*\"closed\\(this\\)\"\\s*\\)\\s*public\\s+void\\s+close",
267+
pattern: validationRegexes.socket.close,
266268
message: "close should accept any non-closed socket and leave it closed.",
267269
},
268270
],
@@ -364,19 +366,19 @@ public interface StackRefinements<E> {
364366
}`,
365367
checks: [
366368
{
367-
pattern: "@StateRefinement\\s*\\(\\s*to\\s*=\\s*\"size\\(this\\)\\s*==\\s*0\"\\s*\\)\\s*public\\s+void\\s+Stack",
369+
pattern: validationRegexes.stack.constructor,
368370
message: "The constructor should initialize size(this) to 0.",
369371
},
370372
{
371-
pattern: "@StateRefinement\\s*\\(\\s*to\\s*=\\s*\"size\\(this\\)\\s*==\\s*size\\(old\\(this\\)\\)\\s*\\+\\s*1\"\\s*\\)\\s*public\\s+E\\s+push",
373+
pattern: validationRegexes.stack.push,
372374
message: "push should increase the previous size by one.",
373375
},
374376
{
375-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"size\\(this\\)\\s*>\\s*0\"\\s*,\\s*to\\s*=\\s*\"size\\(this\\)\\s*==\\s*size\\(old\\(this\\)\\)\\s*-\\s*1\"\\s*\\)\\s*public\\s+E\\s+pop",
377+
pattern: validationRegexes.stack.pop,
376378
message: "pop should require a non-empty stack and decrease size by one.",
377379
},
378380
{
379-
pattern: "@StateRefinement\\s*\\(\\s*from\\s*=\\s*\"size\\(this\\)\\s*>\\s*0\"\\s*\\)\\s*public\\s+E\\s+peek",
381+
pattern: validationRegexes.stack.peek,
380382
message: "peek should require size(this) to be greater than zero.",
381383
},
382384
],
@@ -403,3 +405,5 @@ public interface StackRefinements<E> {
403405
},
404406
],
405407
};
408+
409+
if (typeof window !== "undefined") window.LIQUID_JAVA_TUTORIAL = tutorial;

validation-regexes.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Lightweight source checks used by the interactive exercises.
3+
*
4+
* These remain regular expressions rather than a Java parser, but the helpers
5+
* below make equivalent refinements explicit: operands and boolean clauses may
6+
* be reordered, and a refined local/parameter may be written as either `_` or
7+
* its declared name.
8+
*/
9+
10+
const whitespace = "\\s*";
11+
12+
function optionalParens(pattern) {
13+
return `(?:${pattern}|\\(${whitespace}${pattern}${whitespace}\\))`;
14+
}
15+
16+
function comparison(left, operator, right) {
17+
return `${left}${whitespace}${operator}${whitespace}${right}`;
18+
}
19+
20+
function orderedComparison(value, operator, bound, reversedOperator) {
21+
return `(?:${comparison(value, operator, bound)}|${comparison(bound, reversedOperator, value)})`;
22+
}
23+
24+
function equality(left, right) {
25+
return `(?:${comparison(left, "==", right)}|${comparison(right, "==", left)})`;
26+
}
27+
28+
function conjunction(first, second) {
29+
const left = optionalParens(first);
30+
const right = optionalParens(second);
31+
return optionalParens(`(?:${left}${whitespace}&&${whitespace}${right}|${right}${whitespace}&&${whitespace}${left})`);
32+
}
33+
34+
function permutations(values) {
35+
if (values.length <= 1) return [values];
36+
return values.flatMap((value, index) =>
37+
permutations(values.filter((_, candidateIndex) => candidateIndex !== index)).map((rest) => [value, ...rest]),
38+
);
39+
}
40+
41+
function disjunction(values) {
42+
const alternatives = permutations(values).map((items) =>
43+
items.map(optionalParens).join(`${whitespace}\\|\\|${whitespace}`),
44+
);
45+
return optionalParens(`(?:${alternatives.join("|")})`);
46+
}
47+
48+
function quoted(expression) {
49+
return `"${whitespace}${expression}${whitespace}"`;
50+
}
51+
52+
function refinement(expression, declaration) {
53+
return `@Refinement${whitespace}\\(${whitespace}${quoted(expression)}${whitespace}\\)${whitespace}${declaration}`;
54+
}
55+
56+
function namedArgument(name, expression) {
57+
return `${name}${whitespace}=${whitespace}${quoted(expression)}`;
58+
}
59+
60+
function stateRefinement(argumentsPattern, declaration) {
61+
return `@StateRefinement${whitespace}\\(${whitespace}${argumentsPattern}${whitespace}\\)${whitespace}${declaration}`;
62+
}
63+
64+
function transition(from, to, declaration) {
65+
const fromArgument = namedArgument("from", from);
66+
const toArgument = namedArgument("to", to);
67+
const argumentsPattern = `(?:${fromArgument}${whitespace},${whitespace}${toArgument}|${toArgument}${whitespace},${whitespace}${fromArgument})`;
68+
return stateRefinement(argumentsPattern, declaration);
69+
}
70+
71+
function fromWithOptionalSameTo(state, declaration) {
72+
const fromArgument = namedArgument("from", state);
73+
const toArgument = namedArgument("to", state);
74+
const argumentsPattern = `(?:${fromArgument}|${fromArgument}${whitespace},${whitespace}${toArgument}|${toArgument}${whitespace},${whitespace}${fromArgument})`;
75+
return stateRefinement(argumentsPattern, declaration);
76+
}
77+
78+
const rgbValue = "(?:_|red)";
79+
const rgbLower = orderedComparison(rgbValue, ">=", "0", "<=");
80+
const rgbUpper = orderedComparison(rgbValue, "<=", "255", ">=");
81+
82+
const lowValue = "(?:_|low)";
83+
const lowBeforeHigh = orderedComparison(lowValue, "<=", "high", ">=");
84+
85+
const midpointNormalLower = orderedComparison("_", ">=", "low", "<=");
86+
const midpointNormalUpper = orderedComparison("_", "<=", "high", ">=");
87+
const midpointReversedLower = orderedComparison("_", ">=", "high", "<=");
88+
const midpointReversedUpper = orderedComparison("_", "<=", "low", ">=");
89+
const midpointRange = `(?:${conjunction(midpointNormalLower, midpointNormalUpper)}|${conjunction(midpointReversedLower, midpointReversedUpper)})`;
90+
91+
const state = (name) => `${name}${whitespace}\\(${whitespace}this${whitespace}\\)`;
92+
const unconnected = state("unconnected");
93+
const bound = state("bound");
94+
const connected = state("connected");
95+
const closed = state("closed");
96+
const notClosed = `!${whitespace}${closed}`;
97+
const anyOpenState = `(?:${notClosed}|${disjunction([unconnected, bound, connected])})`;
98+
99+
const sizeNow = `size${whitespace}\\(${whitespace}this${whitespace}\\)`;
100+
const sizeOld = `size${whitespace}\\(${whitespace}old${whitespace}\\(${whitespace}this${whitespace}\\)${whitespace}\\)`;
101+
const zeroSize = equality(sizeNow, "0");
102+
const positiveSize = orderedComparison(sizeNow, ">", "0", "<");
103+
const incrementedSize = equality(sizeNow, `(?:${comparison(sizeOld, "\\+", "1")}|${comparison("1", "\\+", sizeOld)})`);
104+
const decrementedSize = equality(sizeNow, comparison(sizeOld, "-", "1"));
105+
106+
export const validationRegexes = Object.freeze({
107+
rgb: Object.freeze({
108+
refinement: refinement(conjunction(rgbLower, rgbUpper), `int\\s+red\\b`),
109+
assignment: "int\\s+red\\s*=\\s*(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\s*;",
110+
}),
111+
midpoint: Object.freeze({
112+
result: refinement(midpointRange, "public\\s+static\\s+int\\s+midpoint\\b"),
113+
bounds: refinement(lowBeforeHigh, "int\\s+low\\b"),
114+
}),
115+
socket: Object.freeze({
116+
bind: transition(unconnected, bound, "public\\s+void\\s+bind\\b"),
117+
connect: transition(bound, connected, "public\\s+void\\s+connect\\b"),
118+
sendUrgentData: fromWithOptionalSameTo(connected, "public\\s+void\\s+sendUrgentData\\b"),
119+
close: transition(anyOpenState, closed, "public\\s+void\\s+close\\b"),
120+
}),
121+
stack: Object.freeze({
122+
constructor: stateRefinement(namedArgument("to", zeroSize), "public\\s+void\\s+Stack\\b"),
123+
push: stateRefinement(namedArgument("to", incrementedSize), "public\\s+E\\s+push\\b"),
124+
pop: transition(positiveSize, decrementedSize, "public\\s+E\\s+pop\\b"),
125+
peek: stateRefinement(namedArgument("from", positiveSize), "public\\s+E\\s+peek\\b"),
126+
}),
127+
});

0 commit comments

Comments
 (0)