diff options
Diffstat (limited to 'docs/play/player.js')
| -rw-r--r-- | docs/play/player.js | 198 |
1 files changed, 198 insertions, 0 deletions
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); |
