diff options
Diffstat (limited to 'docs/play')
| -rw-r--r-- | docs/play/play.html | 135 | ||||
| -rw-r--r-- | docs/play/player.js | 198 | ||||
| -rw-r--r-- | docs/play/style.css | 337 |
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, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); +} + +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; + } +} |
