cdafffaa70
Côté JS : décompose main.js en modules ES6 (state, services, ui, router) pour séparer les responsabilités et faciliter la maintenance. Côté Rust : ajoute 7 tests sur is_relevant_path (notify_watcher) pour couvrir les cas cachés/non-md ; couverture totale 77 % > seuil 60 %. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.4 KiB
JavaScript
93 lines
3.4 KiB
JavaScript
import { escapeHtml } from '../utils.js';
|
|
|
|
function buildTree(relPaths, sep) {
|
|
const tree = { _files: [], _dirs: {} };
|
|
relPaths.forEach((rel) => {
|
|
const parts = rel.split(sep);
|
|
let node = tree;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const dir = parts[i];
|
|
if (!node._dirs[dir]) node._dirs[dir] = { _files: [], _dirs: {} };
|
|
node = node._dirs[dir];
|
|
}
|
|
node._files.push(rel);
|
|
});
|
|
return tree;
|
|
}
|
|
|
|
function renderTree(node, prefix, sep, currentRel, depth) {
|
|
let html = '';
|
|
const depthClass = depth > 0 ? ` class="nav-depth-${Math.min(depth, 3)}"` : '';
|
|
|
|
node._files.slice().sort().forEach((rel) => {
|
|
const label = rel.split(sep).pop().replace(/\.md$/i, '').replace(/-/g, ' ');
|
|
const abs = prefix + rel;
|
|
const cls = rel === currentRel ? ' active' : '';
|
|
if (depth === 0) {
|
|
html += `\n<li><a href="#" class="nav-root-link${cls}" data-path="${escapeHtml(abs)}">${escapeHtml(label)}</a></li>`;
|
|
} else {
|
|
html += `\n<li${depthClass}><a href="#" class="nav-file-link${cls}" data-path="${escapeHtml(abs)}">${escapeHtml(label)}</a></li>`;
|
|
}
|
|
});
|
|
|
|
Object.keys(node._dirs).sort().forEach((dirName) => {
|
|
const child = node._dirs[dirName];
|
|
const isOpen = currentRel && currentRel.split(sep).includes(dirName);
|
|
const openAttr = isOpen ? ' open' : '';
|
|
|
|
const homeFile = child._files.find((f) => /^home\.md$/i.test(f.split(sep).pop()));
|
|
let folderLabel;
|
|
if (homeFile) {
|
|
const abs = prefix + homeFile;
|
|
const cls = homeFile === currentRel ? ' active' : '';
|
|
folderLabel = `<a href="#" class="nav-folder-link${cls}" data-path="${escapeHtml(abs)}" onclick="event.stopPropagation()">${escapeHtml(dirName)}</a>`;
|
|
} else {
|
|
folderLabel = `<span class="nav-folder-name">${escapeHtml(dirName)}</span>`;
|
|
}
|
|
|
|
const arrow = `<span class="nav-arrow" onclick="event.preventDefault();event.stopPropagation();var d=this.closest('details');d.open=!d.open">▶</span>`;
|
|
|
|
const childNode = {
|
|
_files: homeFile ? child._files.filter((f) => f !== homeFile) : child._files,
|
|
_dirs: child._dirs,
|
|
};
|
|
|
|
html += `\n<li${depthClass}><details class="nav-folder"${openAttr}>`;
|
|
html += `\n <summary>${arrow}${folderLabel}</summary>`;
|
|
html += `\n <ul class="nav-tree">`;
|
|
html += renderTree(childNode, prefix, sep, currentRel, depth + 1);
|
|
html += `\n </ul>\n</details></li>`;
|
|
});
|
|
|
|
return html;
|
|
}
|
|
|
|
function buildSidebar(baseDir, files, currentFile) {
|
|
const sep = baseDir.includes('\\') ? '\\' : '/';
|
|
const dirName = baseDir.split(sep).pop();
|
|
const prefix = baseDir.endsWith(sep) ? baseDir : baseDir + sep;
|
|
const toRel = abs => abs.startsWith(prefix) ? abs.slice(prefix.length) : abs;
|
|
const currentRel = currentFile ? toRel(currentFile) : null;
|
|
const relPaths = files.map(toRel);
|
|
const tree = buildTree(relPaths, sep);
|
|
|
|
let html = `<div class="sidebar__title-block">
|
|
<h1 class="sidebar__title-block__title">${escapeHtml(dirName)}</h1>
|
|
</div>
|
|
<ul class="sidebar__menu nav-tree">`;
|
|
html += renderTree(tree, prefix, sep, currentRel, 0);
|
|
html += `\n </ul>`;
|
|
return html;
|
|
}
|
|
|
|
export function renderSidebar(baseDir, files, currentFile, onNavigate) {
|
|
const sidebarEl = document.querySelector('#sidebar');
|
|
sidebarEl.innerHTML = buildSidebar(baseDir, files, currentFile);
|
|
sidebarEl.querySelectorAll('a[data-path]').forEach(a => {
|
|
a.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
onNavigate(a.dataset.path);
|
|
});
|
|
});
|
|
}
|