refactor: migration architecture clean (Rust + JS)

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>
This commit is contained in:
2026-06-19 14:23:30 +02:00
parent 6ae7a434c4
commit cdafffaa70
12 changed files with 384 additions and 313 deletions
+66
View File
@@ -0,0 +1,66 @@
const CSS_TABS = ['general', 'police', 'contenu', 'avance'];
let activeTab = CSS_TABS[0];
function cssKey(tab) { return `pena_css_${tab}`; }
function loadTabCss(tab) { return localStorage.getItem(cssKey(tab)) ?? ''; }
function saveTabCss(tab, css) {
if (css.trim()) localStorage.setItem(cssKey(tab), css);
else localStorage.removeItem(cssKey(tab));
}
function applyCustomCss() {
let styleEl = document.getElementById('pena-custom-style');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'pena-custom-style';
document.head.appendChild(styleEl);
}
styleEl.textContent = CSS_TABS.map(t => loadTabCss(t)).filter(Boolean).join('\n');
}
export function initCssModal() {
applyCustomCss();
const cssModalOverlay = document.getElementById('css-modal-overlay');
const cssEditor = document.getElementById('css-editor');
function openCssModal() {
cssEditor.value = loadTabCss(activeTab);
cssModalOverlay.classList.remove('hidden');
}
function closeCssModal() {
cssModalOverlay.classList.add('hidden');
}
document.getElementById('btn-customize-css').addEventListener('click', openCssModal);
document.getElementById('btn-css-close').addEventListener('click', closeCssModal);
document.getElementById('btn-css-cancel').addEventListener('click', closeCssModal);
cssModalOverlay.addEventListener('click', (e) => {
if (e.target === cssModalOverlay) closeCssModal();
});
document.getElementById('css-tabs').addEventListener('click', (e) => {
const tab = e.target.closest('.css-modal__tab');
if (!tab) return;
saveTabCss(activeTab, cssEditor.value);
activeTab = tab.dataset.tab;
document.querySelectorAll('.css-modal__tab').forEach(t => t.classList.toggle('active', t === tab));
cssEditor.value = loadTabCss(activeTab);
});
document.getElementById('btn-css-apply').addEventListener('click', () => {
saveTabCss(activeTab, cssEditor.value);
applyCustomCss();
closeCssModal();
});
document.getElementById('btn-css-reset').addEventListener('click', () => {
CSS_TABS.forEach(t => localStorage.removeItem(cssKey(t)));
cssEditor.value = '';
applyCustomCss();
});
}
+44
View File
@@ -0,0 +1,44 @@
import { escapeHtml } from '../utils.js';
const RECENTS_KEY = 'pena_recents';
const RECENTS_MAX = 8;
export function loadRecents() {
try { return JSON.parse(localStorage.getItem(RECENTS_KEY) ?? '[]'); }
catch { return []; }
}
export function saveRecent(path, mode) {
const name = path.replace(/\\/g, '/').split('/').pop().replace(/\.md$/i, '');
const parent = path.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
const recents = loadRecents().filter(r => r.path !== path);
recents.unshift({ path, mode, name, parent });
localStorage.setItem(RECENTS_KEY, JSON.stringify(recents.slice(0, RECENTS_MAX)));
}
export function renderRecents(onOpen) {
const recents = loadRecents();
const container = document.getElementById('home-recents');
const list = document.getElementById('home-recents-list');
if (recents.length === 0) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
list.innerHTML = recents.map(r => {
const icon = r.mode === 'dir' ? '📁' : '📄';
const escaped = escapeHtml(r.path);
return `<li>
<button class="home-recents__item" data-path="${escaped}" data-mode="${r.mode}">
<span class="home-recents__item__icon">${icon}</span>
<span class="home-recents__item__info">
<span class="home-recents__item__name">${escapeHtml(r.name)}</span>
<span class="home-recents__item__path">${escapeHtml(r.parent)}</span>
</span>
</button>
</li>`;
}).join('');
list.querySelectorAll('.home-recents__item').forEach(btn => {
btn.addEventListener('click', () => onOpen(btn.dataset.path, btn.dataset.mode));
});
}
+33
View File
@@ -0,0 +1,33 @@
import { convertFile } from '../services/markdown.js';
const win = window.__TAURI__.window.getCurrentWindow();
const titlebarTitle = document.getElementById('titlebar-title');
const content = document.querySelector('#content');
export async function loadPage(filePath, mode, sidebarEl) {
try {
const html = await convertFile(filePath);
content.innerHTML = html;
const basename = filePath.split('/').pop().split('\\').pop();
const title = basename.replace(/\.md$/i, '');
await win.setTitle(title);
titlebarTitle.textContent = title;
if (mode === 'dir' || mode === 'file') {
sidebarEl.querySelectorAll('a').forEach(a => {
a.classList.toggle('active', a.dataset.path === filePath);
});
const active = sidebarEl.querySelector('a[data-path].active');
if (active) {
let el = active.parentElement;
while (el && el !== sidebarEl) {
if (el.tagName === 'DETAILS') el.open = true;
el = el.parentElement;
}
}
}
} catch (err) {
content.innerHTML = `<p class="error">Impossible de charger le fichier : ${err}</p>`;
}
}
+92
View File
@@ -0,0 +1,92 @@
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);
});
});
}