PageSourceSearch

https://legens.app/js/router.js?v1789414054768

js legens.app collected 2026-09-27 06:04:41 UTC 8,328 bytes, 265 lines download raw bytes

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.