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.
Files changed (177) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +171 -0
  3. package/doc/EXTENSION_MANUAL.md +958 -0
  4. package/doc/FIREWALL_MANUAL.md +419 -0
  5. package/doc/HOOKS_REFERENCE_IMPROVED.md +599 -0
  6. package/doc/MANUAL_API_SDK.md +539 -0
  7. package/doc/MANUAL_MVC.md +397 -0
  8. package/doc/MARIADB_TOKENS_IMPLEMENTATION.md +113 -0
  9. package/doc/MIDDLEWARE_MANUAL.md +521 -0
  10. package/doc/OAUTH2_GOOGLE_MANUAL.md +408 -0
  11. package/doc/frontend-and-sessions.md +356 -0
  12. package/examples/advanced/controllers/productController.js +64 -0
  13. package/examples/advanced/controllers/userController.js +85 -0
  14. package/examples/advanced/routes.json +51 -0
  15. package/examples/advanced_example.js +93 -0
  16. package/examples/basic/controllers/userController.js +85 -0
  17. package/examples/basic_example.js +72 -0
  18. package/examples/frontend/README.md +71 -0
  19. package/examples/frontend/app.js +71 -0
  20. package/examples/frontend/controllers/apiController.js +39 -0
  21. package/examples/frontend/controllers/authController.js +220 -0
  22. package/examples/frontend/controllers/formController.js +47 -0
  23. package/examples/frontend/controllers/messageController.js +96 -0
  24. package/examples/frontend/controllers/pageController.js +178 -0
  25. package/examples/frontend/controllers/staticController.js +167 -0
  26. package/examples/frontend/routes.json +90 -0
  27. package/examples/mvc_example/app.js +138 -0
  28. package/examples/mvc_example/views/home/index.html +26 -0
  29. package/examples/mvc_example/views/home/simple.html +3 -0
  30. package/examples/mvc_example/views/layout.html +23 -0
  31. package/examples/mvc_example/views/test.html +3 -0
  32. package/examples/mvc_example/views/user/invalid.html +6 -0
  33. package/examples/mvc_example/views/user/list.html +36 -0
  34. package/examples/mvc_example/views/user/notfound.html +6 -0
  35. package/examples/mvc_example/views/user/profile.html +11 -0
  36. package/examples/mvc_routes_example/app.js +34 -0
  37. package/examples/mvc_routes_example/controllers/mainController.js +27 -0
  38. package/examples/mvc_routes_example/controllers/productController.js +47 -0
  39. package/examples/mvc_routes_example/controllers/userController.js +76 -0
  40. package/examples/mvc_routes_example/routes.json +30 -0
  41. package/examples/mvc_routes_example/views/layout.html +31 -0
  42. package/examples/mvc_routes_example/views/main/index.html +11 -0
  43. package/examples/mvc_routes_example/views/product/catalog.html +24 -0
  44. package/examples/mvc_routes_example/views/user/invalid.html +6 -0
  45. package/examples/mvc_routes_example/views/user/list.html +40 -0
  46. package/examples/mvc_routes_example/views/user/notfound.html +6 -0
  47. package/examples/mvc_routes_example/views/user/profile.html +18 -0
  48. package/examples/public/README.md +92 -0
  49. package/examples/public/app.js +72 -0
  50. package/examples/public/controllers/healthController.js +20 -0
  51. package/examples/public/controllers/mainController.js +22 -0
  52. package/examples/public/controllers/userController.js +139 -0
  53. package/examples/public/routes.json +51 -0
  54. package/examples/v2/README.md +72 -0
  55. package/examples/v2/app.js +74 -0
  56. package/examples/v2/app_fixed.js +74 -0
  57. package/examples/v2/controllers/authController.js +64 -0
  58. package/examples/v2/controllers/mainController.js +24 -0
  59. package/examples/v2/controllers/protectedController.js +12 -0
  60. package/examples/v2/controllers/userController.js +16 -0
  61. package/examples/v2/package.json +27 -0
  62. package/examples/v2/routes.json +30 -0
  63. package/examples/v2/test_api.sh +47 -0
  64. package/examples/v2/tokens_example.sqlite +0 -0
  65. package/examples/v2.1_firewall_demo/README.md +113 -0
  66. package/examples/v2.1_firewall_demo/app.js +182 -0
  67. package/examples/v2.1_firewall_demo/package.json +27 -0
  68. package/examples/v2.1_hooks_demo/README.md +85 -0
  69. package/examples/v2.1_hooks_demo/app.js +101 -0
  70. package/examples/v2.1_hooks_demo/controllers/hooksController.js +29 -0
  71. package/examples/v2.1_hooks_demo/controllers/mainController.js +18 -0
  72. package/examples/v2.1_hooks_demo/package.json +27 -0
  73. package/examples/v2.1_hooks_demo/routes.json +16 -0
  74. package/examples/v2.1_openapi_demo/README.md +82 -0
  75. package/examples/v2.1_openapi_demo/app.js +296 -0
  76. package/examples/v2.1_openapi_demo/package.json +26 -0
  77. package/examples/v2_cors/README.md +82 -0
  78. package/examples/v2_cors/app.js +108 -0
  79. package/examples/v2_cors/package.json +23 -0
  80. package/examples/v2_json_auth/README.md +83 -0
  81. package/examples/v2_json_auth/app.js +72 -0
  82. package/examples/v2_json_auth/controllers/authController.js +67 -0
  83. package/examples/v2_json_auth/controllers/mainController.js +16 -0
  84. package/examples/v2_json_auth/controllers/protectedController.js +12 -0
  85. package/examples/v2_json_auth/controllers/tokenController.js +28 -0
  86. package/examples/v2_json_auth/controllers/userController.js +15 -0
  87. package/examples/v2_json_auth/package.json +26 -0
  88. package/examples/v2_json_auth/routes.json +37 -0
  89. package/examples/v2_json_auth/tokens.json +20 -0
  90. package/examples/v2_mariadb_auth/README.md +94 -0
  91. package/examples/v2_mariadb_auth/app.js +81 -0
  92. package/examples/v2_mariadb_auth/controllers/authController.js +95 -0
  93. package/examples/v2_mariadb_auth/controllers/mainController.js +31 -0
  94. package/examples/v2_mariadb_auth/controllers/protectedController.js +12 -0
  95. package/examples/v2_mariadb_auth/controllers/userController.js +17 -0
  96. package/examples/v2_mariadb_auth/package.json +27 -0
  97. package/examples/v2_mariadb_auth/routes.json +37 -0
  98. package/examples/v2_no_auth/README.md +75 -0
  99. package/examples/v2_no_auth/app.js +72 -0
  100. package/examples/v2_no_auth/controllers/healthController.js +14 -0
  101. package/examples/v2_no_auth/controllers/mainController.js +19 -0
  102. package/examples/v2_no_auth/controllers/productController.js +31 -0
  103. package/examples/v2_no_auth/controllers/publicController.js +16 -0
  104. package/examples/v2_no_auth/package.json +22 -0
  105. package/examples/v2_no_auth/routes.json +37 -0
  106. package/examples/v2_oauth/README.md +70 -0
  107. package/examples/v2_oauth/app.js +90 -0
  108. package/examples/v2_oauth/controllers/mainController.js +45 -0
  109. package/examples/v2_oauth/controllers/oauthController.js +247 -0
  110. package/examples/v2_oauth/controllers/protectedController.js +13 -0
  111. package/examples/v2_oauth/controllers/userController.js +17 -0
  112. package/examples/v2_oauth/package.json +26 -0
  113. package/examples/v2_oauth/routes.json +44 -0
  114. package/examples/v2_openapi/README.md +77 -0
  115. package/examples/v2_openapi/app.js +222 -0
  116. package/examples/v2_openapi/controllers/authController.js +52 -0
  117. package/examples/v2_openapi/controllers/mainController.js +26 -0
  118. package/examples/v2_openapi/controllers/productController.js +17 -0
  119. package/examples/v2_openapi/controllers/userController.js +27 -0
  120. package/examples/v2_openapi/package.json +26 -0
  121. package/examples/v2_openapi/routes.json +37 -0
  122. package/generate_token.js +10 -0
  123. package/index.js +85 -0
  124. package/jerk.jpg +0 -0
  125. package/lib/core/handler.js +86 -0
  126. package/lib/core/hooks.js +224 -0
  127. package/lib/core/router.js +204 -0
  128. package/lib/core/securityEnhancedServer.js +752 -0
  129. package/lib/core/server.js +369 -0
  130. package/lib/loader/controllerLoader.js +175 -0
  131. package/lib/loader/routeLoader.js +341 -0
  132. package/lib/middleware/auditLogger.js +208 -0
  133. package/lib/middleware/authenticator.js +565 -0
  134. package/lib/middleware/compressor.js +218 -0
  135. package/lib/middleware/cors.js +135 -0
  136. package/lib/middleware/firewall.js +443 -0
  137. package/lib/middleware/rateLimiter.js +210 -0
  138. package/lib/middleware/session.js +301 -0
  139. package/lib/middleware/validator.js +193 -0
  140. package/lib/mvc/controllerBase.js +207 -0
  141. package/lib/mvc/viewEngine.js +752 -0
  142. package/lib/utils/configParser.js +223 -0
  143. package/lib/utils/logger.js +145 -0
  144. package/lib/utils/mariadbTokenAdapter.js +226 -0
  145. package/lib/utils/openapiGenerator.js +140 -0
  146. package/lib/utils/sqliteTokenAdapter.js +224 -0
  147. package/lib/utils/tokenManager.js +254 -0
  148. package/package.json +47 -0
  149. package/v2examplle/v2_json_auth/README.md +83 -0
  150. package/v2examplle/v2_json_auth/app.js +72 -0
  151. package/v2examplle/v2_json_auth/controllers/authController.js +67 -0
  152. package/v2examplle/v2_json_auth/controllers/mainController.js +16 -0
  153. package/v2examplle/v2_json_auth/controllers/protectedController.js +12 -0
  154. package/v2examplle/v2_json_auth/controllers/tokenController.js +28 -0
  155. package/v2examplle/v2_json_auth/controllers/userController.js +15 -0
  156. package/v2examplle/v2_json_auth/package.json +26 -0
  157. package/v2examplle/v2_json_auth/routes.json +37 -0
  158. package/v2examplle/v2_json_auth/tokens.json +20 -0
  159. package/v2examplle/v2_mariadb_auth/README.md +94 -0
  160. package/v2examplle/v2_mariadb_auth/app.js +81 -0
  161. package/v2examplle/v2_mariadb_auth/controllers/authController.js +95 -0
  162. package/v2examplle/v2_mariadb_auth/controllers/mainController.js +31 -0
  163. package/v2examplle/v2_mariadb_auth/controllers/protectedController.js +12 -0
  164. package/v2examplle/v2_mariadb_auth/controllers/userController.js +17 -0
  165. package/v2examplle/v2_mariadb_auth/package.json +27 -0
  166. package/v2examplle/v2_mariadb_auth/routes.json +37 -0
  167. package/v2examplle/v2_sqlite_auth/README.md +72 -0
  168. package/v2examplle/v2_sqlite_auth/app.js +74 -0
  169. package/v2examplle/v2_sqlite_auth/app_fixed.js +74 -0
  170. package/v2examplle/v2_sqlite_auth/controllers/authController.js +64 -0
  171. package/v2examplle/v2_sqlite_auth/controllers/mainController.js +24 -0
  172. package/v2examplle/v2_sqlite_auth/controllers/protectedController.js +12 -0
  173. package/v2examplle/v2_sqlite_auth/controllers/userController.js +16 -0
  174. package/v2examplle/v2_sqlite_auth/package.json +27 -0
  175. package/v2examplle/v2_sqlite_auth/routes.json +30 -0
  176. package/v2examplle/v2_sqlite_auth/test_api.sh +47 -0
  177. 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, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&#x27;');
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;