Skip to content

Commit

Permalink
Merge pull request #416 from moonbitlang/zhiyuan/route
Browse files Browse the repository at this point in the history
feat: client side routing
  • Loading branch information
bzy-debug authored Jan 12, 2025
2 parents a4cc80c + e04a9fc commit f5765cc
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 161 deletions.
5 changes: 5 additions & 0 deletions moonbit-tour/build/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,17 @@ const plugin = (): esbuild.Plugin => {
await Promise.all(
pages.map(async (p) => {
const content = page.render(template, p);
const data = page.route(p);
await fs.mkdir(`./dist/${path.dirname(p.path)}`, {
recursive: true,
});
await fs.writeFile(`./dist/${p.path}`, content, {
encoding: "utf8",
});
await fs.writeFile(
`./dist/${p.path.replace(".html", ".json")}`,
data,
);
}),
);
});
Expand Down
10 changes: 10 additions & 0 deletions moonbit-tour/build/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,13 @@ export function render(template: string, page: Page): string {
.replace("%BACK%", page.back)
.replace("%NEXT%", page.next);
}

export function route(page: Page): string {
return JSON.stringify({
title: page.title,
markdownHtml: remark.mdToHtml(page.markdown),
code: page.code,
back: page.back,
next: page.next,
});
}
4 changes: 2 additions & 2 deletions moonbit-tour/build/scan-tour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ function renderTOC(chapters: Chapter[]): string {
for (const c of chapters) {
lines.push(`<li><div class="toc-chapter pl-1">`);
lines.push(
`<button class="toc-chapter-title cursor-pointer capitalize py-1">${c.chapter}</button>`,
`<button class="toc-chapter-title block w-full text-start cursor-pointer capitalize py-1">${c.chapter}</button>`,
);
lines.push(`<ul class="toc-sections bg-gray-50">`);
for (const l of c.lessons) {
lines.push(
`<li class="text-base capitalize pl-2 py-[2px]"><a href="/${slug(l)}/index.html">${l.lesson}</a></li>`,
`<li><a class="text-base capitalize pl-2 py-[2px] block" href="/${slug(l)}/index.html">${l.lesson}</a></li>`,
);
}
lines.push(`</ul>`);
Expand Down
16 changes: 10 additions & 6 deletions moonbit-tour/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
</svg>
</button>
<a class="text-base" href="/index.html">MoonBit Language Tour</a>
<a class="ml-auto" href="https://www.moonbitlang.com">MoonBit</a>
<div class="cursor-pointer" id="theme">
<a class="ml-auto" href="https://www.moonbitlang.com" target="_blank"
>MoonBit</a
>
<button class="cursor-pointer" id="theme">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
Expand All @@ -45,7 +47,7 @@
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/>
</svg>
</div>
</button>
</header>
<div
id="toc"
Expand All @@ -59,11 +61,13 @@
<section
class="prose flex max-w-none flex-1 flex-col items-center gap-4 p-2 dark:prose-invert prose-h2:capitalize md:justify-between md:overflow-auto md:p-6"
>
<div class="w-full">%MARKDOWN%</div>
<div class="w-full" id="tour-content">%MARKDOWN%</div>
<nav class="flex gap-4 md:pb-2">
%BACK% —
<div id="nav-back">%BACK%</div>
<a href="/table-of-contents/index.html">Contents</a>
— %NEXT%
<div id="nav-next">%NEXT%</div>
</nav>
</section>
<section
Expand Down
97 changes: 97 additions & 0 deletions moonbit-tour/src/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as moonbitMode from "@moonbit/moonpad-monaco";
import * as monaco from "monaco-editor-core";
import * as util from "./util";

const moon = moonbitMode.init({
onigWasmUrl: new URL("./onig.wasm", import.meta.url).toString(),
lspWorker: new Worker("/lsp-server.js"),
mooncWorkerFactory: () => new Worker("/moonc-worker.js"),
codeLensFilter(l) {
return l.command?.command === "moonbit-lsp/debug-main";
},
});

// @ts-ignore
self.MonacoEnvironment = {
getWorkerUrl: function () {
return "/editor.worker.js";
},
};

const codePre = document.querySelector<HTMLPreElement>(".shiki")!;
export const model = monaco.editor.createModel(
codePre.textContent ?? "",
"moonbit",
monaco.Uri.file("/main.mbt"),
);

const output = document.querySelector<HTMLPreElement>("#output")!;
const trace = moonbitMode.traceCommandFactory();

async function run(debug: boolean) {
if (debug) {
const result = await moon.compile({
libInputs: [["main.mbt", model.getValue()]],
debugMain: true,
});
switch (result.kind) {
case "success": {
const js = result.js;
const stream = await moon.run(js);
let buffer = "";
await stream.pipeTo(
new WritableStream({
write(chunk) {
buffer += `${chunk}\n`;
},
}),
);
output.textContent = buffer;
return;
}
case "error": {
console.error(result.diagnostics);
}
}
return;
}
const stdout = await trace(monaco.Uri.file("/main.mbt").toString());
if (stdout === undefined) return;
output.textContent = stdout;
}

model.onDidChangeContent(util.debounce(() => run(false), 100));

monaco.editor.onDidCreateEditor(() => {
codePre.remove();
});

const editorContainer = document.getElementById("editor")!;
editorContainer.classList.remove("pl-[10px]", "text-[14px]");
monaco.editor.create(editorContainer, {
model,
lineNumbers: "off",
glyphMargin: false,
minimap: {
enabled: false,
},
automaticLayout: true,
folding: false,
fontSize: 14,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
fontFamily: "monospace",
theme: util.getTheme() === "light" ? "light-plus" : "dark-plus",
});

monaco.editor.registerCommand("moonbit-lsp/debug-main", () => {
run(true);
});

run(false);

window.addEventListener("theme-change", (e) => {
monaco.editor.setTheme(e.detail === "light" ? "light-plus" : "dark-plus");
});
166 changes: 20 additions & 146 deletions moonbit-tour/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,148 +1,22 @@
import * as moonbitMode from "@moonbit/moonpad-monaco";
import * as monaco from "monaco-editor-core";
import * as editor from "./editor";
import * as router from "./router";
import "./style.css";
import "./toc";

const moon = moonbitMode.init({
onigWasmUrl: new URL("./onig.wasm", import.meta.url).toString(),
lspWorker: new Worker("/lsp-server.js"),
mooncWorkerFactory: () => new Worker("/moonc-worker.js"),
codeLensFilter(l) {
return l.command?.command === "moonbit-lsp/debug-main";
},
import * as theme from "./theme";
import * as toc from "./toc";

router.init();
theme.init();
toc.init();

const markdown = document.querySelector<HTMLDivElement>("#tour-content")!;
const next = document.querySelector<HTMLDivElement>("#nav-next")!;
const back = document.querySelector<HTMLDivElement>("#nav-back")!;

window.addEventListener("route-change", async (e) => {
const state = e.detail;
editor.model.setValue(state.code);
markdown.innerHTML = state.markdownHtml;
next.innerHTML = state.next;
back.innerHTML = state.back;
document.title = state.title;
});

// @ts-ignore
self.MonacoEnvironment = {
getWorkerUrl: function () {
return "/editor.worker.js";
},
};

const sunSvg = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
`;

const moonSvg = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
`;

type Theme = "light" | "dark";

function getTheme(): Theme {
return (localStorage.getItem("theme") as Theme) ?? "light";
}

let theme: Theme = getTheme();
const themeButton = document.querySelector<HTMLDivElement>("#theme")!;
setTheme(theme);

function setTheme(theme: Theme) {
if (theme === "light") {
document.querySelector("html")?.classList.remove("dark");
monaco.editor.setTheme("light-plus");
themeButton.innerHTML = moonSvg;
} else {
document.querySelector("html")?.classList.add("dark");
monaco.editor.setTheme("dark-plus");
themeButton.innerHTML = sunSvg;
}
localStorage.setItem("theme", theme);
}

function toggleTheme() {
theme = theme === "light" ? "dark" : "light";
setTheme(theme);
}

themeButton.addEventListener("click", toggleTheme);

function debounce<P extends any[], R>(f: (...args: P) => R, timeout: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
return (...args: P) => {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
f(...args);
timer = null;
}, timeout);
};
}

const codePre = document.querySelector<HTMLPreElement>(".shiki")!;

const model = monaco.editor.createModel(
codePre.textContent ?? "",
"moonbit",
monaco.Uri.file("/main.mbt"),
);

const output = document.querySelector<HTMLPreElement>("#output")!;
const trace = moonbitMode.traceCommandFactory();

async function run(debug: boolean) {
if (debug) {
const result = await moon.compile({
libInputs: [["main.mbt", model.getValue()]],
debugMain: true,
});
switch (result.kind) {
case "success": {
const js = result.js;
const stream = await moon.run(js);
let buffer = "";
await stream.pipeTo(
new WritableStream({
write(chunk) {
buffer += `${chunk}\n`;
},
}),
);
output.textContent = buffer;
return;
}
case "error": {
console.error(result.diagnostics);
}
}
return;
}
const stdout = await trace(monaco.Uri.file("/main.mbt").toString());
if (stdout === undefined) return;
output.textContent = stdout;
}

model.onDidChangeContent(debounce(() => run(false), 100));

monaco.editor.onDidCreateEditor(() => {
codePre.remove();
});

const editorContainer = document.getElementById("editor")!;
editorContainer.classList.remove("pl-[10px]", "text-[14px]");
monaco.editor.create(editorContainer, {
model,
lineNumbers: "off",
glyphMargin: false,
minimap: {
enabled: false,
},
automaticLayout: true,
folding: false,
fontSize: 14,
scrollBeyondLastLine: false,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
fontFamily: "monospace",
theme: theme === "light" ? "light-plus" : "dark-plus",
});

monaco.editor.registerCommand("moonbit-lsp/debug-main", () => {
run(true);
});

run(false);
36 changes: 36 additions & 0 deletions moonbit-tour/src/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function getRouteDataHref(href: string) {
if (href.endsWith("/")) {
return href + "index.json";
} else if (href.endsWith("/index.html")) {
return href.slice(0, -"/index.html".length) + "/index.json";
} else {
return href + "/index.json";
}
}

async function getRouteData(href: string) {
const url = getRouteDataHref(href);
const res = await fetch(url);
return await res.json();
}

export async function init() {
const state = await getRouteData(location.href);

history.replaceState(state, "", location.href);

window.addEventListener("popstate", (e) => {
window.dispatchEvent(new CustomEvent("route-change", { detail: e.state }));
});

document.addEventListener("click", async (e) => {
if (!(e.target instanceof HTMLAnchorElement)) return;
const a = e.target;
const url = new URL(a.href);
if (url.origin !== location.origin) return;
e.preventDefault();
const data = await getRouteData(url.toString());
window.dispatchEvent(new CustomEvent("route-change", { detail: data }));
history.pushState(data, "", url.toString());
});
}
Loading

0 comments on commit f5765cc

Please sign in to comment.