1/** 2 * Router - LEGENS 3 * Sistema de rutas virtuales con History API 4 */ 5 6class LegensRouter { 7 constructor() { 8 this.routes = {}; 9 this.currentRoute = null; 10 this.basePath = ''; 11 12 // Detectar base path si estamos en subdirectorio 13 this.detectBasePath(); 14 } 15 16 splitPathParts(path = '/') { 17 const rawPath = String(path || '/'); 18 const [pathWithQuery, hashPart = ''] = rawPath.split('#', 2); 19 const [pathnamePart, searchPart = ''] = pathWithQuery.split('?', 2); 20 21 return { 22 path: rawPath, 23 pathname: pathnamePart || '/', 24 search: searchPart ? `?${searchPart}` : '', 25 hash: hashPart ? `#${hashPart}` : '' 26 }; 27 } 28 29 /** 30 * Detecta si la app está en un subdirectorio 31 */ 32 detectBasePath() { 33 // Si hay un <base> tag, usarlo 34 const baseTag = document.querySelector('base'); 35 if (baseTag) { 36 this.basePath = new URL(baseTag.href).pathname.replace(/\/$/, ''); 37 } 38 } 39 40 /** 41 * Inicializa el router 42 */ 43 init() { 44 // Escuchar navegación del navegador (back/forward) 45 window.addEventListener('popstate', (e) => { 46 this.handleRoute(window.location.pathname + window.location.search + window.location.hash, false); 47 }); 48 49 // Interceptar clicks en links 50 document.addEventListener('click', (e) => { 51 const link = e.target.closest('a[href]'); 52 if (link && this.shouldIntercept(link)) { 53 e.preventDefault(); 54 this.navigate(link.getAttribute('href')); 55 } 56 }); 57 58 // Manejar ruta inicial 59 this.handleRoute(window.location.pathname + window.location.search + window.location.hash, false); 60 61 Logger.log('Router inicializado'); 62 } 63 64 /** 65 * Determina si debemos interceptar el click en un link 66 */ 67 shouldIntercept(link) { 68 // No interceptar links externos 69 if (link.hostname !== window.location.hostname) return false; 70 // No interceptar links con target 71 if (link.target === '_blank') return false; 72 // No interceptar links con download 73 if (link.hasAttribute('download')) return false; 74 // No interceptar links a archivos 75 const ext = link.pathname.split('.').pop(); 76 if (['html', 'pdf', 'jpg', 'png', 'mp3', 'mp4'].includes(ext)) return false; 77 78 return true; 79 } 80 81 /** 82 * Registra una ruta 83 * @param {string} pattern - Patrón de ruta (ej: '/book/:public_id/:slug') 84 * @param {Function} handler - Función a ejecutar 85 */ 86 on(pattern, handler) { 87 this.routes[pattern] = { 88 pattern, 89 handler, 90 regex: this.patternToRegex(pattern) 91 }; 92 } 93 94 /** 95 * Convierte un patrón de ruta a regex 96 */ 97 patternToRegex(pattern) { 98 const regexStr = pattern 99 .replace(/\//g, '\\/') 100 .replace(/:([^\/]+)/g, '([^/]+)'); 101 return new RegExp(`^${regexStr}$`); 102 } 103 104 /** 105 * Extrae parámetros de una ruta 106 */ 107 extractParams(pattern, path) { 108 const paramNames = (pattern.match(/:([^\/]+)/g) || []).map(p => p.slice(1)); 109 const route = this.routes[pattern]; 110 if (!route) return {}; 111 112 const match = path.match(route.regex); 113 if (!match) return {}; 114 115 const params = {}; 116 paramNames.forEach((name, index) => { 117 params[name] = match[index + 1]; 118 }); 119 120 return params; 121 } 122 123 /** 124 * Navega a una ruta 125 * @param {string} path - Ruta a navegar 126 * @param {boolean} pushState - Si debe añadir al historial 127 */ 128 navigate(path, pushState = true) { 129 // Normalizar path 130 if (!path.startsWith('/')) { 131 path = '/' + path; 132 } 133 134 if (pushState) { 135 window.history.pushState({}, '', this.basePath + path); 136 } 137 138 this.handleRoute(path); 139 } 140 141 /** 142 * Maneja una ruta 143 */ 144 handleRoute(path, scroll = true) { 145 const routeParts = this.splitPathParts(path);
146 let normalizedPathname = routeParts.pathname; 147 148 // Quitar base path si existe 149 if (this.basePath && normalizedPathname.startsWith(this.basePath)) { 150 normalizedPathname = normalizedPathname.slice(this.basePath.length) || '/'; 151 } 152 153 // Buscar ruta que coincida 154 for (const pattern in this.routes) { 155 const route = this.routes[pattern]; 156 if (route.regex.test(normalizedPathname)) { 157 const params = this.extractParams(pattern, normalizedPathname); 158 this.currentRoute = { 159 pattern, 160 path: routeParts.path, 161 pathname: normalizedPathname, 162 search: routeParts.search, 163 hash: routeParts.hash, 164 params 165 }; 166 167 // Ejecutar handler 168 route.handler(params); 169 170 document.dispatchEvent(new CustomEvent('route:change', { 171 detail: { 172 pattern, 173 path: routeParts.path, 174 pathname: normalizedPathname, 175 search: routeParts.search, 176 hash: routeParts.hash, 177 params 178 } 179 })); 180 181 // Scroll al top 182 if (scroll) { 183 window.scrollTo(0, 0); 184 } 185 186 return; 187 } 188 } 189 190 // Ruta no encontrada - ir a home 191 Logger.log('Ruta no encontrada:', path); 192 this.navigate('/', false); 193 } 194 195 /** 196 * Reemplaza la ruta actual sin añadir al historial 197 */ 198 replace(path) { 199 if (!path.startsWith('/')) { 200 path = '/' + path; 201 } 202 window.history.replaceState({}, '', this.basePath + path); 203 this.currentRoute = { path }; 204 } 205 206 /** 207 * Genera una URL para un libro 208 */ 209 bookUrl(id, title = '') { 210 const slug = this.slugify(title); 211 return `/book/${encodeURIComponent(String(id))}${slug ? '/' + slug : ''}`; 212 } 213 214 /** 215 * Genera una URL para un autor 216 */ 217 authorUrl(author, explicitSlug = '') { 218 const value = typeof author === 'string' 219 ? { identifier: author, name: author, slug: explicitSlug } 220 : (author || {}); 221 const identifier = value.public_id || value.identifier || value.name || value.author || ''; 222 const slugSource = explicitSlug || value.slug || value.name || value.author || identifier; 223 const slug = this.slugify(slugSource); 224 return `/author/${encodeURIComponent(identifier)}${slug ? '/' + slug : ''}`; 225 } 226 227 /** 228 * Convierte texto a slug URL-friendly 229 */ 230 slugify(text) { 231 if (!text) return ''; 232 return text 233 .toString() 234 .normalize('NFD') 235 .replace(/[\u0300-\u036f]/g, '') // Quitar acentos 236 .toLowerCase() 237 .trim() 238 .replace(/[^a-z0-9\s-]/g, '') // Quitar caracteres especiales 239 .replace(/[\s_]+/g, '-') // Espacios a guiones 240 .replace(/-+/g, '-') // Múltiples guiones a uno 241 .slice(0, 50); // Limitar longitud 242 } 243 244 /** 245 * Actualiza el tÃtulo de la página 246 */ 247 setTitle(title, suffix = ' | LEGENS') { 248 document.title = title + suffix; 249 } 250 251 /** 252 * Obtiene la ruta actual 253 */ 254 getCurrentPath() { 255 return window.location.pathname.replace(this.basePath, '') || '/'; 256 } 257} 258 259// Instancia global 260const router = new LegensRouter(); 261 262// Export para módulos 263if (typeof module !== 'undefined' && module.exports) { 264 module.exports = { LegensRouter, router }; 265}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.