const routes = []; let currentCleanup = null; /** @returns {{ regex: RegExp, paramNames: string[] }} */ export function compileRoute(pattern) { const paramNames = []; const parts = pattern.replace(/\/:([^/]+)/g, (_, name) => { paramNames.push(name); return '/([^/]+)'; }); return { regex: new RegExp('^' + parts + '$'), paramNames }; } /** Match a path against a string route pattern; returns params or null. */ export function matchRoute(pattern, path) { const { regex, paramNames } = compileRoute(pattern); const match = path.match(regex); if (!match) { return null; } const params = {}; paramNames.forEach((name, i) => { params[name] = decodeURIComponent(match[i + 1]); }); return params; } export function addRoute(pattern, handler) { let regex; let paramNames = []; if (pattern instanceof RegExp) { regex = pattern; } else { ({ regex, paramNames } = compileRoute(pattern)); } routes.push({ regex, paramNames, handler }); } export function navigate(hash) { window.location.hash = hash; } export function currentHash() { return window.location.hash.slice(1) || '/'; } async function resolve() { if (typeof currentCleanup === 'function') { currentCleanup(); currentCleanup = null; } const path = currentHash(); for (const route of routes) { const match = path.match(route.regex); if (match) { const params = {}; route.paramNames.forEach((name, i) => { params[name] = decodeURIComponent(match[i + 1]); }); try { currentCleanup = await route.handler(params) || null; } catch (e) { console.error('Route handler error:', e); } return; } } // fallback: dashboard navigate('#/'); } export function startRouter() { window.addEventListener('hashchange', resolve); resolve(); }