jerkjs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -0
- package/README.md +171 -0
- package/doc/EXTENSION_MANUAL.md +958 -0
- package/doc/FIREWALL_MANUAL.md +419 -0
- package/doc/HOOKS_REFERENCE_IMPROVED.md +599 -0
- package/doc/MANUAL_API_SDK.md +539 -0
- package/doc/MANUAL_MVC.md +397 -0
- package/doc/MARIADB_TOKENS_IMPLEMENTATION.md +113 -0
- package/doc/MIDDLEWARE_MANUAL.md +521 -0
- package/doc/OAUTH2_GOOGLE_MANUAL.md +408 -0
- package/doc/frontend-and-sessions.md +356 -0
- package/examples/advanced/controllers/productController.js +64 -0
- package/examples/advanced/controllers/userController.js +85 -0
- package/examples/advanced/routes.json +51 -0
- package/examples/advanced_example.js +93 -0
- package/examples/basic/controllers/userController.js +85 -0
- package/examples/basic_example.js +72 -0
- package/examples/frontend/README.md +71 -0
- package/examples/frontend/app.js +71 -0
- package/examples/frontend/controllers/apiController.js +39 -0
- package/examples/frontend/controllers/authController.js +220 -0
- package/examples/frontend/controllers/formController.js +47 -0
- package/examples/frontend/controllers/messageController.js +96 -0
- package/examples/frontend/controllers/pageController.js +178 -0
- package/examples/frontend/controllers/staticController.js +167 -0
- package/examples/frontend/routes.json +90 -0
- package/examples/mvc_example/app.js +138 -0
- package/examples/mvc_example/views/home/index.html +26 -0
- package/examples/mvc_example/views/home/simple.html +3 -0
- package/examples/mvc_example/views/layout.html +23 -0
- package/examples/mvc_example/views/test.html +3 -0
- package/examples/mvc_example/views/user/invalid.html +6 -0
- package/examples/mvc_example/views/user/list.html +36 -0
- package/examples/mvc_example/views/user/notfound.html +6 -0
- package/examples/mvc_example/views/user/profile.html +11 -0
- package/examples/mvc_routes_example/app.js +34 -0
- package/examples/mvc_routes_example/controllers/mainController.js +27 -0
- package/examples/mvc_routes_example/controllers/productController.js +47 -0
- package/examples/mvc_routes_example/controllers/userController.js +76 -0
- package/examples/mvc_routes_example/routes.json +30 -0
- package/examples/mvc_routes_example/views/layout.html +31 -0
- package/examples/mvc_routes_example/views/main/index.html +11 -0
- package/examples/mvc_routes_example/views/product/catalog.html +24 -0
- package/examples/mvc_routes_example/views/user/invalid.html +6 -0
- package/examples/mvc_routes_example/views/user/list.html +40 -0
- package/examples/mvc_routes_example/views/user/notfound.html +6 -0
- package/examples/mvc_routes_example/views/user/profile.html +18 -0
- package/examples/public/README.md +92 -0
- package/examples/public/app.js +72 -0
- package/examples/public/controllers/healthController.js +20 -0
- package/examples/public/controllers/mainController.js +22 -0
- package/examples/public/controllers/userController.js +139 -0
- package/examples/public/routes.json +51 -0
- package/examples/v2/README.md +72 -0
- package/examples/v2/app.js +74 -0
- package/examples/v2/app_fixed.js +74 -0
- package/examples/v2/controllers/authController.js +64 -0
- package/examples/v2/controllers/mainController.js +24 -0
- package/examples/v2/controllers/protectedController.js +12 -0
- package/examples/v2/controllers/userController.js +16 -0
- package/examples/v2/package.json +27 -0
- package/examples/v2/routes.json +30 -0
- package/examples/v2/test_api.sh +47 -0
- package/examples/v2/tokens_example.sqlite +0 -0
- package/examples/v2.1_firewall_demo/README.md +113 -0
- package/examples/v2.1_firewall_demo/app.js +182 -0
- package/examples/v2.1_firewall_demo/package.json +27 -0
- package/examples/v2.1_hooks_demo/README.md +85 -0
- package/examples/v2.1_hooks_demo/app.js +101 -0
- package/examples/v2.1_hooks_demo/controllers/hooksController.js +29 -0
- package/examples/v2.1_hooks_demo/controllers/mainController.js +18 -0
- package/examples/v2.1_hooks_demo/package.json +27 -0
- package/examples/v2.1_hooks_demo/routes.json +16 -0
- package/examples/v2.1_openapi_demo/README.md +82 -0
- package/examples/v2.1_openapi_demo/app.js +296 -0
- package/examples/v2.1_openapi_demo/package.json +26 -0
- package/examples/v2_cors/README.md +82 -0
- package/examples/v2_cors/app.js +108 -0
- package/examples/v2_cors/package.json +23 -0
- package/examples/v2_json_auth/README.md +83 -0
- package/examples/v2_json_auth/app.js +72 -0
- package/examples/v2_json_auth/controllers/authController.js +67 -0
- package/examples/v2_json_auth/controllers/mainController.js +16 -0
- package/examples/v2_json_auth/controllers/protectedController.js +12 -0
- package/examples/v2_json_auth/controllers/tokenController.js +28 -0
- package/examples/v2_json_auth/controllers/userController.js +15 -0
- package/examples/v2_json_auth/package.json +26 -0
- package/examples/v2_json_auth/routes.json +37 -0
- package/examples/v2_json_auth/tokens.json +20 -0
- package/examples/v2_mariadb_auth/README.md +94 -0
- package/examples/v2_mariadb_auth/app.js +81 -0
- package/examples/v2_mariadb_auth/controllers/authController.js +95 -0
- package/examples/v2_mariadb_auth/controllers/mainController.js +31 -0
- package/examples/v2_mariadb_auth/controllers/protectedController.js +12 -0
- package/examples/v2_mariadb_auth/controllers/userController.js +17 -0
- package/examples/v2_mariadb_auth/package.json +27 -0
- package/examples/v2_mariadb_auth/routes.json +37 -0
- package/examples/v2_no_auth/README.md +75 -0
- package/examples/v2_no_auth/app.js +72 -0
- package/examples/v2_no_auth/controllers/healthController.js +14 -0
- package/examples/v2_no_auth/controllers/mainController.js +19 -0
- package/examples/v2_no_auth/controllers/productController.js +31 -0
- package/examples/v2_no_auth/controllers/publicController.js +16 -0
- package/examples/v2_no_auth/package.json +22 -0
- package/examples/v2_no_auth/routes.json +37 -0
- package/examples/v2_oauth/README.md +70 -0
- package/examples/v2_oauth/app.js +90 -0
- package/examples/v2_oauth/controllers/mainController.js +45 -0
- package/examples/v2_oauth/controllers/oauthController.js +247 -0
- package/examples/v2_oauth/controllers/protectedController.js +13 -0
- package/examples/v2_oauth/controllers/userController.js +17 -0
- package/examples/v2_oauth/package.json +26 -0
- package/examples/v2_oauth/routes.json +44 -0
- package/examples/v2_openapi/README.md +77 -0
- package/examples/v2_openapi/app.js +222 -0
- package/examples/v2_openapi/controllers/authController.js +52 -0
- package/examples/v2_openapi/controllers/mainController.js +26 -0
- package/examples/v2_openapi/controllers/productController.js +17 -0
- package/examples/v2_openapi/controllers/userController.js +27 -0
- package/examples/v2_openapi/package.json +26 -0
- package/examples/v2_openapi/routes.json +37 -0
- package/generate_token.js +10 -0
- package/index.js +85 -0
- package/jerk.jpg +0 -0
- package/lib/core/handler.js +86 -0
- package/lib/core/hooks.js +224 -0
- package/lib/core/router.js +204 -0
- package/lib/core/securityEnhancedServer.js +752 -0
- package/lib/core/server.js +369 -0
- package/lib/loader/controllerLoader.js +175 -0
- package/lib/loader/routeLoader.js +341 -0
- package/lib/middleware/auditLogger.js +208 -0
- package/lib/middleware/authenticator.js +565 -0
- package/lib/middleware/compressor.js +218 -0
- package/lib/middleware/cors.js +135 -0
- package/lib/middleware/firewall.js +443 -0
- package/lib/middleware/rateLimiter.js +210 -0
- package/lib/middleware/session.js +301 -0
- package/lib/middleware/validator.js +193 -0
- package/lib/mvc/controllerBase.js +207 -0
- package/lib/mvc/viewEngine.js +752 -0
- package/lib/utils/configParser.js +223 -0
- package/lib/utils/logger.js +145 -0
- package/lib/utils/mariadbTokenAdapter.js +226 -0
- package/lib/utils/openapiGenerator.js +140 -0
- package/lib/utils/sqliteTokenAdapter.js +224 -0
- package/lib/utils/tokenManager.js +254 -0
- package/package.json +47 -0
- package/v2examplle/v2_json_auth/README.md +83 -0
- package/v2examplle/v2_json_auth/app.js +72 -0
- package/v2examplle/v2_json_auth/controllers/authController.js +67 -0
- package/v2examplle/v2_json_auth/controllers/mainController.js +16 -0
- package/v2examplle/v2_json_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_json_auth/controllers/tokenController.js +28 -0
- package/v2examplle/v2_json_auth/controllers/userController.js +15 -0
- package/v2examplle/v2_json_auth/package.json +26 -0
- package/v2examplle/v2_json_auth/routes.json +37 -0
- package/v2examplle/v2_json_auth/tokens.json +20 -0
- package/v2examplle/v2_mariadb_auth/README.md +94 -0
- package/v2examplle/v2_mariadb_auth/app.js +81 -0
- package/v2examplle/v2_mariadb_auth/controllers/authController.js +95 -0
- package/v2examplle/v2_mariadb_auth/controllers/mainController.js +31 -0
- package/v2examplle/v2_mariadb_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_mariadb_auth/controllers/userController.js +17 -0
- package/v2examplle/v2_mariadb_auth/package.json +27 -0
- package/v2examplle/v2_mariadb_auth/routes.json +37 -0
- package/v2examplle/v2_sqlite_auth/README.md +72 -0
- package/v2examplle/v2_sqlite_auth/app.js +74 -0
- package/v2examplle/v2_sqlite_auth/app_fixed.js +74 -0
- package/v2examplle/v2_sqlite_auth/controllers/authController.js +64 -0
- package/v2examplle/v2_sqlite_auth/controllers/mainController.js +24 -0
- package/v2examplle/v2_sqlite_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_sqlite_auth/controllers/userController.js +16 -0
- package/v2examplle/v2_sqlite_auth/package.json +27 -0
- package/v2examplle/v2_sqlite_auth/routes.json +30 -0
- package/v2examplle/v2_sqlite_auth/test_api.sh +47 -0
- package/v2examplle/v2_sqlite_auth/tokens_example.sqlite +0 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motor de vistas profesional para el framework API SDK
|
|
3
|
+
* Implementación del componente MVC viewEngine.js
|
|
4
|
+
* Sistema robusto con soporte para hooks, filters y actions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class ViewEngine {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.viewsPath = options.viewsPath || './views';
|
|
13
|
+
this.defaultExtension = options.defaultExtension || '.html';
|
|
14
|
+
this.cacheEnabled = options.cacheEnabled !== false; // Por defecto habilitado
|
|
15
|
+
this.viewCache = new Map(); // Cache de vistas compiladas
|
|
16
|
+
this.logger = options.logger || console;
|
|
17
|
+
|
|
18
|
+
// Sistema de hooks para extensibilidad
|
|
19
|
+
this.hooks = options.hooks || null;
|
|
20
|
+
|
|
21
|
+
// Sistema de filtros
|
|
22
|
+
this.filters = new Map();
|
|
23
|
+
this.helpers = new Map(); // Sistema de helpers personalizados
|
|
24
|
+
this.registerDefaultFilters();
|
|
25
|
+
this.registerDefaultHelpers();
|
|
26
|
+
|
|
27
|
+
// Asegurar que el directorio de vistas existe
|
|
28
|
+
if (!fs.existsSync(this.viewsPath)) {
|
|
29
|
+
fs.mkdirSync(this.viewsPath, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Registra filtros por defecto
|
|
35
|
+
*/
|
|
36
|
+
registerDefaultFilters() {
|
|
37
|
+
// Filtro para escapar HTML
|
|
38
|
+
this.filters.set('escape', (value) => {
|
|
39
|
+
if (typeof value !== 'string') return value;
|
|
40
|
+
return value
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"')
|
|
45
|
+
.replace(/'/g, ''');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Filtro para convertir a mayúsculas
|
|
49
|
+
this.filters.set('upper', (value) => {
|
|
50
|
+
if (typeof value !== 'string') return value;
|
|
51
|
+
return value.toUpperCase();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Filtro para convertir a minúsculas
|
|
55
|
+
this.filters.set('lower', (value) => {
|
|
56
|
+
if (typeof value !== 'string') return value;
|
|
57
|
+
return value.toLowerCase();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Filtro para capitalizar
|
|
61
|
+
this.filters.set('capitalize', (value) => {
|
|
62
|
+
if (typeof value !== 'string') return value;
|
|
63
|
+
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Filtro para formatear fecha
|
|
67
|
+
this.filters.set('date', (value, format = 'YYYY-MM-DD HH:mm:ss') => {
|
|
68
|
+
if (!value) return value;
|
|
69
|
+
const date = new Date(value);
|
|
70
|
+
if (isNaN(date.getTime())) return value;
|
|
71
|
+
|
|
72
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
73
|
+
const padYear = (n) => n.toString().padStart(4, '0');
|
|
74
|
+
|
|
75
|
+
return format
|
|
76
|
+
.replace('YYYY', padYear(date.getFullYear()))
|
|
77
|
+
.replace('MM', pad(date.getMonth() + 1))
|
|
78
|
+
.replace('DD', pad(date.getDate()))
|
|
79
|
+
.replace('HH', pad(date.getHours()))
|
|
80
|
+
.replace('mm', pad(date.getMinutes()))
|
|
81
|
+
.replace('ss', pad(date.getSeconds()));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Filtro para truncar texto
|
|
85
|
+
this.filters.set('truncate', (value, length = 100, suffix = '...') => {
|
|
86
|
+
if (typeof value !== 'string') return value;
|
|
87
|
+
return value.length > length ? value.substring(0, length) + suffix : value;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Registra helpers por defecto
|
|
93
|
+
*/
|
|
94
|
+
registerDefaultHelpers() {
|
|
95
|
+
// Helper para formatear fecha
|
|
96
|
+
this.helpers.set('formatDate', (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
|
97
|
+
if (!date) return '';
|
|
98
|
+
const d = new Date(date);
|
|
99
|
+
if (isNaN(d.getTime())) return date;
|
|
100
|
+
|
|
101
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
102
|
+
const padYear = (n) => n.toString().padStart(4, '0');
|
|
103
|
+
|
|
104
|
+
return format
|
|
105
|
+
.replace('YYYY', padYear(d.getFullYear()))
|
|
106
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
107
|
+
.replace('DD', pad(d.getDate()))
|
|
108
|
+
.replace('HH', pad(d.getHours()))
|
|
109
|
+
.replace('mm', pad(d.getMinutes()))
|
|
110
|
+
.replace('ss', pad(d.getSeconds()));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Helper para verificar si un valor es par
|
|
114
|
+
this.helpers.set('isEven', (value) => {
|
|
115
|
+
return Number(value) % 2 === 0;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Helper para contar elementos
|
|
119
|
+
this.helpers.set('count', (array) => {
|
|
120
|
+
return Array.isArray(array) ? array.length : 0;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Registra un filtro personalizado
|
|
126
|
+
* @param {string} name - Nombre del filtro
|
|
127
|
+
* @param {Function} filterFn - Función del filtro
|
|
128
|
+
*/
|
|
129
|
+
addFilter(name, filterFn) {
|
|
130
|
+
this.filters.set(name, filterFn);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Registra un helper personalizado
|
|
135
|
+
* @param {string} name - Nombre del helper
|
|
136
|
+
* @param {Function} helperFn - Función del helper
|
|
137
|
+
*/
|
|
138
|
+
addHelper(name, helperFn) {
|
|
139
|
+
this.helpers.set(name, helperFn);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Aplica un filtro a un valor
|
|
144
|
+
* @param {*} value - Valor a filtrar
|
|
145
|
+
* @param {string} filterName - Nombre del filtro
|
|
146
|
+
* @param {...*} args - Argumentos adicionales para el filtro
|
|
147
|
+
* @returns {*} - Valor filtrado
|
|
148
|
+
*/
|
|
149
|
+
applyFilter(value, filterName, ...args) {
|
|
150
|
+
const filter = this.filters.get(filterName);
|
|
151
|
+
if (filter) {
|
|
152
|
+
return filter(value, ...args);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Ejecuta un helper
|
|
159
|
+
* @param {string} helperName - Nombre del helper
|
|
160
|
+
* @param {...*} args - Argumentos para el helper
|
|
161
|
+
* @returns {*} - Resultado del helper
|
|
162
|
+
*/
|
|
163
|
+
executeHelper(helperName, ...args) {
|
|
164
|
+
const helper = this.helpers.get(helperName);
|
|
165
|
+
if (helper) {
|
|
166
|
+
return helper(...args);
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Valida la sintaxis de un template
|
|
173
|
+
* @param {string} template - Template a validar
|
|
174
|
+
* @returns {Array} - Array de errores encontrados
|
|
175
|
+
*/
|
|
176
|
+
static validateTemplate(template) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
|
|
179
|
+
// Validar apertura y cierre de bloques condicionales
|
|
180
|
+
const ifMatches = template.match(/\{\{if\s+.*?\}\}/g) || [];
|
|
181
|
+
const endifMatches = template.match(/\{\{endif\}\}/g) || [];
|
|
182
|
+
|
|
183
|
+
if (ifMatches.length !== endifMatches.length) {
|
|
184
|
+
errors.push(`Desbalance de bloques {{if}}/{{endif}}: ${ifMatches.length} aperturas, ${endifMatches.length} cierres`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validar apertura y cierre de bloques foreach
|
|
188
|
+
const foreachMatches = template.match(/\{\{foreach:.*?\}\}/g) || [];
|
|
189
|
+
const endforeachMatches = template.match(/\{\{endforeach\}\}/g) || [];
|
|
190
|
+
|
|
191
|
+
if (foreachMatches.length !== endforeachMatches.length) {
|
|
192
|
+
errors.push(`Desbalance de bloques {{foreach}}/{{endforeach}}: ${foreachMatches.length} aperturas, ${endforeachMatches.length} cierres`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Validar apertura y cierre de bloques include
|
|
196
|
+
const includeMatches = template.match(/\{\{include:.*?\}\}/g) || [];
|
|
197
|
+
|
|
198
|
+
// Validar sintaxis básica de variables
|
|
199
|
+
const malformedVars = template.match(/\{\{[^}]*$/g);
|
|
200
|
+
if (malformedVars && malformedVars.length > 0) {
|
|
201
|
+
errors.push(`Variables mal formadas: ${malformedVars.join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return errors;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Renderiza una vista con variables
|
|
209
|
+
* @param {string} viewName - Nombre de la vista a renderizar
|
|
210
|
+
* @param {Object} data - Variables a pasar a la vista
|
|
211
|
+
* @param {Object} options - Opciones adicionales
|
|
212
|
+
* @returns {string} - Contenido renderizado de la vista
|
|
213
|
+
*/
|
|
214
|
+
render(viewName, data = {}, options = {}) {
|
|
215
|
+
// Obtener la ruta completa de la vista
|
|
216
|
+
const viewPath = this.getViewPath(viewName);
|
|
217
|
+
|
|
218
|
+
// Verificar si la vista existe
|
|
219
|
+
if (!fs.existsSync(viewPath)) {
|
|
220
|
+
throw new Error(`Vista no encontrada: ${viewPath}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Intentar obtener del cache si está habilitado
|
|
224
|
+
if (this.cacheEnabled && this.viewCache.has(viewPath)) {
|
|
225
|
+
const cachedView = this.viewCache.get(viewPath);
|
|
226
|
+
return this.processTemplate(cachedView, data, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Leer el contenido de la vista
|
|
230
|
+
let viewContent = fs.readFileSync(viewPath, 'utf8');
|
|
231
|
+
|
|
232
|
+
// Validar sintaxis del template si está habilitado
|
|
233
|
+
if (options.validateSyntax !== false) {
|
|
234
|
+
const validationErrors = ViewEngine.validateTemplate(viewContent);
|
|
235
|
+
if (validationErrors.length > 0) {
|
|
236
|
+
this.logger.warn(`Errores de sintaxis en la vista ${viewName}:`, validationErrors);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Procesar bloques de inclusión (similar a <?php include ?>)
|
|
241
|
+
viewContent = this.processIncludes(viewContent, path.dirname(viewPath));
|
|
242
|
+
|
|
243
|
+
// Si el cache está habilitado, guardar la vista compilada (sin variables)
|
|
244
|
+
if (this.cacheEnabled) {
|
|
245
|
+
this.viewCache.set(viewPath, viewContent);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Procesar el template con las variables
|
|
249
|
+
return this.processTemplate(viewContent, data, options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Obtiene la ruta completa de una vista
|
|
254
|
+
* @param {string} viewName - Nombre de la vista
|
|
255
|
+
* @returns {string} - Ruta completa a la vista
|
|
256
|
+
*/
|
|
257
|
+
getViewPath(viewName) {
|
|
258
|
+
// Convertir puntos a barras para permitir vistas anidadas
|
|
259
|
+
const normalizedPath = viewName.replace(/\./g, '/');
|
|
260
|
+
|
|
261
|
+
// Construir la ruta
|
|
262
|
+
let viewPath = path.join(this.viewsPath, normalizedPath);
|
|
263
|
+
|
|
264
|
+
// Añadir extensión por defecto si no está presente
|
|
265
|
+
if (!path.extname(viewPath)) {
|
|
266
|
+
viewPath += this.defaultExtension;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return viewPath;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Procesa bloques de inclusión en la vista
|
|
274
|
+
* @param {string} content - Contenido de la vista
|
|
275
|
+
* @param {string} currentDir - Directorio actual para resolver inclusiones relativas
|
|
276
|
+
* @returns {string} - Contenido con inclusiones procesadas
|
|
277
|
+
*/
|
|
278
|
+
processIncludes(content, currentDir) {
|
|
279
|
+
// Patrón para encontrar bloques de inclusión como {{include:nombre_vista}}
|
|
280
|
+
const includePattern = /\{\{include:(.*?)\}\}/g;
|
|
281
|
+
|
|
282
|
+
return content.replace(includePattern, (match, includePath) => {
|
|
283
|
+
try {
|
|
284
|
+
// Resolver la ruta de inclusión
|
|
285
|
+
let includeFullPath;
|
|
286
|
+
|
|
287
|
+
// Si la ruta empieza con ./ o ../, es relativa al directorio actual
|
|
288
|
+
if (includePath.startsWith('./') || includePath.startsWith('../')) {
|
|
289
|
+
includeFullPath = path.resolve(currentDir, includePath);
|
|
290
|
+
|
|
291
|
+
// Si no tiene extensión, añadir la por defecto
|
|
292
|
+
if (!path.extname(includeFullPath)) {
|
|
293
|
+
includeFullPath += this.defaultExtension;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Sino, usar la ruta de vistas por defecto
|
|
297
|
+
includeFullPath = this.getViewPath(includePath);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Verificar si el archivo de inclusión existe
|
|
301
|
+
if (!fs.existsSync(includeFullPath)) {
|
|
302
|
+
this.logger.warn(`Archivo de inclusión no encontrado: ${includeFullPath}`);
|
|
303
|
+
return `<!-- Archivo de inclusión no encontrado: ${includePath} -->`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Leer y procesar el contenido del archivo incluido
|
|
307
|
+
let includedContent = fs.readFileSync(includeFullPath, 'utf8');
|
|
308
|
+
includedContent = this.processIncludes(includedContent, path.dirname(includeFullPath)); // Recursión para inclusiones anidadas
|
|
309
|
+
|
|
310
|
+
return includedContent;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.logger.error(`Error procesando inclusión: ${includePath}`, error);
|
|
313
|
+
return `<!-- Error procesando inclusión: ${includePath} -->`;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Procesa un template con variables
|
|
320
|
+
* @param {string} template - Template a procesar
|
|
321
|
+
* @param {Object} data - Variables a sustituir
|
|
322
|
+
* @param {Object} options - Opciones adicionales
|
|
323
|
+
* @returns {string} - Template procesado
|
|
324
|
+
*/
|
|
325
|
+
processTemplate(template, data, options = {}) {
|
|
326
|
+
let processedTemplate = template;
|
|
327
|
+
|
|
328
|
+
// Aplicar hooks antes de procesar el template
|
|
329
|
+
if (this.hooks) {
|
|
330
|
+
processedTemplate = this.hooks.applyFilters('template_pre_process', processedTemplate, data);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Procesar el template con múltiples pasadas para manejar anidaciones
|
|
334
|
+
let previousTemplate;
|
|
335
|
+
let iterations = 0;
|
|
336
|
+
const maxIterations = 10; // Prevenir bucles infinitos
|
|
337
|
+
|
|
338
|
+
do {
|
|
339
|
+
previousTemplate = processedTemplate;
|
|
340
|
+
|
|
341
|
+
// Procesar bucles foreach: {{foreach:array}}contenido{{endforeach}}
|
|
342
|
+
processedTemplate = this.processForeach(processedTemplate, data, options);
|
|
343
|
+
|
|
344
|
+
// Procesar estructuras condicionales: {{if variable}}contenido{{endif}}
|
|
345
|
+
processedTemplate = this.processConditionals(processedTemplate, data, options);
|
|
346
|
+
|
|
347
|
+
// Reemplazar variables simples y anidadas: {{variable}} y {{objeto.propiedad}} -> valor
|
|
348
|
+
processedTemplate = this.replaceVariablesAndFilters(processedTemplate, data, options);
|
|
349
|
+
|
|
350
|
+
iterations++;
|
|
351
|
+
} while (previousTemplate !== processedTemplate && iterations < maxIterations);
|
|
352
|
+
|
|
353
|
+
// Aplicar hooks después de procesar el template
|
|
354
|
+
if (this.hooks) {
|
|
355
|
+
processedTemplate = this.hooks.applyFilters('template_post_process', processedTemplate, data);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return processedTemplate;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Reemplaza variables y aplica filtros
|
|
363
|
+
* @param {string} template - Template a procesar
|
|
364
|
+
* @param {Object} data - Variables a sustituir
|
|
365
|
+
* @param {Object} options - Opciones adicionales
|
|
366
|
+
* @returns {string} - Template con variables reemplazadas
|
|
367
|
+
*/
|
|
368
|
+
replaceVariablesAndFilters(template, data, options = {}) {
|
|
369
|
+
let processedTemplate = template;
|
|
370
|
+
|
|
371
|
+
// Patrón para encontrar variables con filtros: {{variable|filtro1|filtro2:arg1,arg2}}
|
|
372
|
+
const variablePattern = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
373
|
+
|
|
374
|
+
processedTemplate = processedTemplate.replace(variablePattern, (match, variableWithFilters) => {
|
|
375
|
+
// Separar la variable de los filtros
|
|
376
|
+
const parts = variableWithFilters.split('|');
|
|
377
|
+
const variableName = parts[0].trim();
|
|
378
|
+
|
|
379
|
+
// Verificar si es un helper (función)
|
|
380
|
+
if (variableName.includes('(') && variableName.includes(')')) {
|
|
381
|
+
// Es un helper, procesarlo
|
|
382
|
+
const helperMatch = variableName.match(/^([a-zA-Z0-9_]+)\((.*)\)$/);
|
|
383
|
+
if (helperMatch) {
|
|
384
|
+
const helperName = helperMatch[1];
|
|
385
|
+
const argsString = helperMatch[2];
|
|
386
|
+
|
|
387
|
+
// Parsear argumentos
|
|
388
|
+
let args = [];
|
|
389
|
+
if (argsString.trim()) {
|
|
390
|
+
args = this.parseArguments(argsString);
|
|
391
|
+
// Si los argumentos son variables, obtener sus valores
|
|
392
|
+
args = args.map(arg => {
|
|
393
|
+
// Si no es una cadena entre comillas, intentar obtener el valor como variable
|
|
394
|
+
if (!(arg.startsWith('"') && arg.endsWith('"')) &&
|
|
395
|
+
!(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
396
|
+
const varValue = this.getVariableValue(arg, data);
|
|
397
|
+
return varValue !== undefined ? varValue : arg;
|
|
398
|
+
}
|
|
399
|
+
return arg;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return String(this.executeHelper(helperName, ...args));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Obtener el valor de la variable
|
|
408
|
+
let value = this.getVariableValue(variableName, data);
|
|
409
|
+
|
|
410
|
+
// Si la variable no existe y estamos en modo desarrollo, registrar advertencia
|
|
411
|
+
if (value === undefined && options.showWarnings !== false) {
|
|
412
|
+
this.logger.warn(`Variable no definida en template: ${variableName}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Si la variable no existe, devolver la variable original para debugging
|
|
416
|
+
if (value === undefined) {
|
|
417
|
+
return options.preserveUndefined !== false ? match : '';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Aplicar filtros secuencialmente
|
|
421
|
+
for (let i = 1; i < parts.length; i++) {
|
|
422
|
+
const filterPart = parts[i].trim();
|
|
423
|
+
// Modificar la expresión regular para manejar argumentos entre comillas
|
|
424
|
+
const filterMatch = filterPart.match(/^([a-zA-Z0-9_]+)(?::(.*))?$/);
|
|
425
|
+
|
|
426
|
+
if (filterMatch) {
|
|
427
|
+
const filterName = filterMatch[1];
|
|
428
|
+
let filterArgs = [];
|
|
429
|
+
|
|
430
|
+
if (filterMatch[2]) {
|
|
431
|
+
// Separar argumentos por coma, pero respetando cadenas entre comillas
|
|
432
|
+
filterArgs = this.parseArguments(filterMatch[2]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
value = this.applyFilter(value, filterName, ...filterArgs);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return String(value);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
return processedTemplate;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Procesa estructuras condicionales en el template
|
|
447
|
+
* @param {string} template - Template a procesar
|
|
448
|
+
* @param {Object} data - Variables disponibles
|
|
449
|
+
* @param {Object} options - Opciones adicionales
|
|
450
|
+
* @returns {string} - Template con condiciones procesadas
|
|
451
|
+
*/
|
|
452
|
+
processConditionals(template, data, options = {}) {
|
|
453
|
+
// Patrón para encontrar bloques condicionales: {{if variable}}contenido{{endif}} o {{if variable}}contenido{{else}}contenido{{endif}}
|
|
454
|
+
const conditionalPattern = /\{\{if\s+(.*?)\}\}(.*?)(?:\{\{else\}\}(.*?))?\{\{endif\}\}/gs;
|
|
455
|
+
|
|
456
|
+
return template.replace(conditionalPattern, (match, condition, ifContent, elseContent) => {
|
|
457
|
+
// Limpiar la condición
|
|
458
|
+
const cleanCondition = condition.trim();
|
|
459
|
+
|
|
460
|
+
// Evaluar la condición (ahora soporta variables anidadas como objeto.propiedad)
|
|
461
|
+
let conditionResult = false;
|
|
462
|
+
|
|
463
|
+
// Soportar condiciones simples como: variable, !variable, variable == valor, etc.
|
|
464
|
+
if (cleanCondition.startsWith('!')) {
|
|
465
|
+
const varName = cleanCondition.substring(1).trim();
|
|
466
|
+
conditionResult = !this.getVariableValue(varName, data);
|
|
467
|
+
} else if (cleanCondition.includes('==')) {
|
|
468
|
+
const [left, right] = cleanCondition.split('==').map(s => s.trim());
|
|
469
|
+
const leftVal = this.getVariableValue(left, data);
|
|
470
|
+
const rightVal = this.getVariableValue(right, data);
|
|
471
|
+
conditionResult = leftVal == rightVal;
|
|
472
|
+
} else if (cleanCondition.includes('===')) {
|
|
473
|
+
const [left, right] = cleanCondition.split('===').map(s => s.trim());
|
|
474
|
+
const leftVal = this.getVariableValue(left, data);
|
|
475
|
+
const rightVal = this.getVariableValue(right, data);
|
|
476
|
+
conditionResult = leftVal === rightVal;
|
|
477
|
+
} else if (cleanCondition.includes('!=')) {
|
|
478
|
+
const [left, right] = cleanCondition.split('!=').map(s => s.trim());
|
|
479
|
+
const leftVal = this.getVariableValue(left, data);
|
|
480
|
+
const rightVal = this.getVariableValue(right, data);
|
|
481
|
+
conditionResult = leftVal != rightVal;
|
|
482
|
+
} else if (cleanCondition.includes('!==')) {
|
|
483
|
+
const [left, right] = cleanCondition.split('!==').map(s => s.trim());
|
|
484
|
+
const leftVal = this.getVariableValue(left, data);
|
|
485
|
+
const rightVal = this.getVariableValue(right, data);
|
|
486
|
+
conditionResult = leftVal !== rightVal;
|
|
487
|
+
} else {
|
|
488
|
+
// Condición simple: verdadero si la variable existe y no es falsy
|
|
489
|
+
conditionResult = !!this.getVariableValue(cleanCondition, data);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Devolver el contenido correspondiente según el resultado de la condición
|
|
493
|
+
return conditionResult ? ifContent : (elseContent || '');
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Procesa bucles foreach en el template
|
|
499
|
+
* @param {string} template - Template a procesar
|
|
500
|
+
* @param {Object} data - Variables disponibles
|
|
501
|
+
* @param {Object} options - Opciones adicionales
|
|
502
|
+
* @returns {string} - Template con bucles procesados
|
|
503
|
+
*/
|
|
504
|
+
processForeach(template, data, options = {}) {
|
|
505
|
+
// Patrón para encontrar bloques foreach: {{foreach:array}}contenido{{endforeach}}
|
|
506
|
+
const foreachPattern = /\{\{foreach:(.*?)\}\}(.*?)\{\{endforeach\}\}/gs;
|
|
507
|
+
|
|
508
|
+
return template.replace(foreachPattern, (match, arraySpec, content) => {
|
|
509
|
+
// Analizar la especificación del array: array as key => value o solo array
|
|
510
|
+
const arrayMatch = arraySpec.match(/^(\w+)\s+as\s+(\w+)\s*=>\s*(\w+)$/);
|
|
511
|
+
|
|
512
|
+
let arrayName, keyVar, valueVar;
|
|
513
|
+
if (arrayMatch) {
|
|
514
|
+
// Formato: array as key => value
|
|
515
|
+
[, arrayName, keyVar, valueVar] = arrayMatch;
|
|
516
|
+
} else {
|
|
517
|
+
// Formato: array (usar índice como clave y valor como elemento)
|
|
518
|
+
arrayName = arraySpec.trim();
|
|
519
|
+
keyVar = 'index';
|
|
520
|
+
valueVar = 'item';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Obtener el array de datos
|
|
524
|
+
const array = data[arrayName];
|
|
525
|
+
if (!Array.isArray(array) && typeof array !== 'object') {
|
|
526
|
+
this.logger.warn(`La variable '${arrayName}' no es un array u objeto iterable`);
|
|
527
|
+
return '';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let result = '';
|
|
531
|
+
|
|
532
|
+
// Iterar sobre el array u objeto
|
|
533
|
+
if (Array.isArray(array)) {
|
|
534
|
+
array.forEach((item, index) => {
|
|
535
|
+
// Crear un contexto temporal con las variables del bucle
|
|
536
|
+
const loopContext = { ...data };
|
|
537
|
+
loopContext[keyVar] = index;
|
|
538
|
+
loopContext[valueVar] = item;
|
|
539
|
+
|
|
540
|
+
// Procesar el contenido del bucle con el contexto actual
|
|
541
|
+
let loopContent = content;
|
|
542
|
+
|
|
543
|
+
// Primero procesar las variables del objeto interno (como item.name, item.email)
|
|
544
|
+
if (typeof item === 'object' && item !== null) {
|
|
545
|
+
for (const [propKey, propValue] of Object.entries(item)) {
|
|
546
|
+
const fullKey = `${valueVar}.${propKey}`;
|
|
547
|
+
const escapedKey = fullKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
548
|
+
const regex = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g');
|
|
549
|
+
|
|
550
|
+
let stringValue;
|
|
551
|
+
if (typeof propValue === 'object' && propValue !== null) {
|
|
552
|
+
stringValue = JSON.stringify(propValue);
|
|
553
|
+
} else {
|
|
554
|
+
stringValue = String(propValue);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
loopContent = loopContent.replace(regex, stringValue);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Luego procesar las variables generales del contexto
|
|
562
|
+
for (const [key, value] of Object.entries(loopContext)) {
|
|
563
|
+
// Solo procesar variables simples, no las que ya se procesaron como item.prop
|
|
564
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
565
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
566
|
+
const regex = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g');
|
|
567
|
+
|
|
568
|
+
let stringValue;
|
|
569
|
+
if (typeof value === 'object' && value !== null) {
|
|
570
|
+
stringValue = JSON.stringify(value);
|
|
571
|
+
} else {
|
|
572
|
+
stringValue = String(value);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
loopContent = loopContent.replace(regex, stringValue);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Procesar condiciones y bucles anidados
|
|
580
|
+
// Aplicar recursivamente el procesamiento para manejar condiciones anidadas
|
|
581
|
+
let previousLoopContent;
|
|
582
|
+
let iterations = 0;
|
|
583
|
+
const maxIterations = 5; // Prevenir bucles infinitos
|
|
584
|
+
|
|
585
|
+
do {
|
|
586
|
+
previousLoopContent = loopContent;
|
|
587
|
+
loopContent = this.processConditionals(loopContent, loopContext, options);
|
|
588
|
+
loopContent = this.processForeach(loopContent, loopContext, options);
|
|
589
|
+
loopContent = this.replaceVariablesAndFilters(loopContent, loopContext, options);
|
|
590
|
+
iterations++;
|
|
591
|
+
} while (previousLoopContent !== loopContent && iterations < maxIterations);
|
|
592
|
+
|
|
593
|
+
result += loopContent;
|
|
594
|
+
});
|
|
595
|
+
} else {
|
|
596
|
+
// Para objetos
|
|
597
|
+
for (const [key, value] of Object.entries(array)) {
|
|
598
|
+
// Crear un contexto temporal con las variables del bucle
|
|
599
|
+
const loopContext = { ...data };
|
|
600
|
+
loopContext[keyVar] = key;
|
|
601
|
+
loopContext[valueVar] = value;
|
|
602
|
+
|
|
603
|
+
// Procesar el contenido del bucle con el contexto actual
|
|
604
|
+
let loopContent = content;
|
|
605
|
+
|
|
606
|
+
// Primero procesar las variables del objeto interno (como item.nombre, item.valor)
|
|
607
|
+
if (typeof value === 'object' && value !== null) {
|
|
608
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
609
|
+
const fullKey = `${valueVar}.${propKey}`;
|
|
610
|
+
const escapedKey = fullKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
611
|
+
const regex = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g');
|
|
612
|
+
|
|
613
|
+
let stringValue;
|
|
614
|
+
if (typeof propValue === 'object' && propValue !== null) {
|
|
615
|
+
stringValue = JSON.stringify(propValue);
|
|
616
|
+
} else {
|
|
617
|
+
stringValue = String(propValue);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
loopContent = loopContent.replace(regex, stringValue);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Luego procesar las variables generales del contexto
|
|
625
|
+
for (const [k, v] of Object.entries(loopContext)) {
|
|
626
|
+
// Solo procesar variables simples, no las que ya se procesaron como item.prop
|
|
627
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
628
|
+
const escapedKey = k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
629
|
+
const regex = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g');
|
|
630
|
+
|
|
631
|
+
let stringValue;
|
|
632
|
+
if (typeof v === 'object' && v !== null) {
|
|
633
|
+
stringValue = JSON.stringify(v);
|
|
634
|
+
} else {
|
|
635
|
+
stringValue = String(v);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
loopContent = loopContent.replace(regex, stringValue);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Procesar condiciones y bucles anidados
|
|
643
|
+
// Aplicar recursivamente el procesamiento para manejar condiciones anidadas
|
|
644
|
+
let previousLoopContent;
|
|
645
|
+
let iterations = 0;
|
|
646
|
+
const maxIterations = 5; // Prevenir bucles infinitos
|
|
647
|
+
|
|
648
|
+
do {
|
|
649
|
+
previousLoopContent = loopContent;
|
|
650
|
+
loopContent = this.processConditionals(loopContent, loopContext, options);
|
|
651
|
+
loopContent = this.processForeach(loopContent, loopContext, options);
|
|
652
|
+
loopContent = this.replaceVariablesAndFilters(loopContent, loopContext, options);
|
|
653
|
+
iterations++;
|
|
654
|
+
} while (previousLoopContent !== loopContent && iterations < maxIterations);
|
|
655
|
+
|
|
656
|
+
result += loopContent;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return result;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Obtiene el valor de una variable, soportando anidación (objeto.propiedad)
|
|
666
|
+
* @param {string} variableName - Nombre de la variable (puede ser anidada)
|
|
667
|
+
* @param {Object} data - Datos donde buscar la variable
|
|
668
|
+
* @returns {*} - Valor de la variable o undefined si no existe
|
|
669
|
+
*/
|
|
670
|
+
getVariableValue(variableName, data) {
|
|
671
|
+
// Si no contiene punto, es una variable simple
|
|
672
|
+
if (!variableName.includes('.')) {
|
|
673
|
+
return data[variableName];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Si contiene puntos, navegar por las propiedades anidadas
|
|
677
|
+
const parts = variableName.split('.');
|
|
678
|
+
let value = data;
|
|
679
|
+
|
|
680
|
+
for (const part of parts) {
|
|
681
|
+
if (value && typeof value === 'object') {
|
|
682
|
+
value = value[part];
|
|
683
|
+
} else {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return value;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Parsea argumentos de filtros o helpers, respetando cadenas entre comillas
|
|
693
|
+
* @param {string} argsString - Cadena de argumentos
|
|
694
|
+
* @returns {Array} - Array de argumentos parseados
|
|
695
|
+
*/
|
|
696
|
+
parseArguments(argsString) {
|
|
697
|
+
const args = [];
|
|
698
|
+
let currentArg = '';
|
|
699
|
+
let inQuotes = false;
|
|
700
|
+
let quoteChar = null;
|
|
701
|
+
|
|
702
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
703
|
+
const char = argsString[i];
|
|
704
|
+
|
|
705
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
706
|
+
inQuotes = true;
|
|
707
|
+
quoteChar = char;
|
|
708
|
+
} else if (char === quoteChar && inQuotes) {
|
|
709
|
+
inQuotes = false;
|
|
710
|
+
quoteChar = null;
|
|
711
|
+
} else if (char === ',' && !inQuotes) {
|
|
712
|
+
args.push(currentArg.trim());
|
|
713
|
+
currentArg = '';
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
currentArg += char;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (currentArg.trim() !== '') {
|
|
721
|
+
args.push(currentArg.trim());
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Remover comillas de los argumentos si están presentes
|
|
725
|
+
return args.map(arg => {
|
|
726
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
727
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
728
|
+
return arg.substring(1, arg.length - 1);
|
|
729
|
+
}
|
|
730
|
+
return arg;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Verifica si una vista existe
|
|
736
|
+
* @param {string} viewName - Nombre de la vista
|
|
737
|
+
* @returns {boolean} - Verdadero si la vista existe
|
|
738
|
+
*/
|
|
739
|
+
viewExists(viewName) {
|
|
740
|
+
const viewPath = this.getViewPath(viewName);
|
|
741
|
+
return fs.existsSync(viewPath);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Limpia el cache de vistas
|
|
746
|
+
*/
|
|
747
|
+
clearCache() {
|
|
748
|
+
this.viewCache.clear();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
module.exports = ViewEngine;
|