aboutsummaryrefslogtreecommitdiff
path: root/docs/play
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-05-25 22:01:06 +0800
committer魏曹先生 <1992414357@qq.com>2026-05-25 22:01:06 +0800
commit17217317eaaf57dd5c39538c115e35ddccb8666d (patch)
tree9f1944b847d5e9d157bbc6a8c496bf8f2e7e1d23 /docs/play
parent979e881762a728661e72efd99bc2b35b3db8c71b (diff)
Restructure docs
add template and interactive tutorial, update tool runner
Diffstat (limited to 'docs/play')
-rw-r--r--docs/play/play.html135
-rw-r--r--docs/play/player.js198
-rw-r--r--docs/play/style.css337
3 files changed, 670 insertions, 0 deletions
diff --git a/docs/play/play.html b/docs/play/play.html
new file mode 100644
index 0000000..8620908
--- /dev/null
+++ b/docs/play/play.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<html lang="zh-CN">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+ <!-- highlight.js -->
+ <link rel="stylesheet" href="../scripts/highlight/github.min.css" />
+ <link
+ id="hljs-light"
+ rel="stylesheet"
+ href="../scripts/highlight/github.min.css"
+ />
+ <link
+ id="hljs-dark"
+ rel="stylesheet"
+ href="../scripts/highlight/github-dark.min.css"
+ disabled
+ />
+ <script src="../scripts/highlight/highlight.min.js"></script>
+ <script src="../scripts/highlight/rust.min.js"></script>
+ <script src="../scripts/highlight/bash.min.js"></script>
+
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+ <link
+ href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap"
+ rel="stylesheet"
+ />
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <!-- Topbar -->
+ <div class="topbar">
+ <h1 class="topbar__title">
+ <span id="tutorialTitle">Title</span>
+ </h1>
+ <div style="display: flex; align-items: center; gap: 16px">
+ <span class="topbar__step">
+ STEP <span id="stepNum">1</span> /
+ <span id="totalSteps">0</span>
+ </span>
+ <button class="theme-toggle" id="themeToggle">🌙</button>
+ </div>
+ </div>
+
+ <!-- Main area -->
+ <div class="layout" id="layout">
+ <!-- Left: code panel -->
+ <div class="code-panel">
+ <pre id="codeDisplay"></pre>
+ </div>
+
+ <!-- Right: description -->
+ <div class="speech-panel">
+ <div class="speech-content" id="speechDisplay"></div>
+ </div>
+ </div>
+
+ <!-- Control bar -->
+ <div class="controls">
+ <button id="prevBtn" disabled>◀ PREV</button>
+ <button id="nextBtn" class="controls__btn--primary">NEXT ▶</button>
+
+ <div class="controls__progress">
+ <div
+ class="controls__progress-fill"
+ id="progressFill"
+ style="width: 0%"
+ ></div>
+ </div>
+
+ <span class="controls__text" id="progressText">0%</span>
+ </div>
+
+ <script src="player.js"></script>
+ <script>
+ (function () {
+ var hljsDark = document.getElementById("hljs-dark");
+ var html = document.documentElement;
+ var btn = document.getElementById("themeToggle");
+
+ function resolveTheme() {
+ var t = localStorage.getItem("theme");
+ if (t === "dark" || t === "light") return t;
+ return window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+ }
+
+ function applyTheme(t) {
+ if (t === "dark") {
+ hljsDark.disabled = false;
+ html.setAttribute("data-theme", "dark");
+ if (btn) btn.textContent = "☀️";
+ } else {
+ hljsDark.disabled = true;
+ html.setAttribute("data-theme", "light");
+ if (btn) btn.textContent = "🌙";
+ }
+ }
+
+ applyTheme(resolveTheme());
+
+ window.addEventListener("storage", function (e) {
+ if (e.key === "theme") applyTheme(resolveTheme());
+ });
+
+ setInterval(function () {
+ var desired = resolveTheme();
+ var current = html.getAttribute("data-theme") || "dark";
+ if (desired !== current) applyTheme(desired);
+ }, 1000);
+
+ if (btn) {
+ btn.addEventListener("click", function () {
+ var current = resolveTheme();
+ var next = current === "dark" ? "light" : "dark";
+ localStorage.setItem("theme", next);
+ applyTheme(next);
+ });
+ }
+ })();
+ </script>
+ </body>
+</html>
+
+<!--
+ INCLUDE
+ <iframe
+ src="../play/play.html?tur=default.md&title=Title"
+ height="400px"/>
+-->
diff --git a/docs/play/player.js b/docs/play/player.js
new file mode 100644
index 0000000..796debe
--- /dev/null
+++ b/docs/play/player.js
@@ -0,0 +1,198 @@
+function parseTeachMarkdown(raw) {
+ raw = raw.replace(/^```\s*\n?/, "");
+
+ const blocks = raw.split(/^---\s*$/m);
+ const steps = [];
+
+ for (const block of blocks) {
+ const text = block.trim();
+ if (!text) continue;
+
+ const codes = [];
+ let match;
+ const codeRe = /```(rust|toml|bash|text)\n([\s\S]*?)```/g;
+ while ((match = codeRe.exec(text)) !== null) {
+ const lang = match[1];
+ const code = match[2].trimEnd();
+ codes.push({ lang, code });
+ }
+
+ const last = codes.length > 0 ? codes[codes.length - 1] : null;
+ const code = last ? { lang: last.lang, code: last.code } : null;
+
+ const speech = text
+ .replace(/```(rust|toml|bash|text)\n[\s\S]*?```/g, "")
+ .trim();
+ const paragraphs = speech
+ .split(/\n+/)
+ .map((s) => s.trim())
+ .filter(Boolean);
+
+ steps.push({ code, paragraphs });
+ }
+
+ let lastCode = "";
+ let lastLang = "rust";
+ let lastParagraphs = [];
+ for (const step of steps) {
+ if (step.code !== null) {
+ lastCode = step.code.code;
+ lastLang = step.code.lang;
+ step.code = { lang: lastLang, code: lastCode };
+ } else {
+ step.code = { lang: lastLang, code: lastCode };
+ }
+
+ if (step.paragraphs.length > 0) {
+ lastParagraphs = step.paragraphs;
+ } else {
+ step.paragraphs = lastParagraphs;
+ }
+ }
+
+ return steps;
+}
+
+function renderCode(codeInfo) {
+ if (!codeInfo || !codeInfo.code) return "";
+
+ const lang = codeInfo.lang || "rust";
+ const code = codeInfo.code;
+ const lines = code.split("\n");
+ const html = [];
+
+ const plainText = lang === "text";
+
+ for (let i = 0; i < lines.length; i++) {
+ const lineNum = i + 1;
+ const rawLine = lines[i];
+ const prefix = `<span class="line-num">${lineNum}</span>`;
+
+ if (plainText) {
+ html.push(`${prefix}${escapeHtml(rawLine)}`);
+ continue;
+ }
+
+ const hl = rawLine.match(/^(.*?)\s*<{8,}\s*"([^"]*?)"\s*$/);
+
+ if (hl) {
+ const indent = rawLine.match(/^\s*/)[0];
+ const hlCode = hljs.highlight(indent + hl[1].trimStart(), {
+ language: lang,
+ }).value;
+ html.push(
+ `${prefix}${hlCode} <span class="bubble">${escapeHtml(hl[2])}</span>`,
+ );
+ } else if (rawLine.trim() === "") {
+ html.push(prefix);
+ } else {
+ const hlLine = hljs.highlight(rawLine, { language: lang }).value;
+ html.push(`${prefix}${hlLine}`);
+ }
+ }
+
+ return html.join("\n");
+}
+
+function escapeHtml(text) {
+ return text
+ .replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+}
+
+function renderSpeech(paragraphs) {
+ return paragraphs
+ .map((p) => {
+ if (p.startsWith("> ")) {
+ return `<div class="cmd">${escapeHtml(p.slice(2))}</div>`;
+ }
+ let html = p
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
+ .replace(
+ /\*\*([^*]+)\*\*/g,
+ '<strong style="color:#7dcfff">$1</strong>',
+ );
+ return `<p>${html}</p>`;
+ })
+ .join("");
+}
+
+const dom = {
+ code: document.getElementById("codeDisplay"),
+ speech: document.getElementById("speechDisplay"),
+ stepNum: document.getElementById("stepNum"),
+ totalSteps: document.getElementById("totalSteps"),
+ progress: document.getElementById("progressFill"),
+ progressText: document.getElementById("progressText"),
+ prevBtn: document.getElementById("prevBtn"),
+ nextBtn: document.getElementById("nextBtn"),
+};
+
+let steps = [];
+let currentStep = 0;
+
+function goToStep(index) {
+ currentStep = Math.max(0, Math.min(index, steps.length - 1));
+ const step = steps[currentStep];
+
+ dom.code.innerHTML = renderCode(step.code);
+ dom.speech.innerHTML = renderSpeech(step.paragraphs);
+
+ dom.stepNum.textContent = currentStep + 1;
+ dom.totalSteps.textContent = steps.length;
+ const pct =
+ steps.length > 1
+ ? Math.round((currentStep / (steps.length - 1)) * 100)
+ : 100;
+ dom.progress.style.width = pct + "%";
+ dom.progressText.textContent = pct + "%";
+
+ dom.prevBtn.disabled = currentStep === 0;
+ const isLast = currentStep === steps.length - 1;
+ dom.nextBtn.disabled = false;
+ dom.nextBtn.textContent = isLast ? "🎉 COMPLETE" : "NEXT ▶";
+}
+
+dom.prevBtn.addEventListener("click", () => goToStep(currentStep - 1));
+dom.nextBtn.addEventListener("click", () => {
+ if (currentStep < steps.length - 1) goToStep(currentStep + 1);
+});
+
+document.addEventListener("keydown", (e) => {
+ if (e.key === "ArrowRight" || e.key === " " || e.key === "Enter") {
+ e.preventDefault();
+ if (currentStep < steps.length - 1) goToStep(currentStep + 1);
+ } else if (e.key === "ArrowLeft") {
+ e.preventDefault();
+ if (currentStep > 0) goToStep(currentStep - 1);
+ }
+});
+
+async function loadTutorial(filePath) {
+ try {
+ const resp = await fetch(filePath);
+ const raw = await resp.text();
+ steps = parseTeachMarkdown(raw);
+ dom.totalSteps.textContent = steps.length;
+ goToStep(0);
+ } catch (e) {
+ dom.code.textContent = "Load tutorial failed: " + e.message;
+ }
+}
+
+const params = new URLSearchParams(window.location.search);
+const tutorialFile = "sources/" + (params.get("tur") || "default.md");
+
+const title = params.get("title");
+if (title) {
+ document.getElementById("tutorialTitle").textContent = title;
+}
+
+const layoutEl = document.getElementById("layout");
+const speechPanel = document.querySelector(".speech-panel");
+const codePanel = document.querySelector(".code-panel");
+layoutEl.append(speechPanel, codePanel);
+layoutEl.classList.add("layout--tb");
+
+loadTutorial(tutorialFile);
diff --git a/docs/play/style.css b/docs/play/style.css
new file mode 100644
index 0000000..7a6f27d
--- /dev/null
+++ b/docs/play/style.css
@@ -0,0 +1,337 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --bg-primary: #1a1b1c;
+ --bg-secondary: #1f2021;
+ --border: #2f3031;
+ --text-primary: #c0c1c2;
+ --text-secondary: #7a7b7c;
+ --text-muted: #565758;
+ --code-bg: #292a2b;
+ --code-text: #bbbcbd;
+ --bubble-bg: #e64a199f;
+ --bubble-text: #ffccbc;
+ --line-num: #3b3c3d;
+ --btn-bg: #2f3031;
+ --btn-text: #c0c1c2;
+ --btn-hover: #3b3c3d;
+ --btn-primary-bg: #7a7b7c;
+ --btn-primary-text: #1a1b1c;
+ --btn-primary-hover: #898a8b;
+ --progress-bg: #2f3031;
+ --progress-fill: #7a7b7c;
+ --cmd-bg: #1a1b1c;
+ --cmd-border: #2f3031;
+ --cmd-text: #9e9fa0;
+ --topbar-bg: #1f2021;
+ --hljs-bg: #1a1b1c;
+}
+
+[data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f6f7;
+ --border: #dcddde;
+ --text-primary: #2c2d2e;
+ --text-secondary: #5a5b5c;
+ --text-muted: #8a8b8c;
+ --code-bg: #e8e9ea;
+ --code-text: #2c2d2e;
+ --bubble-bg: #e64a19cf;
+ --bubble-text: #ffffff;
+ --line-num: #b0b1b2;
+ --btn-bg: #dcddde;
+ --btn-text: #2c2d2e;
+ --btn-hover: #c8c9ca;
+ --btn-primary-bg: #3a7bd5;
+ --btn-primary-text: #ffffff;
+ --btn-primary-hover: #2a6bc5;
+ --progress-bg: #dcddde;
+ --progress-fill: #3a7bd5;
+ --cmd-bg: #f5f6f7;
+ --cmd-border: #dcddde;
+ --cmd-text: #5a5b5c;
+ --topbar-bg: #f5f6f7;
+ --hljs-bg: #f5f6f7;
+}
+
+body {
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC",
+ sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Top bar */
+.topbar {
+ padding: 12px 24px;
+ background: var(--topbar-bg);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
+}
+
+.topbar__title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.topbar__step {
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+/* Theme toggle button */
+.theme-toggle {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--text-primary);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 4px 10px;
+ border-radius: 6px;
+ transition: all 0.15s;
+ font-family: inherit;
+ line-height: 1;
+}
+
+.theme-toggle:hover {
+ background: var(--btn-hover);
+}
+
+/* Main panel */
+.layout {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* Left: code panel */
+.code-panel {
+ flex: 1;
+ background: var(--bg-primary);
+ overflow: hidden;
+}
+
+.code-panel pre {
+ height: 100%;
+ margin: 0;
+ padding: 32px;
+ overflow: auto;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 15px;
+ line-height: 1.7;
+ color: var(--text-primary);
+ tab-size: 4;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.code-panel pre .hljs {
+ background: transparent;
+}
+
+.code-panel pre code.hljs {
+ font-family: inherit;
+ background: transparent;
+ padding: 0;
+}
+
+/* Bubble comment */
+.code-panel pre .bubble {
+ display: inline-block;
+ position: relative;
+ margin-left: 8px;
+ padding: 0 12px;
+ background: var(--bubble-bg);
+ color: var(--bubble-text);
+ border-radius: 10px;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC",
+ sans-serif;
+ font-size: 13px;
+ font-style: normal;
+ line-height: 24px;
+ white-space: nowrap;
+ cursor: default;
+}
+
+.code-panel pre .bubble::before {
+ content: "";
+ position: absolute;
+ left: -6px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 0;
+ height: 0;
+ border-top: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+ border-right: 6px solid var(--bubble-bg);
+}
+
+/* Line number */
+.code-panel .line-num {
+ display: inline-block;
+ width: 32px;
+ text-align: right;
+ margin-right: 16px;
+ color: var(--line-num);
+ user-select: none;
+}
+
+/* Right: description panel */
+.speech-panel {
+ width: 380px;
+ background: var(--bg-secondary);
+ border-left: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.layout--tb {
+ flex-direction: column;
+}
+
+.layout--tb .speech-panel {
+ width: 100%;
+ border-left: none;
+ border-bottom: 1px solid var(--border);
+ max-height: none;
+ flex: 0 0 auto;
+}
+
+.layout--tb .code-panel {
+ flex: 1;
+ min-height: 0;
+}
+
+.speech-content {
+ flex: 1;
+ padding: 32px 24px;
+ overflow-y: auto;
+}
+
+.speech-content p {
+ font-size: 15px;
+ line-height: 1.8;
+ color: var(--text-primary);
+ margin-bottom: 16px;
+}
+
+.speech-content p:last-child {
+ margin-bottom: 0;
+}
+
+.speech-content code {
+ background: var(--code-bg);
+ color: var(--code-text);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 13px;
+}
+
+.speech-content .cmd {
+ display: block;
+ background: var(--cmd-bg);
+ border: 1px solid var(--cmd-border);
+ border-radius: 6px;
+ padding: 10px 14px;
+ margin: 12px 0;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 14px;
+ color: var(--cmd-text);
+}
+
+/* Control panel */
+.controls {
+ padding: 12px 24px;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.controls button {
+ background: var(--btn-bg);
+ border: none;
+ color: var(--btn-text);
+ padding: 8px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.15s;
+ font-family: inherit;
+}
+
+.controls button:hover {
+ background: var(--btn-hover);
+}
+
+.controls button:active {
+ transform: scale(0.97);
+}
+
+.controls__btn--primary {
+ background: var(--btn-primary-bg);
+ color: var(--btn-primary-text);
+ font-weight: 600;
+}
+
+.controls__btn--primary:hover {
+ background: var(--btn-primary-hover);
+}
+
+.controls button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.controls__progress {
+ flex: 1;
+ height: 4px;
+ background: var(--progress-bg);
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.controls__progress-fill {
+ height: 100%;
+ background: var(--progress-fill);
+ border-radius: 2px;
+ transition: width 0.3s;
+}
+
+.controls__text {
+ font-size: 13px;
+ color: var(--text-muted);
+ min-width: 60px;
+ text-align: right;
+}
+
+@media (max-width: 800px) {
+ .layout {
+ flex-direction: column;
+ }
+
+ .speech-panel {
+ width: 100%;
+ border-left: none;
+ border-top: 1px solid var(--border);
+ max-height: 40vh;
+ }
+}