Skip to content

Commit bc6c027

Browse files
committed
Add Routing
1 parent 2689959 commit bc6c027

6 files changed

Lines changed: 121 additions & 8 deletions

File tree

app.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
{ id: "welcome", shortTitle: "Welcome" },
77
...content.lessons.map((lesson) => ({ id: lesson.id, shortTitle: lesson.shortTitle })),
88
];
9+
const appScriptUrl = new URL(document.currentScript.src);
10+
const basePath = appScriptUrl.pathname.slice(0, appScriptUrl.pathname.lastIndexOf("/") + 1);
911

1012
const blankState = () => ({
1113
currentStep: 0,
@@ -20,6 +22,28 @@
2022
const screen = document.querySelector("#screen");
2123
const navigation = document.querySelector("#step-navigation");
2224
const resetButton = document.querySelector("#reset-button");
25+
const brandLink = document.querySelector(".brand");
26+
27+
function stepIndexFromPath() {
28+
const relativePath = window.location.pathname.startsWith(basePath)
29+
? window.location.pathname.slice(basePath.length)
30+
: "";
31+
const route = decodeURIComponent(relativePath).replace(/^\/+|\/+$/g, "");
32+
33+
if (!route || route === "index.html" || route === "404.html") return 0;
34+
return steps.findIndex((step) => step.id === route);
35+
}
36+
37+
function pathForStep(index) {
38+
const step = steps[index];
39+
return step.id === "welcome" ? basePath : `${basePath}${encodeURIComponent(step.id)}`;
40+
}
41+
42+
function updatePath(index, mode = "push") {
43+
const path = pathForStep(index);
44+
if (window.location.pathname === path) return;
45+
window.history[`${mode}State`]({ step: steps[index].id }, "", path);
46+
}
2347

2448
function escapeHtml(value) {
2549
return String(value)
@@ -33,6 +57,7 @@
3357
function render() {
3458
renderNavigation();
3559
const step = steps[state.currentStep];
60+
document.title = step.id === "welcome" ? content.meta.title : `${step.shortTitle} · ${content.meta.title}`;
3661
if (step.id === "welcome") renderWelcome();
3762
else renderLesson(content.lessons.find((lesson) => lesson.id === step.id));
3863
updateProgress();
@@ -350,17 +375,33 @@
350375
if (!state.completed.includes(stepId)) state.completed.push(stepId);
351376
}
352377

353-
function goToStep(index) {
378+
function goToStep(index, historyMode = "push") {
354379
state.currentStep = Math.max(0, Math.min(index, steps.length - 1));
380+
updatePath(state.currentStep, historyMode);
355381
render();
356382
document.querySelector("#tutorial-main").focus({ preventScroll: true });
357383
}
358384

359385
resetButton.addEventListener("click", () => {
360386
if (!window.confirm("Reset this tutorial session, including edited code and answers?")) return;
361387
state = blankState();
388+
goToStep(0, "replace");
389+
});
390+
391+
brandLink.addEventListener("click", (event) => {
392+
event.preventDefault();
393+
goToStep(0);
394+
});
395+
396+
window.addEventListener("popstate", () => {
397+
const routeIndex = stepIndexFromPath();
398+
state.currentStep = routeIndex >= 0 ? routeIndex : 0;
362399
render();
400+
document.querySelector("#tutorial-main").focus({ preventScroll: true });
363401
});
364402

403+
const initialRouteIndex = stepIndexFromPath();
404+
state.currentStep = initialRouteIndex >= 0 ? initialRouteIndex : 0;
405+
if (initialRouteIndex < 0) updatePath(0, "replace");
365406
render();
366407
})();

index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
<meta name="twitter:description" content="Catch the bug before it runs with four guided LiquidJava exercises." />
1414
<link rel="icon" type="image/png" href="images/liquidjava-icon.png" />
1515
<link rel="stylesheet" href="vendor/codemirror.css" />
16-
<link rel="stylesheet" href="styles.css?v=2" />
16+
<link rel="stylesheet" href="styles.css" />
1717
<title>LiquidJava Interactive Tutorial</title>
1818
</head>
1919
<body>
2020
<a class="skip-link" href="#tutorial-main">Skip to the tutorial</a>
2121

2222
<header class="site-header">
23-
<a class="brand" href="#" aria-label="LiquidJava Interactive Tutorial home">
23+
<a class="brand" href="./" aria-label="LiquidJava Interactive Tutorial home">
2424
<img class="brand-mark" src="images/liquidjava-icon.png" alt="" width="42" height="42" />
2525
<span>
2626
<strong>LiquidJava</strong>
@@ -63,7 +63,7 @@
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?v=5"></script>
67-
<script src="app.js?v=5"></script>
66+
<script src="tutorial-data.js"></script>
67+
<script src="app.js"></script>
6868
</body>
6969
</html>

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"type": "module",
66
"scripts": {
77
"vendor:syntax": "node scripts/vendor-syntax.mjs",
8-
"dev": "npm run vendor:syntax && python3 -m http.server 8000 --directory .",
8+
"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 scripts/validate-content.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"
1111
},
1212
"dependencies": {
1313
"codemirror": "5.65.21",

scripts/build-site.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ for (const entry of staticEntries) {
2323
await cp(resolve(root, entry), resolve(clientOutput, entry), { recursive: true });
2424
}
2525

26+
await cp(resolve(root, "index.html"), resolve(clientOutput, "404.html"));
27+
2628
await writeFile(
2729
resolve(output, "server", "index.js"),
2830
`export default {

scripts/dev-server.mjs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createReadStream } from "node:fs";
2+
import { stat } from "node:fs/promises";
3+
import { createServer } from "node:http";
4+
import { dirname, extname, resolve, sep } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8+
const port = 8000;
9+
const mimeTypes = {
10+
".css": "text/css; charset=utf-8",
11+
".gif": "image/gif",
12+
".html": "text/html; charset=utf-8",
13+
".ico": "image/x-icon",
14+
".js": "text/javascript; charset=utf-8",
15+
".jpg": "image/jpeg",
16+
".jpeg": "image/jpeg",
17+
".png": "image/png",
18+
".svg": "image/svg+xml",
19+
".webp": "image/webp",
20+
};
21+
22+
function safePath(pathname) {
23+
const target = resolve(root, `.${pathname}`);
24+
return target === root || target.startsWith(`${root}${sep}`) ? target : null;
25+
}
26+
27+
const server = createServer(async (request, response) => {
28+
const url = new URL(request.url, `http://${request.headers.host || "localhost"}`);
29+
let pathname;
30+
31+
try {
32+
pathname = decodeURIComponent(url.pathname);
33+
} catch {
34+
response.writeHead(400).end("Bad request");
35+
return;
36+
}
37+
38+
let target = pathname === "/" ? resolve(root, "index.html") : safePath(pathname);
39+
let file;
40+
41+
if (target) {
42+
try {
43+
file = await stat(target);
44+
} catch {
45+
file = null;
46+
}
47+
}
48+
49+
if (!file?.isFile()) {
50+
if (request.method === "GET" && !extname(pathname)) {
51+
target = resolve(root, "index.html");
52+
file = await stat(target);
53+
} else {
54+
response.writeHead(404).end("Not found");
55+
return;
56+
}
57+
}
58+
59+
response.writeHead(200, {
60+
"Cache-Control": "no-store",
61+
"Content-Type": mimeTypes[extname(target).toLowerCase()] || "application/octet-stream",
62+
});
63+
64+
if (request.method === "HEAD") response.end();
65+
else createReadStream(target).pipe(response);
66+
});
67+
68+
server.listen(port, () => {
69+
console.log(`LiquidJava Interactive Tutorial: http://localhost:${port}/`);
70+
});

tutorial-data.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public class LightBulb {
193193
prompt:
194194
"Replace the true refinements so bind, connect, sendUrgentData, and close follow the socket protocol.",
195195
guide: {
196-
image: "images/socket_dfa.png?v=20260809",
196+
image: "images/socket_dfa.png",
197197
alt:
198198
"Socket protocol diagram. A socket starts unconnected; bind moves it to bound; connect moves it to connected; sendUrgentData keeps it connected; and close moves any non-closed socket to closed.",
199199
caption:

0 commit comments

Comments
 (0)