/* ObsiGate — Vanilla JS SPA */ (function () { "use strict"; // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let currentVault = null; let currentPath = null; let searchTimeout = null; // --------------------------------------------------------------------------- // Safe CDN helpers // --------------------------------------------------------------------------- function safeCreateIcons() { if (typeof lucide !== "undefined" && lucide.createIcons) { try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ } } } function safeHighlight(block) { if (typeof hljs !== "undefined" && hljs.highlightElement) { try { hljs.highlightElement(block); } catch (e) { /* CDN not loaded */ } } } // --------------------------------------------------------------------------- // Theme // --------------------------------------------------------------------------- function initTheme() { const saved = localStorage.getItem("obsigate-theme") || "dark"; applyTheme(saved); } function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("obsigate-theme", theme); const icon = document.getElementById("theme-icon"); if (icon) { icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun"); safeCreateIcons(); } // Swap highlight.js theme const darkSheet = document.getElementById("hljs-theme-dark"); const lightSheet = document.getElementById("hljs-theme-light"); if (darkSheet && lightSheet) { darkSheet.disabled = theme !== "dark"; lightSheet.disabled = theme !== "light"; } } function toggleTheme() { const current = document.documentElement.getAttribute("data-theme"); applyTheme(current === "dark" ? "light" : "dark"); } // --------------------------------------------------------------------------- // API helpers // --------------------------------------------------------------------------- async function api(path) { const res = await fetch(path); if (!res.ok) throw new Error(`API error: ${res.status}`); return res.json(); } // --------------------------------------------------------------------------- // Sidebar — Vault tree // --------------------------------------------------------------------------- async function loadVaults() { const vaults = await api("/api/vaults"); const container = document.getElementById("vault-tree"); const filter = document.getElementById("vault-filter"); container.innerHTML = ""; vaults.forEach((v) => { // Sidebar tree entry const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [ icon("chevron-right", 14), icon("database", 16), document.createTextNode(` ${v.name} `), smallBadge(v.file_count), ]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); container.appendChild(vaultItem); const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` }); container.appendChild(childContainer); // Vault filter dropdown const opt = document.createElement("option"); opt.value = v.name; opt.textContent = v.name; filter.appendChild(opt); }); safeCreateIcons(); } async function toggleVault(itemEl, vaultName) { const childContainer = document.getElementById(`vault-children-${vaultName}`); if (!childContainer) return; if (childContainer.classList.contains("collapsed")) { // Expand — load children if empty if (childContainer.children.length === 0) { await loadDirectory(vaultName, "", childContainer); } childContainer.classList.remove("collapsed"); // Swap chevron const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { childContainer.classList.add("collapsed"); const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } } async function loadDirectory(vaultName, dirPath, container) { const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`; const data = await api(url); container.innerHTML = ""; data.items.forEach((item) => { if (item.type === "directory") { const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [ icon("chevron-right", 14), icon("folder", 16), document.createTextNode(` ${item.name} `), smallBadge(item.children_count), ]); container.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); container.appendChild(subContainer); dirItem.addEventListener("click", async () => { if (subContainer.classList.contains("collapsed")) { if (subContainer.children.length === 0) { await loadDirectory(vaultName, item.path, subContainer); } subContainer.classList.remove("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { subContainer.classList.add("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } }); } else { const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [ icon("file-text", 16), document.createTextNode(` ${item.name.replace(/\.md$/i, "")}`), ]); fileItem.addEventListener("click", () => openFile(vaultName, item.path)); container.appendChild(fileItem); } }); safeCreateIcons(); } // --------------------------------------------------------------------------- // Tags // --------------------------------------------------------------------------- async function loadTags() { const data = await api("/api/tags"); const cloud = document.getElementById("tag-cloud"); cloud.innerHTML = ""; const tags = data.tags; const counts = Object.values(tags); if (counts.length === 0) return; const maxCount = Math.max(...counts); const minSize = 0.7; const maxSize = 1.25; Object.entries(tags).forEach(([tag, count]) => { const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0; const size = minSize + ratio * (maxSize - minSize); const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [ document.createTextNode(`#${tag}`), ]); tagEl.addEventListener("click", () => searchByTag(tag)); cloud.appendChild(tagEl); }); } function searchByTag(tag) { const input = document.getElementById("search-input"); input.value = ""; performSearch("", "all", tag); } // --------------------------------------------------------------------------- // File viewer // --------------------------------------------------------------------------- async function openFile(vaultName, filePath) { currentVault = vaultName; currentPath = filePath; // Highlight active document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active")); const selector = `.tree-item[data-vault="${vaultName}"][data-path="${filePath}"]`; const active = document.querySelector(selector); if (active) active.classList.add("active"); const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`; const data = await api(url); renderFile(data); } function renderFile(data) { const area = document.getElementById("content-area"); // Breadcrumb const parts = data.path.split("/"); const breadcrumbEls = []; breadcrumbEls.push(makeBreadcrumbSpan(data.vault, () => {})); let accumulated = ""; parts.forEach((part, i) => { breadcrumbEls.push(el("span", { class: "sep" }, [document.createTextNode(" / ")])); accumulated += (accumulated ? "/" : "") + part; const p = accumulated; if (i < parts.length - 1) { breadcrumbEls.push(makeBreadcrumbSpan(part, () => {})); } else { breadcrumbEls.push(el("span", {}, [document.createTextNode(part.replace(/\.md$/i, ""))])); } }); const breadcrumb = el("div", { class: "breadcrumb" }, breadcrumbEls); // Tags const tagsDiv = el("div", { class: "file-tags" }); (data.tags || []).forEach((tag) => { const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); t.addEventListener("click", () => searchByTag(tag)); tagsDiv.appendChild(t); }); // Copy path button const copyBtn = el("button", { class: "btn-copy-path" }, [document.createTextNode("Copier le chemin")]); copyBtn.addEventListener("click", () => { navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => { copyBtn.textContent = "Copié !"; setTimeout(() => (copyBtn.textContent = "Copier le chemin"), 1500); }); }); // Frontmatter let fmSection = null; if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { const fmToggle = el("div", { class: "frontmatter-toggle" }, [ document.createTextNode("▶ Frontmatter"), ]); const fmContent = el("div", { class: "frontmatter-content" }, [ document.createTextNode(JSON.stringify(data.frontmatter, null, 2)), ]); fmToggle.addEventListener("click", () => { fmContent.classList.toggle("open"); fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter"; }); fmSection = el("div", {}, [fmToggle, fmContent]); } // Markdown content const mdDiv = el("div", { class: "md-content" }); mdDiv.innerHTML = data.html; // Assemble area.innerHTML = ""; area.appendChild(breadcrumb); area.appendChild(el("div", { class: "file-header" }, [ el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn]), ])); if (fmSection) area.appendChild(fmSection); area.appendChild(mdDiv); // Highlight code blocks area.querySelectorAll("pre code").forEach((block) => { safeHighlight(block); }); // Wire up wikilinks area.querySelectorAll(".wikilink").forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const v = link.getAttribute("data-vault"); const p = link.getAttribute("data-path"); if (v && p) openFile(v, p); }); }); area.scrollTop = 0; } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- function initSearch() { const input = document.getElementById("search-input"); input.addEventListener("input", () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; if (q.length > 0) { performSearch(q, vault, null); } else { showWelcome(); } }, 300); }); } async function performSearch(query, vaultFilter, tagFilter) { let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; const data = await api(url); renderSearchResults(data, query, tagFilter); } function renderSearchResults(data, query, tagFilter) { const area = document.getElementById("content-area"); area.innerHTML = ""; const header = el("div", { class: "search-results-header" }); if (query) { header.textContent = `${data.count} résultat(s) pour "${query}"`; } else if (tagFilter) { header.textContent = `${data.count} fichier(s) avec le tag #${tagFilter}`; } area.appendChild(header); if (data.results.length === 0) { area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [ document.createTextNode("Aucun résultat trouvé."), ])); return; } const container = el("div", { class: "search-results" }); data.results.forEach((r) => { const item = el("div", { class: "search-result-item" }, [ el("div", { class: "search-result-title" }, [document.createTextNode(r.title)]), el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), el("div", { class: "search-result-snippet" }, [document.createTextNode(r.snippet || "")]), ]); if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { tagsDiv.appendChild(el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)])); }); item.appendChild(tagsDiv); } item.addEventListener("click", () => openFile(r.vault, r.path)); container.appendChild(item); }); area.appendChild(container); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function el(tag, attrs, children) { const e = document.createElement(tag); if (attrs) { Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v)); } if (children) { children.forEach((c) => { if (c) e.appendChild(c); }); } return e; } function icon(name, size) { const i = document.createElement("i"); i.setAttribute("data-lucide", name); i.style.width = size + "px"; i.style.height = size + "px"; i.classList.add("icon"); return i; } function smallBadge(count) { const s = document.createElement("span"); s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px"; s.textContent = `(${count})`; return s; } function makeBreadcrumbSpan(text, onClick) { const s = document.createElement("span"); s.textContent = text; if (onClick) s.addEventListener("click", onClick); return s; } function showWelcome() { const area = document.getElementById("content-area"); area.innerHTML = `

ObsiGate

Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.

`; safeCreateIcons(); } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- async function init() { initTheme(); document.getElementById("theme-toggle").addEventListener("click", toggleTheme); initSearch(); try { await Promise.all([loadVaults(), loadTags()]); } catch (err) { console.error("Failed to initialize ObsiGate:", err); } safeCreateIcons(); } document.addEventListener("DOMContentLoaded", init); })();