// Client-side rendering: syntax highlighting for source files and markdown // rendering for READMEs and .md files. (function () { "use strict"; // Map a filename to an explicit highlight.js language. Resolution order: // exact full-name match, then filename patterns, then extension. Anything // that resolves here overrides highlight.js auto-detection. var FILENAME_LANGUAGES = { "CMakeLists.txt": "cmake" }; // Patterns matched against the whole filename, in order. var FILENAME_PATTERNS = [ { test: /^\..*ignore$/i, language: "plaintext" }, // .gitignore, .dockerignore, ... { test: /^Dockerfile/i, language: "dockerfile" }, // Dockerfile, Dockerfile.dev, ... ]; var EXTENSION_LANGUAGES = { cpp: "cpp", hpp: "cpp", js: "javascript", json: "json", yml: "yaml", txt: "plaintext", man: "plaintext", // no troff/man grammar in highlight.js; pin to plain text py: "python", tf: "terraform", sql: "sql", patch: "diff", // patch files are unified diffs }; function languageForFilename(filename) { if (!filename) return null; if (FILENAME_LANGUAGES[filename]) return FILENAME_LANGUAGES[filename]; for (var i = 0; i < FILENAME_PATTERNS.length; i++) { if (FILENAME_PATTERNS[i].test.test(filename)) return FILENAME_PATTERNS[i].language; } var dot = filename.lastIndexOf("."); // Extensionless files (LICENSE, README, etc.) are treated as plain text // rather than letting auto-detection guess a language. if (dot === -1) return "plaintext"; return EXTENSION_LANGUAGES[filename.slice(dot + 1).toLowerCase()] || null; } function highlight() { if (typeof hljs === "undefined") return; document.querySelectorAll("pre.code > code.hljs").forEach(function (el) { var lang = languageForFilename(el.dataset.filename); if (lang) el.classList.add("language-" + lang); hljs.highlightElement(el); var detected = el.result && el.result.language ? el.result.language : "plaintext"; var label = document.createElement("p"); label.className = "lang-detected"; label.textContent = "Detected language: " + detected; el.parentElement.insertAdjacentElement("afterend", label); }); } // Each ".markdown-src" element holds raw markdown and a data-target CSS // selector pointing at the element to render it into. function renderMarkdown() { if (typeof marked === "undefined" || typeof DOMPurify === "undefined") return; document.querySelectorAll(".markdown-src").forEach(function (src) { var target = document.querySelector(src.dataset.target); // Repository markdown is untrusted: marked passes embedded HTML through // verbatim, so the result must be sanitized before insertion. if (target) target.innerHTML = DOMPurify.sanitize(marked.parse(src.textContent)); }); } // Single toggle above the log that shows or hides every commit body at once. function setupLogToggle() { var btn = document.querySelector(".log-toggle"); if (!btn) return; btn.addEventListener("click", function () { var on = btn.getAttribute("aria-pressed") !== "true"; btn.setAttribute("aria-pressed", on ? "true" : "false"); btn.textContent = "expand: " + (on ? "on" : "off"); document.querySelectorAll(".commit-body").forEach(function (el) { el.hidden = !on; }); }); } // Toggle between the file list table and a thumbnail grid. The choice is // remembered in localStorage so it persists while browsing into subfolders. function setupViewToggle() { var btn = document.querySelector(".view-toggle"); if (!btn) return; var table = document.querySelector("table.tree"); var grid = document.querySelector(".tree-grid"); if (!table || !grid) return; function apply(grid_on) { btn.setAttribute("aria-pressed", grid_on ? "true" : "false"); btn.textContent = "view: " + (grid_on ? "grid" : "list"); table.hidden = grid_on; grid.hidden = !grid_on; } apply(localStorage.getItem("treeView") === "grid"); btn.addEventListener("click", function () { var grid_on = btn.getAttribute("aria-pressed") !== "true"; localStorage.setItem("treeView", grid_on ? "grid" : "list"); apply(grid_on); }); } // Light/dark theme toggle. The effective theme is applied to the document // by an inline script in before first paint; here we only reflect it // in the button and let the user switch and persist a choice. function setupThemeToggle() { var btn = document.querySelector(".theme-toggle"); if (!btn) return; var root = document.documentElement; function current() { return root.getAttribute("data-theme") === "dark" ? "dark" : "light"; } function render() { var dark = current() === "dark"; btn.textContent = dark ? "☀️" : "🌙"; // sun / moon btn.setAttribute("aria-label", "Switch to " + (dark ? "light" : "dark") + " theme"); btn.setAttribute("aria-pressed", dark ? "true" : "false"); } render(); btn.addEventListener("click", function () { var next = current() === "dark" ? "light" : "dark"; root.setAttribute("data-theme", next); try { localStorage.setItem("theme", next); } catch (e) { /* private mode: no persistence */ } render(); }); } document.addEventListener("DOMContentLoaded", function () { highlight(); renderMarkdown(); setupLogToggle(); setupViewToggle(); setupThemeToggle(); }); })();