diff --git a/index.js b/index.js index ff3e438..6ad2f4b 100644 --- a/index.js +++ b/index.js @@ -23,54 +23,118 @@ async function initializeMarked() { return marked; } +function createFileIndexItem(doc, container, level = 0) { + if (doc.type === 'folder') { + const folderDiv = document.createElement('div'); + folderDiv.className = 'folder open'; + folderDiv.dataset.path = doc.title; // Add data attribute for folder identification + folderDiv.style.paddingLeft = `${level * 0.8}rem`; + + const folderHeader = document.createElement('div'); + folderHeader.className = 'folder-header'; + folderHeader.innerHTML = ` + + ${doc.title} + `; + folderDiv.appendChild(folderHeader); + + const folderContent = document.createElement('div'); + folderContent.className = 'folder-content'; + doc.items.forEach(item => createFileIndexItem(item, folderContent, level + 1)); + folderDiv.appendChild(folderContent); + + folderHeader.addEventListener('click', () => { + folderDiv.classList.toggle('open'); + folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-closed'); + folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-open'); + }); + + container.appendChild(folderDiv); + } else { + const link = document.createElement('a'); + link.href = `?${doc.slug}`; // Use slug instead of generating filename + link.textContent = doc.title; + link.dataset.path = doc.path; + link.style.paddingLeft = `${level * 0.8 + 1.2}rem`; + + link.onclick = (e) => { + e.preventDefault(); + loadDocument(doc.path); + history.pushState(null, '', link.href); + }; + container.appendChild(link); + } +} + +// Add this new function to find parent folders of a path +function findParentFolders(documents, path, parentFolders = []) { + for (const doc of documents) { + if (doc.type === 'folder') { + // Check if path is in this folder + const found = doc.items.find(item => { + if (item.path === path) return true; + if (item.type === 'folder') { + return findParentFolders([item], path).length > 0; + } + return false; + }); + + if (found) { + parentFolders.push(doc); + // Continue searching in case of nested folders + doc.items.forEach(item => { + if (item.type === 'folder') { + findParentFolders([item], path, parentFolders); + } + }); + } + } + } + return parentFolders; +} + +// Helper function to find document by slug in nested structure +function findDocumentBySlug(documents, slug) { + for (const doc of documents) { + if (doc.type === 'folder') { + const found = findDocumentBySlug(doc.items, slug); + if (found) return found; + } else if (doc.slug === slug) { + return doc; + } + } + return null; +} + async function loadIndex() { try { const response = await fetch('index.json'); const data = await response.json(); const fileIndex = document.getElementById('file-index'); - // Build file index - data.documents.forEach(doc => { - const link = document.createElement('a'); - const fileName = doc.path.replace(/^docs\//, '') - .replace(/\.md$/, '') - .replace(/\s+/g, '-') - .toLowerCase(); - - link.href = `?${fileName}`; - link.textContent = doc.title; - link.dataset.path = doc.path; // Add path as data attribute for comparison - link.onclick = (e) => { - e.preventDefault(); - loadDocument(doc.path); - history.pushState(null, '', link.href); - }; - fileIndex.appendChild(link); - }); + // Clear existing content + fileIndex.innerHTML = ''; + + // Build file index with folder support + data.documents.forEach(doc => createFileIndexItem(doc, fileIndex)); // Handle initial URL load or load index.md by default const queryString = window.location.search; if (queryString) { - const fileParam = queryString.substring(1); // Remove the leading '?' - console.log('Loading document for:', fileParam); // Debug log + const slug = queryString.substring(1); // Remove the leading '?' + console.log('Loading document for slug:', slug); // Debug log - const matchingDoc = data.documents.find(doc => { - const docFileName = doc.path.replace(/^docs\//, '') - .replace(/\.md$/, '') - .replace(/\s+/g, '-') - .toLowerCase(); - return docFileName === fileParam; - }); + const matchingDoc = findDocumentBySlug(data.documents, slug); if (matchingDoc) { console.log('Found matching document:', matchingDoc); // Debug log await loadDocument(matchingDoc.path); } else { - console.warn('No matching document found for:', fileParam); // Debug log + console.warn('No matching document found for slug:', slug); // Debug log } } else { // Load index.md by default when no document is specified - const defaultDoc = data.documents.find(doc => doc.path === 'docs/index.md'); + const defaultDoc = findDocumentBySlug(data.documents, 'welcome'); if (defaultDoc) { await loadDocument(defaultDoc.path); } @@ -85,18 +149,11 @@ async function loadIndex() { // Add popstate handler for browser back/forward buttons window.addEventListener('popstate', async () => { const urlParams = new URLSearchParams(window.location.search); - const fileParam = urlParams.toString().replace(/^=/, '').replace(/^\?/, ''); - if (fileParam) { + const slug = urlParams.toString().replace(/^=/, '').replace(/^\?/, ''); + if (slug) { const response = await fetch('index.json'); const data = await response.json(); - const matchingDoc = data.documents.find(doc => { - const docFileName = doc.path.replace(/^docs\//, '') - .replace(/\.md$/, '') - .replace(/\s+/g, '-') - .toLowerCase(); - return docFileName === fileParam; - }); - + const matchingDoc = findDocumentBySlug(data.documents, slug); if (matchingDoc) { await loadDocument(matchingDoc.path); } @@ -142,9 +199,28 @@ function extractMetadata(content) { async function loadDocument(path) { try { - // Update active state in sidebar + // Update active state in sidebar and expand folders document.querySelectorAll('#file-index a').forEach(link => { link.classList.toggle('active', link.dataset.path === path); + + // If this is the active link, expand its parent folders + if (link.dataset.path === path) { + const response = fetch('index.json') + .then(res => res.json()) + .then(data => { + const parentFolders = findParentFolders(data.documents, path); + parentFolders.forEach(folder => { + // Find and expand the corresponding folder div + const folderDiv = document.querySelector(`.folder[data-path="${folder.title}"]`); + if (folderDiv) { + folderDiv.classList.add('open'); + const icon = folderDiv.querySelector('.folder-icon'); + icon.classList.remove('fa-folder-closed'); + icon.classList.add('fa-folder-open'); + } + }); + }); + } }); const [response, marked] = await Promise.all([ diff --git a/index.json b/index.json index 44f1d95..7f0813a 100644 --- a/index.json +++ b/index.json @@ -2,11 +2,19 @@ "documents": [ { "title": "Welcome", - "path": "docs/index.md" + "path": "docs/index.md", + "slug": "welcome" }, { - "title": "Blueprint Depth Trace", - "path": "docs/Blueprint Penetration Trace.md" + "title": "Unreal Engine", + "type": "folder", + "items": [ + { + "title": "Blueprint Depth Trace", + "path": "docs/Blueprint Penetration Trace.md", + "slug": "blueprint-penetration-trace" + } + ] } ] } diff --git a/styles.css b/styles.css index adf7560..c7beae7 100644 --- a/styles.css +++ b/styles.css @@ -102,6 +102,41 @@ body { border-left: 2px solid var(--ifm-color-primary); } +.folder { + color: var(--ifm-color-content); +} + +.folder-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 1.2rem; + cursor: pointer; + user-select: none; + font-size: 0.875rem; +} + +.folder-header:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.folder-icon { + width: 1.2em; + color: var(--ifm-color-content-secondary); +} + +.folder-content { + display: none; +} + +.folder.open > .folder-content { + display: block; +} + +.folder .folder-header:hover .folder-icon { + color: var(--ifm-color-primary); +} + .social-links { padding: 1rem 1rem 0.5rem; border-top: 1px solid var(--ifm-border-color);