|
| 1 | +// ==== CONFIG ==== |
| 2 | +const OWNER = "HumanNeuronLab"; // Username |
| 3 | +const REPO = "HumanNeuronLab.github.io"; // Repository name |
| 4 | +const BRANCH = "main"; // Branch to display |
| 5 | +const SUBFOLDER = "pages"; // Root for all pages to be displayed |
| 6 | +const ICONSPATH = `${SUBFOLDER}/icons/`; // Folder containing inline SVG icons (defaults to "unlisted.icon" if none specified). File names are case-insensitive. |
| 7 | +const ORDERPATH = `${SUBFOLDER}/order.txt`; // File containing ordering list for the navigation bar. Unlisted items are appended in alphabetical order. Order of display is depth-specific. File names are case-insensitive. |
| 8 | + |
| 9 | +const API_URL = `https://api.github.com/repos/${OWNER}/${REPO}/git/trees/${BRANCH}?recursive=1`; |
| 10 | +const RAW_BASE = `https://raw.githubusercontent.com/${OWNER}/${REPO}/${BRANCH}/`; |
| 11 | + |
| 12 | + |
| 13 | +// ==== EXECUTE ==== |
| 14 | +main(); |
| 15 | + |
| 16 | + |
| 17 | +// ==== HELPERS ==== |
| 18 | +function capitalizeName(file, type) { |
| 19 | + let base = file; |
| 20 | + if (type === "file") { |
| 21 | + const dot = base.lastIndexOf("."); |
| 22 | + if (dot > 0) base = base.slice(0, dot); |
| 23 | + } |
| 24 | + return base.charAt(0).toUpperCase() + base.slice(1); |
| 25 | +} |
| 26 | + |
| 27 | +function normalizeType(apiType) { |
| 28 | + return apiType === "blob" ? "file" : "directory"; |
| 29 | +} |
| 30 | + |
| 31 | +// recursively build a nested tree from flat paths |
| 32 | +function buildTree(flat, basePath = "", depth = 0) { |
| 33 | + const tree = []; |
| 34 | + |
| 35 | + const entries = flat |
| 36 | + .filter(f => f.path.startsWith(basePath) && f.path !== basePath) |
| 37 | + .map(f => { |
| 38 | + const relPath = f.path.slice(basePath.length).replace(/^\/+/, ""); |
| 39 | + const firstPart = relPath.split("/")[0]; |
| 40 | + return { ...f, relPath, firstPart }; |
| 41 | + }); |
| 42 | + |
| 43 | + const groups = {}; |
| 44 | + for (const e of entries) { |
| 45 | + if (!groups[e.firstPart]) groups[e.firstPart] = []; |
| 46 | + groups[e.firstPart].push(e); |
| 47 | + } |
| 48 | + |
| 49 | + for (const [part, group] of Object.entries(groups)) { |
| 50 | + const fullPath = basePath ? basePath + "/" + part : part; |
| 51 | + const first = group[0]; |
| 52 | + const type = group.some(g => g.relPath.includes("/")) ? "directory" : normalizeType(first.type); |
| 53 | + |
| 54 | + // skip order.txt and icons folder |
| 55 | + if (part.toLowerCase() === "order.txt") continue; |
| 56 | + if (part.toLowerCase() === "icons") continue; |
| 57 | + |
| 58 | + const node = { |
| 59 | + file: part, |
| 60 | + path: fullPath, |
| 61 | + name: capitalizeName(part, type), |
| 62 | + type, |
| 63 | + depth, |
| 64 | + download_url: type === "file" ? RAW_BASE + fullPath : null, |
| 65 | + svg: null, |
| 66 | + children: [] |
| 67 | + }; |
| 68 | + |
| 69 | + if (type === "directory") { |
| 70 | + node.children = buildTree(group, fullPath, depth + 1); |
| 71 | + } |
| 72 | + |
| 73 | + tree.push(node); |
| 74 | + } |
| 75 | + |
| 76 | + return tree; |
| 77 | +} |
| 78 | + |
| 79 | +// reorder children recursively using order.txt |
| 80 | +function reorderTree(tree, orders, icons) { |
| 81 | + function reorderChildren(children, orderList) { |
| 82 | + const orderLower = orderList.map(x => x.toLowerCase().trim()); |
| 83 | + const listed = []; |
| 84 | + const unlisted = []; |
| 85 | + |
| 86 | + for (const child of children) { |
| 87 | + const matchIdx = orderLower.indexOf(child.file.toLowerCase()); |
| 88 | + if (matchIdx >= 0) { |
| 89 | + listed[matchIdx] = child; |
| 90 | + } else { |
| 91 | + unlisted.push(child); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + const sortedUnlisted = unlisted.sort((a, b) => |
| 96 | + a.file.toLowerCase().localeCompare(b.file.toLowerCase()) |
| 97 | + ); |
| 98 | + |
| 99 | + const reordered = listed.filter(Boolean).concat(sortedUnlisted); |
| 100 | + |
| 101 | + // attach svg icons properly |
| 102 | + for (const child of reordered) { |
| 103 | + // strip extension if file, keep folder name as-is |
| 104 | + let iconKey = (child.type === "file" |
| 105 | + ? child.file.replace(/\.[^/.]+$/, "") |
| 106 | + : child.file).toLowerCase().trim(); |
| 107 | + |
| 108 | + if (icons[iconKey]) { |
| 109 | + child.svg = icons[iconKey]; |
| 110 | + } else if (icons["unlisted"]) { |
| 111 | + child.svg = icons["unlisted"]; |
| 112 | + } |
| 113 | + |
| 114 | + if (child.type === "directory") { |
| 115 | + child.children = reorderChildren(child.children, orderList); |
| 116 | + } |
| 117 | + console.log("Looking for icon:", iconKey, "Available:", Object.keys(icons)); |
| 118 | + } |
| 119 | + |
| 120 | + return reordered; |
| 121 | + } |
| 122 | + |
| 123 | + return reorderChildren(tree, orders); |
| 124 | +} |
| 125 | + |
| 126 | +function createNavbar(finalTree,elementParent) { |
| 127 | + const ul = document.createElement('ul'); |
| 128 | + elementParent.appendChild(ul); |
| 129 | + var subparent = []; |
| 130 | + if (elementParent.id === "sidebar") { |
| 131 | + const uli = document.createElement('li'); |
| 132 | + ul.appendChild(uli); |
| 133 | + const span = document.createElement('span'); |
| 134 | + span.className = "logo"; |
| 135 | + span.innerText = 'Human Neuron Lab'; |
| 136 | + uli.appendChild(span); |
| 137 | + const uliButton = document.createElement('button'); |
| 138 | + uli.appendChild(uliButton); |
| 139 | + uliButton.setAttribute('onclick','toggleSidebar(this)'); |
| 140 | + uliButton.setAttribute('id','toggle-btn'); |
| 141 | + uliButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="m313-480 155 156q11 11 11.5 27.5T468-268q-11 11-28 11t-28-11L228-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T468-692q11 11 11 28t-11 28L313-480Zm264 0 155 156q11 11 11.5 27.5T732-268q-11 11-28 11t-28-11L492-452q-6-6-8.5-13t-2.5-15q0-8 2.5-15t8.5-13l184-184q11-11 27.5-11.5T732-692q11 11 11 28t-11 28L577-480Z"/></svg>'; |
| 142 | + uli.appendChild(uliButton); |
| 143 | + subparent = ul; |
| 144 | + } else { |
| 145 | + ul.className = 'sub-menu'; |
| 146 | + const div = document.createElement('div'); |
| 147 | + ul.appendChild(div); |
| 148 | + subparent = div; |
| 149 | + } |
| 150 | + for (const currentEntry of finalTree){ |
| 151 | + const li = document.createElement('li'); |
| 152 | + subparent.appendChild(li); |
| 153 | + const liButton = document.createElement('button'); |
| 154 | + if (elementParent.id === "sidebar" && finalTree.indexOf(currentEntry) === 0){ |
| 155 | + li.className = "active"; |
| 156 | + } |
| 157 | + liButton.innerHTML = currentEntry.svg; |
| 158 | + li.appendChild(liButton); |
| 159 | + const span = document.createElement('span'); |
| 160 | + span.innerText = currentEntry.name; |
| 161 | + liButton.appendChild(span); |
| 162 | + if (currentEntry.type === "file"){ |
| 163 | + if (elementParent.id === "sidebar" && finalTree.indexOf(currentEntry) === 0){ |
| 164 | + liButton.setAttribute('id','homePage'); |
| 165 | + } |
| 166 | + liButton.className = 'menu-btn'; |
| 167 | + liButton.setAttribute('onclick','openSesame(this)'); |
| 168 | + liButton.setAttribute('HNL-link',currentEntry.download_url); |
| 169 | + } else { |
| 170 | + liButton.className = 'dropdown-btn'; |
| 171 | + liButton.setAttribute('onclick','toggleSubMenu(this)'); |
| 172 | + liButton.innerHTML += '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M480-361q-8 0-15-2.5t-13-8.5L268-556q-11-11-11-28t11-28q11-11 28-11t28 11l156 156 156-156q11-11 28-11t28 11q11 11 11 28t-11 28L508-372q-6 6-13 8.5t-15 2.5Z"/></svg>'; |
| 173 | + } |
| 174 | + console.log(currentEntry); |
| 175 | + const idx = finalTree.indexOf(currentEntry); |
| 176 | + console.log(idx); |
| 177 | + if (currentEntry.children.length > 0){ |
| 178 | + createNavbar(currentEntry.children,li); |
| 179 | + }; |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +function toggleSidebar(toggleButton){ |
| 184 | + const sidebar = document.getElementById("sidebar"); |
| 185 | + sidebar.classList.toggle('close') |
| 186 | + toggleButton.classList.toggle('rotate') |
| 187 | + |
| 188 | + closeAllSubMenus() |
| 189 | +} |
| 190 | + |
| 191 | +function toggleSubMenu(button){ |
| 192 | + |
| 193 | + if(!button.nextElementSibling.classList.contains('show')){ |
| 194 | + closeAllSubMenus() |
| 195 | + } |
| 196 | + |
| 197 | + button.nextElementSibling.classList.toggle('show') |
| 198 | + button.classList.toggle('rotate') |
| 199 | + |
| 200 | + if(sidebar.classList.contains('close')){ |
| 201 | + sidebar.classList.toggle('close') |
| 202 | + toggleButton.classList.toggle('rotate') |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +function closeAllSubMenus(){ |
| 207 | + const sidebar = document.getElementById("sidebar"); |
| 208 | + Array.from(sidebar.getElementsByClassName('show')).forEach(ul => { |
| 209 | + ul.classList.remove('show') |
| 210 | + ul.previousElementSibling.classList.remove('rotate') |
| 211 | + }) |
| 212 | +} |
| 213 | + |
| 214 | +async function openSesame(button){ |
| 215 | + let x = await fetch(button.getAttribute("hnl-link")); |
| 216 | + let y = await x.text(); |
| 217 | + document.getElementById("logger").innerHTML = y; |
| 218 | + myList = document.getElementsByTagName("li"); |
| 219 | + for (i = 0; i < myList.length; i++) { |
| 220 | + myList[i].classList.remove("active"); |
| 221 | + } |
| 222 | + button.parentNode.classList.add("active"); |
| 223 | +} |
| 224 | + |
| 225 | +async function main() { |
| 226 | + try { |
| 227 | + // 1. Fetch repo tree |
| 228 | + const res = await fetch(API_URL); |
| 229 | + if (!res.ok) throw new Error("Failed to fetch repo tree"); |
| 230 | + const repoData = await res.json(); |
| 231 | + const flat = repoData.tree; |
| 232 | + |
| 233 | + // 2. Fetch all icons |
| 234 | + const icons = {}; |
| 235 | + const iconFiles = flat.filter(f => f.path.startsWith(ICONSPATH) && f.path.endsWith(".icon")); |
| 236 | + for (const icon of iconFiles) { |
| 237 | + const svgRes = await fetch(RAW_BASE + icon.path); |
| 238 | + if (svgRes.ok) { |
| 239 | + const svgText = await svgRes.text(); |
| 240 | + const key = icon.path.split("/").pop().replace(/\.icon$/i, "").toLowerCase(); |
| 241 | + icons[key] = svgText; |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + // 3. Fetch order.txt (only root-level) |
| 246 | + let orderList = []; |
| 247 | + const orderFile = flat.find(f => f.path.toLowerCase() === ORDERPATH); |
| 248 | + if (orderFile) { |
| 249 | + const orderRes = await fetch(RAW_BASE + orderFile.path); |
| 250 | + orderPath = RAW_BASE + orderFile.path; |
| 251 | + if (orderRes.ok) { |
| 252 | + const text = await orderRes.text(); |
| 253 | + orderList = text.split(/\r?\n/).filter(Boolean); |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + // 4. Build + reorder |
| 258 | + const tree = buildTree(flat, SUBFOLDER, 0); |
| 259 | + const finalTree = reorderTree(tree, orderList, icons); |
| 260 | + |
| 261 | + //5. Create and populate the navbar |
| 262 | + const sidebar = document.getElementById("sidebar"); |
| 263 | + createNavbar(finalTree,sidebar); |
| 264 | + openSesame(document.getElementById("homePage")); |
| 265 | + |
| 266 | + // document.getElementById("output").textContent = JSON.stringify(finalTree, null, 2); |
| 267 | + } catch (err) { |
| 268 | + // document.getElementById("output").textContent = "Error: " + err.message; |
| 269 | + } |
| 270 | +} |
0 commit comments