/** * app.js – v3.3.3 FIELD COMMENTARY EDITION * High-coherence, readable, maintainable blueprint. * No hacks. No surgery. Only truth. * Enhanced with modular sanitization, resilient recursion, and inline rationale. * * ΔFIELD: This script orchestrates the breathing field: navigation, routing, rendering. * Rationale: Dependency-free; async fetches for dynamic content; stateful only where essential (e.g., sidebar). * Assumptions: index.json provides metadata; marked.js for MD; DOM elements pre-exist in HTML skeleton. */ const els = { menuBtn: document.getElementById("menuBtn"), /* ΔHORIZON: Toggle for sidebar. */ primaryNav: document.getElementById("primaryNav"), /* ΔHORIZON: Top-level sections. */ subNav: document.getElementById("subNav"), /* ΔRECURSION: Nested sub-horizons. */ sectionSelect: document.getElementById("sectionSelect"), /* ΔFIELD: Filter by section. */ tagSelect: document.getElementById("tagSelect"), /* ΔFIELD: Multi-tag filter. */ sortSelect: document.getElementById("sortSelect"), /* ΔRHYTHM: Time-based sorting. */ searchMode: document.getElementById("searchMode"), /* ΔTRUTH: Scope of search. */ searchBox: document.getElementById("searchBox"), /* ΔTRUTH: Query input. */ postList: document.getElementById("postList"), /* ΔFIELD: Dynamic post enumeration. */ viewer: document.getElementById("viewer"), /* ΔFIELD: Content rendering canvas. */ content: document.getElementById("content"), /* ΔHORIZON: Main wrapper for click events. */ toggleControls: document.getElementById("toggleControls"), /* ΔHORIZON: Filter panel toggle. */ filterPanel: document.getElementById("filterPanel") /* ΔFIELD: Collapsible filters. */ }; let indexData = null; /* ΔRECURSION: Cached metadata for all operations. */ let sidebarOpen = false; /* ΔBREATH: State for mobile sidebar. */ let currentParent = null; /* ΔRECURSION: Track for subnav rendering. */ let indexFiles = null; // Cached index files /* ΔRECURSION: Pre-filtered for quick lookups. */ // === INITIALIZATION === /* ΔFIELD: Async init loads data and wires UI; fallback for errors. * Rationale: Single entry point; console log as harmony affirmation. */ async function init() { try { indexData = await (await fetch("index.json")).json(); /* ΔTRUTH: Source of all content truth. */ indexFiles = indexData.flat.filter(f => f.isIndex); /* ΔRECURSION: Cache for directory indices. */ populateNav(); /* ΔHORIZON: Build primary nav from sections. */ populateSections(); /* ΔFIELD: Populate section dropdown. */ populateTags(); /* ΔFIELD: Populate tag multi-select. */ wireUI(); /* ΔHORIZON: Attach all event listeners. */ renderList(); /* ΔFIELD: Initial post list render. */ handleHash(); /* ΔRECURSION: Process current URL state. */ window.addEventListener("hashchange", handleHash); /* ΔRECURSION: Listen for navigation. */ console.info('%cThe Fold Within: Harmony sustained.', 'color:#e0b84b'); /* ΔFIELD: Dev resonance. */ } catch (e) { els.viewer.innerHTML = "
Failed to load site data.
"; /* ΔTRUTH: Graceful failure. */ } } // === NAVIGATION === /* ΔHORIZON: Dynamically build primary nav from unique top-level sections. * Rationale: Sort for alphabetical order; capitalize for aesthetics. */ function populateNav() { els.primaryNav.innerHTML = 'Home'; /* ΔFIELD: Fixed home anchor. */ const navSections = [...new Set( indexData.flat .filter(f => f.isIndex && f.path.split("/").length > 1) .map(f => f.path.split("/")[0]) )].sort(); navSections.forEach(s => { els.primaryNav.innerHTML += `${s.charAt(0).toUpperCase() + s.slice(1)}`; }); } /* ΔFIELD: Section dropdown with default to 'posts' if available. * Rationale: 'all' option for broad views. */ function populateSections() { els.sectionSelect.innerHTML = ''; indexData.sections.forEach(s => { const opt = document.createElement("option"); opt.value = s; opt.textContent = s; els.sectionSelect.appendChild(opt); }); const defaultSection = indexData.sections.includes("posts") ? "posts" : indexData.sections[0]; if (defaultSection) els.sectionSelect.value = defaultSection; } /* ΔFIELD: Tags as multi-select options. * Rationale: Lowercase normalization in filters for case-insensitivity. */ function populateTags() { indexData.tags.forEach(t => { const opt = document.createElement("option"); opt.value = t; opt.textContent = t; els.tagSelect.appendChild(opt); }); } // === UI WIRING === /* ΔHORIZON: Attach listeners for interactivity. * Rationale: Centralized wiring; mobile-specific sidebar close on content click. */ function wireUI() { els.menuBtn.addEventListener("click", () => { sidebarOpen = !sidebarOpen; document.body.classList.toggle("sidebar-open", sidebarOpen); /* ΔBREATH: Class toggle for CSS-driven motion. */ }); els.toggleControls.addEventListener("click", () => { const open = els.filterPanel.open; els.filterPanel.open = !open; els.toggleControls.textContent = open ? "Filters" : "Hide"; /* ΔFIELD: Dynamic label for state clarity. */ }); els.sectionSelect.addEventListener("change", () => { renderList(); if (els.sectionSelect.value !== "all") loadDefaultForSection(els.sectionSelect.value); /* ΔRECURSION: Auto-load default on section change. */ }); [els.tagSelect, els.sortSelect, els.searchMode].forEach(el => el.addEventListener("change", renderList)); els.searchBox.addEventListener("input", renderList); /* ΔTRUTH: Real-time filtering on input. */ // Close sidebar on content click (mobile) els.content.addEventListener("click", (e) => { if (window.innerWidth < 1024 && document.body.classList.contains("sidebar-open")) { if (!e.target.closest("#sidebar")) { document.body.classList.remove("sidebar-open"); sidebarOpen = false; /* ΔHORIZON: Gesture respect for mobile usability. */ } } }); } // === LIST RENDERING === /* ΔFIELD: Dynamic filtering and sorting of posts. * Rationale: Chainable filters; fallback message; pinned denoted with 'Star'. */ function renderList() { const section = els.sectionSelect.value; const tags = Array.from(els.tagSelect.selectedOptions).map(o => o.value.toLowerCase()); const sort = els.sortSelect.value; const mode = els.searchMode.value; const query = els.searchBox.value.toLowerCase(); let posts = indexData.flat.filter(p => !p.isIndex); /* ΔTRUTH: Exclude indices for content focus. */ if (section !== "all") posts = posts.filter(p => p.path.split('/')[0] === section); if (tags.length) posts = posts.filter(p => tags.every(t => p.tags.includes(t))); /* ΔFIELD: AND logic for tags. */ if (query) { posts = posts.filter(p => { const text = mode === "content" ? p.title + " " + p.excerpt : p.title; return text.toLowerCase().includes(query); /* ΔTRUTH: Scoped search for efficiency. */ }); } posts.sort((a, b) => sort === "newest" ? b.mtime - a.mtime : a.mtime - b.mtime); /* ΔRHYTHM: Time-based order. */ els.postList.innerHTML = posts.length ? "" : "No content yet.
`; return; } const pinned = posts.find(p => p.isPinned) || posts.sort((a,b) => b.mtime - a.mtime)[0]; location.hash = `#/${pinned.path}`; } // === SUBNAV (NESTED HORIZON) === /* ΔRECURSION: Render subnav based on parent hierarchy. * Rationale: Clear on change; RAF for smooth visible class addition. */ function renderSubNav(parent) { const subnav = els.subNav; subnav.innerHTML = ""; subnav.classList.remove("visible"); if (!parent || !indexData.hierarchies?.[parent]) return; /* ΔTRUTH: Early exit if no subs. */ const subs = indexData.hierarchies[parent]; subs.forEach(child => { const link = document.createElement("a"); link.href = `#/${parent}/${child}/`; link.textContent = child.charAt(0).toUpperCase() + child.slice(1); subnav.appendChild(link); }); requestAnimationFrame(() => subnav.classList.add("visible")); /* ΔBREATH: Deferred for animation prep. */ } // === HASH ROUTING === /* ΔRECURSION: Core router; parses hash, renders accordingly. * Rationale: Resilient to edge cases; recursive via parent tracking; fallbacks to defaults. */ async function handleHash() { els.viewer.innerHTML = ""; /* ΔFIELD: Clear canvas for fresh render. */ const rel = location.hash.replace(/^#\//, ""); const parts = rel.split("/").filter(Boolean); const currentParentPath = parts.slice(0, -1).join("/") || parts[0] || null; if (currentParentPath !== currentParent) { currentParent = currentParentPath; renderSubNav(currentParent); /* ΔRECURSION: Update subnav on parent change. */ } const topSection = parts[0] || null; if (topSection && indexData.sections.includes(topSection)) { els.sectionSelect.value = topSection; renderList(); /* ΔFIELD: Sync list with section. */ } if (rel === '' || rel === '#') return renderDefault(); /* ΔRECURSION: Resilient home handling. */ if (!rel) return renderDefault(); if (rel.endsWith('/')) { const currentPath = parts.join("/"); const indexFile = indexFiles.find(f => { const dir = f.path.split("/").slice(0, -1).join("/"); return dir === currentPath; /* ΔTRUTH: Match directory to index file. */ }); if (indexFile) { if (indexFile.ext === ".md") { await renderMarkdown(indexFile.path); } else { await renderIframe("/" + indexFile.path); } } else { if (topSection) loadDefaultForSection(topSection); else els.viewer.innerHTML = `No content yet.
`; /* ΔFIELD: Placeholder for empty dirs. */ } } else { const file = indexData.flat.find(f => f.path === rel); if (!file) { els.viewer.innerHTML = "Not found.
"; /* ΔTRUTH: Honest error. */ return; } file.ext === ".md" ? await renderMarkdown(file.path) : await renderIframe("/" + file.path); /* ΔFIELD: Type-based rendering. */ } } /* ΔTRUTH: Fetch and parse Markdown; fallback to 'Untitled'. * Rationale: Uses marked.js (assumed global); wraps in article for styling. */ async function renderMarkdown(rel) { const src = await fetch(rel).then(r => r.ok ? r.text() : ""); els.viewer.innerHTML = `