(function () { "use strict"; const content = window.LIQUID_JAVA_TUTORIAL; const steps = [ { id: "welcome", shortTitle: "Welcome" }, ...content.lessons.map((lesson) => ({ id: lesson.id, shortTitle: lesson.shortTitle })), ]; const appScriptUrl = new URL(document.currentScript.src); const basePath = appScriptUrl.pathname.slice(0, appScriptUrl.pathname.lastIndexOf("/") + 1); const blankState = () => ({ currentStep: 0, completed: [], code: Object.fromEntries(content.lessons.map((lesson) => [lesson.id, lesson.exercise.starterCode])), answers: {}, checkResults: {}, }); let state = blankState(); const screen = document.querySelector("#screen"); const navigation = document.querySelector("#step-navigation"); const resetButton = document.querySelector("#reset-button"); const brandLink = document.querySelector(".brand"); function stepIndexFromPath() { const relativePath = window.location.pathname.startsWith(basePath) ? window.location.pathname.slice(basePath.length) : ""; const route = decodeURIComponent(relativePath).replace(/^\/+|\/+$/g, ""); if (!route || route === "index.html" || route === "404.html") return 0; return steps.findIndex((step) => step.id === route); } function pathForStep(index) { const step = steps[index]; return step.id === "welcome" ? basePath : `${basePath}${encodeURIComponent(step.id)}`; } function updatePath(index, mode = "push") { const path = pathForStep(index); if (window.location.pathname === path) return; window.history[`${mode}State`]({ step: steps[index].id }, "", path); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function render() { renderNavigation(); const step = steps[state.currentStep]; document.title = step.id === "welcome" ? content.meta.title : `${step.shortTitle} · ${content.meta.title}`; if (step.id === "welcome") renderWelcome(); else renderLesson(content.lessons.find((lesson) => lesson.id === step.id)); updateProgress(); window.Prism?.highlightAllUnder(screen); window.scrollTo({ top: 0, behavior: "smooth" }); } function renderNavigation() { navigation.innerHTML = `
    ${steps .map((step, index) => { const active = index === state.currentStep; const complete = state.completed.includes(step.id); return `
  1. `; }) .join("")}
`; navigation.querySelectorAll("[data-step]").forEach((button) => { button.addEventListener("click", () => goToStep(Number(button.dataset.step))); }); } function updateProgress() { const completedCount = state.completed.length; const percentage = Math.round((completedCount / steps.length) * 100); document.querySelector("#progress-percent").textContent = `${percentage}%`; document.querySelector("#progress-bar").style.width = `${percentage}%`; document.querySelector("#progress-copy").textContent = completedCount ? `${completedCount} of ${steps.length} steps complete.` : "Start when you are ready."; } function renderWelcome() { screen.innerHTML = `

${escapeHtml(content.meta.eyebrow)}

Catch the bug
before it runs.

${escapeHtml(content.meta.introduction)}

What you’ll practice

    ${content.lessons .map( (lesson) => `
  1. ${lesson.number} ${escapeHtml(lesson.shortTitle)}
  2. `, ) .join("")}

No setup required. You can skip any activity and return later.

`; document.querySelector("#begin-button").addEventListener("click", () => { markComplete("welcome"); goToStep(1); }); } function renderLesson(lesson) { const currentCode = state.code[lesson.id] || lesson.exercise.starterCode; const checked = state.checkResults[lesson.id]; const isLastLesson = state.currentStep === steps.length - 1; screen.innerHTML = `

Lesson ${lesson.number}

${escapeHtml(lesson.title)}

${escapeHtml(lesson.lead)}

${escapeHtml(lesson.concept.explanation)}

    ${lesson.concept.notes.map((note) => `
  • ${escapeHtml(note)}
  • `).join("")}
${escapeHtml(lesson.concept.code)}

${escapeHtml(lesson.exercise.title)}

${escapeHtml(lesson.exercise.prompt)}

Java
${ lesson.exercise.guide ? `
${escapeHtml(lesson.exercise.guide.alt)}
${escapeHtml(lesson.exercise.guide.caption)}
` : "" }
${renderExerciseFeedback(checked)}

Explain what LiquidJava knows

${lesson.questions.map((question, index) => renderQuestion(question, index + 1)).join("")}
${renderFooterControls(state.currentStep, isLastLesson ? "Finish tutorial" : `Complete lesson ${lesson.number}`)}
`; wireLesson(lesson); wireQuestions(lesson.questions); wireFooterControls(lesson.id, isLastLesson); } function renderExerciseFeedback(result) { if (!result) return "

Run the check when you are ready. This checker looks for the contract, not exact formatting.

"; if (result.passed) { return '

Contract satisfied. Your annotations express the requested guarantee.

'; } return `

Almost there.

`; } function wireLesson(lesson) { const textarea = document.querySelector(`#code-${lesson.id}`); const feedback = document.querySelector(`#exercise-feedback-${lesson.id}`); const solutionButton = document.querySelector(`#solution-${lesson.id}`); const solutionPanel = document.querySelector(`#solution-panel-${lesson.id}`); const editor = window.CodeMirror ? window.CodeMirror.fromTextArea(textarea, { mode: "text/x-java", theme: "liquidjava", lineNumbers: true, lineWrapping: true, indentUnit: 4, tabSize: 4, indentWithTabs: false, matchBrackets: true, autoCloseBrackets: true, inputStyle: "textarea", screenReaderLabel: `Editable Java exercise: ${lesson.exercise.title}`, extraKeys: { Tab(codeMirror) { if (codeMirror.somethingSelected()) codeMirror.indentSelection("add"); else codeMirror.execCommand("insertSoftTab"); }, "Shift-Tab": "indentLess", }, }) : null; const getCode = () => (editor ? editor.getValue() : textarea.value); const setCode = (value) => { if (editor) editor.setValue(value); else { textarea.value = value; textarea.dispatchEvent(new Event("input")); } }; const focusEditor = () => (editor ? editor.focus() : textarea.focus()); const handleCodeChange = () => { state.code[lesson.id] = getCode(); delete state.checkResults[lesson.id]; feedback.className = "exercise-feedback"; feedback.innerHTML = renderExerciseFeedback(null); }; if (editor) editor.on("change", handleCodeChange); else textarea.addEventListener("input", handleCodeChange); document.querySelector(`#check-${lesson.id}`).addEventListener("click", () => { const failures = lesson.exercise.checks.filter((check) => !new RegExp(check.pattern, "m").test(getCode())); const result = { passed: failures.length === 0, messages: failures.map((check) => check.message) }; state.checkResults[lesson.id] = result; feedback.className = `exercise-feedback ${result.passed ? "is-success" : "is-error"}`; feedback.innerHTML = renderExerciseFeedback(result); feedback.scrollIntoView({ behavior: "smooth", block: "nearest" }); }); document.querySelector(`#reset-${lesson.id}`).addEventListener("click", () => { setCode(lesson.exercise.starterCode); focusEditor(); }); solutionButton.addEventListener("click", () => { const isHidden = solutionPanel.hidden; solutionPanel.hidden = !isHidden; solutionButton.setAttribute("aria-expanded", String(isHidden)); solutionButton.textContent = isHidden ? "Hide solution" : "Show one solution"; }); } function renderQuestion(question, number) { const currentAnswer = state.answers[question.id] ?? ""; const feedback = questionFeedback(question, currentAnswer); const heading = `
${number}

${escapeHtml(question.prompt)}

`; if (question.type === "text") { return `
${heading}
${feedback}
`; } return `
${escapeHtml(question.prompt)} ${heading}
${question.choices .map( (choice, index) => ``, ) .join("")}
${feedback}
`; } function questionFeedback(question, value) { if (value === "" || value === undefined) return ""; const correct = question.type === "text" ? question.accepted.some((answer) => answer.toLowerCase() === String(value).trim().toLowerCase()) : Number(value) === question.correct; return `

${correct ? "That’s right." : "Try once more."} ${escapeHtml(question.explanation || "")}

`; } function wireQuestions(questions) { questions.forEach((question) => { if (question.type === "text") { const input = document.querySelector(`#answer-${question.id}`); input.addEventListener("input", () => { state.answers[question.id] = input.value; document.querySelector(`#feedback-${question.id}`).innerHTML = questionFeedback(question, input.value); }); return; } document.querySelectorAll(`input[name="${question.id}"]`).forEach((input) => { input.addEventListener("change", () => { state.answers[question.id] = input.value; const feedback = document.querySelector(`#feedback-${question.id}`); if (feedback) feedback.innerHTML = questionFeedback(question, input.value); }); }); }); } function renderFooterControls(stepIndex, nextLabel) { return ``; } function wireFooterControls(stepId, isFinal = false) { document.querySelector("#previous-button").addEventListener("click", () => goToStep(state.currentStep - 1)); document.querySelector("#next-button").addEventListener("click", () => { markComplete(stepId); if (isFinal) { showFinishedState(); } else { goToStep(Math.min(state.currentStep + 1, steps.length - 1)); } }); } function showFinishedState() { const footer = document.querySelector(".lesson-footer"); footer.innerHTML = `
Tutorial complete.

You finished all four guided examples.

`; document.querySelector("#review-button").addEventListener("click", () => goToStep(1)); updateProgress(); } function markComplete(stepId) { if (!state.completed.includes(stepId)) state.completed.push(stepId); } function goToStep(index, historyMode = "push") { state.currentStep = Math.max(0, Math.min(index, steps.length - 1)); updatePath(state.currentStep, historyMode); render(); document.querySelector("#tutorial-main").focus({ preventScroll: true }); } resetButton.addEventListener("click", () => { if (!window.confirm("Reset this tutorial session, including edited code and answers?")) return; state = blankState(); goToStep(0, "replace"); }); brandLink.addEventListener("click", (event) => { event.preventDefault(); goToStep(0); }); window.addEventListener("popstate", () => { const routeIndex = stepIndexFromPath(); state.currentStep = routeIndex >= 0 ? routeIndex : 0; render(); document.querySelector("#tutorial-main").focus({ preventScroll: true }); }); const initialRouteIndex = stepIndexFromPath(); state.currentStep = initialRouteIndex >= 0 ? initialRouteIndex : 0; if (initialRouteIndex < 0) updatePath(0, "replace"); render(); })();