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 = `${lineNum}`; 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} ${escapeHtml(hl[2])}`, ); } 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, ">"); } function renderSpeech(paragraphs) { return paragraphs .map((p) => { if (p.startsWith("> ")) { return `
${escapeHtml(p.slice(2))}
`; } let html = p .replace(/`([^`]+)`/g, "$1") .replace( /\*\*([^*]+)\*\*/g, '$1', ); return `

${html}

`; }) .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);