dars-framework 1.0.0__py3-none-any.whl

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 (68) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +52 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/hot_reload.py +33 -0
  5. dars/cli/main.py +637 -0
  6. dars/cli/preview.py +419 -0
  7. dars/cli/translations.py +389 -0
  8. dars/components/__init__.py +0 -0
  9. dars/components/advanced/__init__.py +8 -0
  10. dars/components/advanced/accordion.py +21 -0
  11. dars/components/advanced/card.py +28 -0
  12. dars/components/advanced/modal.py +40 -0
  13. dars/components/advanced/navbar.py +31 -0
  14. dars/components/advanced/table.py +24 -0
  15. dars/components/advanced/tabs.py +26 -0
  16. dars/components/basic/__init__.py +34 -0
  17. dars/components/basic/button.py +29 -0
  18. dars/components/basic/checkbox.py +34 -0
  19. dars/components/basic/container.py +23 -0
  20. dars/components/basic/datepicker.py +139 -0
  21. dars/components/basic/image.py +36 -0
  22. dars/components/basic/input.py +50 -0
  23. dars/components/basic/link.py +31 -0
  24. dars/components/basic/page.py +20 -0
  25. dars/components/basic/progressbar.py +17 -0
  26. dars/components/basic/radiobutton.py +34 -0
  27. dars/components/basic/select.py +81 -0
  28. dars/components/basic/slider.py +63 -0
  29. dars/components/basic/spinner.py +11 -0
  30. dars/components/basic/text.py +22 -0
  31. dars/components/basic/textarea.py +46 -0
  32. dars/components/basic/tooltip.py +18 -0
  33. dars/components/layout/__init__.py +0 -0
  34. dars/components/layout/anchor.py +13 -0
  35. dars/components/layout/flex.py +26 -0
  36. dars/components/layout/grid.py +45 -0
  37. dars/core/__init__.py +0 -0
  38. dars/core/app.py +630 -0
  39. dars/core/component.py +25 -0
  40. dars/core/events.py +101 -0
  41. dars/core/properties.py +127 -0
  42. dars/docs/__init__.py +0 -0
  43. dars/exporters/__init__.py +0 -0
  44. dars/exporters/base.py +69 -0
  45. dars/exporters/web/__init__.py +0 -0
  46. dars/exporters/web/html_css_js.py +1406 -0
  47. dars/scripts/__init__.py +0 -0
  48. dars/scripts/script.py +38 -0
  49. dars/templates/__init__.py +0 -0
  50. dars/templates/examples/advanced/all_components_demo.py +87 -0
  51. dars/templates/examples/advanced/dashboard.py +440 -0
  52. dars/templates/examples/advanced/modern_web_app.py +452 -0
  53. dars/templates/examples/basic/flex_layout_responsive.py +13 -0
  54. dars/templates/examples/basic/form_components.py +516 -0
  55. dars/templates/examples/basic/grid_layout_responsive.py +13 -0
  56. dars/templates/examples/basic/hello_world.py +104 -0
  57. dars/templates/examples/basic/layout_multipage_demo.py +23 -0
  58. dars/templates/examples/basic/multipage_example.py +70 -0
  59. dars/templates/examples/basic/pwa_custom_icons.py +31 -0
  60. dars/templates/examples/basic/simple_form.py +377 -0
  61. dars/templates/examples/demo/complete_app.py +720 -0
  62. dars/templates/html/__init__.py +0 -0
  63. dars_framework-1.0.0.dist-info/METADATA +146 -0
  64. dars_framework-1.0.0.dist-info/RECORD +68 -0
  65. dars_framework-1.0.0.dist-info/WHEEL +5 -0
  66. dars_framework-1.0.0.dist-info/entry_points.txt +2 -0
  67. dars_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
  68. dars_framework-1.0.0.dist-info/top_level.txt +1 -0
dars/core/app.py ADDED
@@ -0,0 +1,630 @@
1
+ from typing import Optional, List, Dict, Any
2
+ from .component import Component
3
+ from .events import EventManager
4
+
5
+ class Page:
6
+ """Representa una página individual en la app Dars (multipágina)."""
7
+ def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
8
+ self.name = name # slug o nombre de la página
9
+ self.root = root # componente raíz de la página
10
+ self.title = title
11
+ self.meta = meta or {}
12
+ self.index = index # ¿Es la página principal?
13
+
14
+ class App:
15
+ """Clase principal que representa una aplicación Dars"""
16
+
17
+ def rTimeCompile(self, exporter=None, port=None):
18
+ """
19
+ Genera una preview rápida de la app en un servidor local usando un exportador
20
+ (por defecto HTMLCSSJSExporter) y sirviendo los archivos en un directorio temporal.
21
+ No abre el navegador automáticamente. El servidor se detiene con Ctrl+C.
22
+ Puedes pasar el puerto como argumento de línea de comandos: python main.py --port 8080
23
+ """
24
+ import threading
25
+ import time
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ # Rich para mensajes bonitos
30
+ try:
31
+ from rich.console import Console
32
+ from rich.panel import Panel
33
+ from rich.text import Text
34
+ except ImportError:
35
+ Console = None
36
+ console = Console() if 'Console' in locals() else None
37
+
38
+ # Leer puerto de sys.argv si no se pasa explícito
39
+ if port is None:
40
+ port = 8000
41
+ for i, arg in enumerate(sys.argv):
42
+ if arg in ('--port', '-p') and i + 1 < len(sys.argv):
43
+ try:
44
+ port = int(sys.argv[i + 1])
45
+ except Exception:
46
+ pass
47
+
48
+ # Importar exportador por defecto si no se pasa
49
+ if exporter is None:
50
+ try:
51
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
52
+ except ImportError:
53
+ print("Could not import HTMLCSSJSExporter")
54
+ return
55
+ exporter = HTMLCSSJSExporter()
56
+
57
+ # Importar PreviewServer
58
+ try:
59
+ from dars.cli.preview import PreviewServer
60
+ except ImportError:
61
+ print("Could not import PreviewServer")
62
+ return
63
+
64
+ import shutil
65
+ import traceback
66
+
67
+ try:
68
+ import os
69
+ preview_dir = os.path.abspath("./dars_preview")
70
+ cwd_original = os.getcwd()
71
+ if os.path.exists(preview_dir):
72
+ import shutil
73
+ try:
74
+ shutil.rmtree(preview_dir)
75
+ except Exception as e:
76
+ if console:
77
+ console.print(f"[yellow]Warning: Could not clean previous preview directory: {e}[/yellow]")
78
+ else:
79
+ print(f"Warning: Could not clean previous preview directory: {e}")
80
+ os.makedirs(preview_dir, exist_ok=True)
81
+ exporter.export(self, preview_dir)
82
+ url = f"http://localhost:{port}"
83
+ app_title = getattr(self, 'title', 'Dars App')
84
+ if console:
85
+ from rich.text import Text
86
+ from rich.panel import Panel
87
+ panel = Panel(
88
+ Text(f"✔ App running successfully\n\nName: {app_title}\nPreview available at: {url}\n\nPress Ctrl+C to stop the server.",
89
+ style="bold green", justify="center"),
90
+ title="Dars Preview", border_style="cyan")
91
+ console.print(panel)
92
+ else:
93
+ print(f"[Dars] App '{app_title}' running. Preview at {url}")
94
+ server = PreviewServer(preview_dir, port)
95
+ try:
96
+ if not server.start():
97
+ if console:
98
+ console.print("[red] Could not start preview server. [/red]")
99
+ else:
100
+ print("Could not start preview server.")
101
+ return
102
+ try:
103
+ # --- HOT RELOAD ---
104
+ import inspect
105
+ import importlib.util
106
+ from dars.cli.hot_reload import FileWatcher
107
+
108
+ app_file = None
109
+ # Detectar archivo fuente de la app (donde está definida la clase App)
110
+ for frame in inspect.stack():
111
+ if frame.function == "<module>":
112
+ app_file = frame.filename
113
+ break
114
+ if not app_file:
115
+ app_file = sys.argv[0]
116
+
117
+ def reload_and_export():
118
+ # Limpiar y recompilar app
119
+ if console:
120
+ console.print("[yellow]Detected app file change. Reloading...[/yellow]")
121
+ else:
122
+ print("[Dars] Detected app file change. Reloading...")
123
+ try:
124
+ # Recargar módulo de la app
125
+ spec = importlib.util.spec_from_file_location("dars_app", app_file)
126
+ module = importlib.util.module_from_spec(spec)
127
+ spec.loader.exec_module(module)
128
+ # Buscar instancia App
129
+ new_app = None
130
+ for v in vars(module).values():
131
+ if isinstance(v, App):
132
+ new_app = v
133
+ break
134
+ if not new_app:
135
+ if console:
136
+ console.print("[red]No App instance found after reload.[/red]")
137
+ else:
138
+ print("[Dars] No App instance found after reload.")
139
+ return
140
+ # Exportar de nuevo
141
+ exporter.export(new_app, preview_dir)
142
+ if console:
143
+ console.print("[green]App reloaded and re-exported successfully.[/green]")
144
+ else:
145
+ print("[Dars] App reloaded and re-exported successfully.")
146
+ except Exception as e:
147
+ if console:
148
+ console.print(f"[red]Hot reload failed: {e}[/red]")
149
+ else:
150
+ print(f"[Dars] Hot reload failed: {e}")
151
+
152
+ watcher = FileWatcher(app_file, reload_and_export)
153
+ watcher.start()
154
+
155
+ while True:
156
+ time.sleep(1)
157
+ except KeyboardInterrupt:
158
+ watcher.stop()
159
+ if console:
160
+ console.print("\n[cyan] Stopping preview and watcher... [/cyan]")
161
+ else:
162
+ print("\n[Dars] Stopping preview and watcher...")
163
+ finally:
164
+ server.stop()
165
+ if console:
166
+ console.print("[green] Preview stopped. [/green]")
167
+ else:
168
+ print("[Dars] Preview stopped.")
169
+ finally:
170
+ os.chdir(cwd_original)
171
+ try:
172
+ shutil.rmtree(preview_dir)
173
+ if console:
174
+ console.print("[yellow]Preview files deleted.[/yellow]")
175
+ else:
176
+ print("Preview files deleted.")
177
+ except Exception as e:
178
+ if console:
179
+ console.print(f"[red]Could not delete preview directory: {e}[/red]")
180
+ else:
181
+ print(f"Could not delete preview directory: {e}")
182
+
183
+ except PermissionError as e:
184
+ # Windows: temp dir cleanup error
185
+ msg = f"[yellow] Warning: Could not clean temp directory due to permissions: {e} [/yellow]"
186
+ if 'console' in locals() and console:
187
+ console.print(msg)
188
+ else:
189
+ print(msg)
190
+ except Exception as e:
191
+ msg = f"[red] Unexpected error in fast preview: {e}\n{traceback.format_exc()} [/red]"
192
+ if 'console' in locals() and console:
193
+ console.print(msg)
194
+ else:
195
+ print(msg)
196
+
197
+ """
198
+ Genera una preview rápida de la app en un servidor local usando un exportador
199
+ (por defecto HTMLCSSJSExporter) y sirviendo los archivos en un directorio temporal.
200
+ No abre el navegador automáticamente. El servidor se detiene con Ctrl+C.
201
+ Puedes pasar el puerto como argumento de línea de comandos: python main.py --port 8080
202
+ """
203
+ import tempfile
204
+ import threading
205
+ import time
206
+ import sys
207
+ from pathlib import Path
208
+
209
+ # Rich para mensajes bonitos
210
+ try:
211
+ from rich.console import Console
212
+ from rich.panel import Panel
213
+ from rich.text import Text
214
+ except ImportError:
215
+ Console = None
216
+ console = Console() if 'Console' in locals() else None
217
+
218
+ # Leer puerto de sys.argv si no se pasa explícito
219
+ if port is None:
220
+ port = 8000
221
+ for i, arg in enumerate(sys.argv):
222
+ if arg in ('--port', '-p') and i + 1 < len(sys.argv):
223
+ try:
224
+ port = int(sys.argv[i + 1])
225
+ except Exception:
226
+ pass
227
+
228
+ # Importar exportador por defecto si no se pasa
229
+ if exporter is None:
230
+ try:
231
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
232
+ except ImportError:
233
+ print("Could not import HTMLCSSJSExporter")
234
+ return
235
+ exporter = HTMLCSSJSExporter()
236
+
237
+ # Importar PreviewServer
238
+ try:
239
+ from dars.cli.preview import PreviewServer
240
+ except ImportError:
241
+ print("Could not import PreviewServer")
242
+ return
243
+
244
+
245
+ def __init__(
246
+ self,
247
+ title: str = "Dars App",
248
+ description: str = "",
249
+ author: str = "",
250
+ keywords: List[str] = None,
251
+ language: str = "en",
252
+ favicon: str = "",
253
+ icon: str = "",
254
+ apple_touch_icon: str = "",
255
+ manifest: str = "",
256
+ theme_color: str = "#000000",
257
+ background_color: str = "#ffffff",
258
+ **config
259
+ ):
260
+ # Propiedades básicas de la aplicación
261
+ self.title = title
262
+ self.description = description
263
+ self.author = author
264
+ self.keywords = keywords or []
265
+ self.language = language
266
+
267
+ # Iconos y favicon
268
+ self.favicon = favicon
269
+ self.icon = icon # Para PWA y meta tags
270
+ self.apple_touch_icon = apple_touch_icon
271
+ self.manifest = manifest # Para PWA manifest.json
272
+
273
+ # Colores para PWA y tema
274
+ self.theme_color = theme_color
275
+ self.background_color = background_color
276
+
277
+ # Propiedades Open Graph (para redes sociales)
278
+
279
+ #
280
+ # [RECOMENDACIÓN DARS]
281
+ # Para lanzar la compilación/preview rápido de tu app, añade al final de tu archivo principal:
282
+ # if __name__ == "__main__":
283
+ # app.rTimeCompile() # o app.timeCompile()
284
+ # Así tendrás preview instantáneo y control explícito, sin efectos colaterales.
285
+ #
286
+ self.og_title = config.get('og_title', title)
287
+ self.og_description = config.get('og_description', description)
288
+ self.og_image = config.get('og_image', '')
289
+ self.og_url = config.get('og_url', '')
290
+ self.og_type = config.get('og_type', 'website')
291
+ self.og_site_name = config.get('og_site_name', '')
292
+
293
+ # Twitter Cards
294
+ self.twitter_card = config.get('twitter_card', 'summary')
295
+ self.twitter_site = config.get('twitter_site', '')
296
+ self.twitter_creator = config.get('twitter_creator', '')
297
+
298
+ # SEO y robots
299
+ self.robots = config.get('robots', 'index, follow')
300
+ self.canonical_url = config.get('canonical_url', '')
301
+
302
+ # PWA configuración
303
+ self.pwa_enabled = config.get('pwa_enabled', False)
304
+ self.pwa_name = config.get('pwa_name', title)
305
+ self.pwa_short_name = config.get('pwa_short_name', title[:12])
306
+ self.pwa_display = config.get('pwa_display', 'standalone')
307
+ self.pwa_orientation = config.get('pwa_orientation', 'portrait')
308
+
309
+ # Propiedades del framework
310
+ self.root: Optional[Component] = None # Single-page mode
311
+ self._pages: Dict[str, Page] = {} # Multipage mode
312
+ self._index_page: str = None # Nombre de la página principal (si existe)
313
+ self.scripts: List['Script'] = []
314
+ self.global_styles: Dict[str, Any] = {}
315
+ self.event_manager = EventManager()
316
+ self.config = config
317
+
318
+ # Configuración por defecto
319
+ self.config.setdefault('viewport', {
320
+ 'width': 'device-width',
321
+ 'initial_scale': 1.0,
322
+ 'user_scalable': 'yes'
323
+ })
324
+ self.config.setdefault('theme', 'light')
325
+ self.config.setdefault('responsive', True)
326
+ self.config.setdefault('charset', 'UTF-8')
327
+
328
+ def set_root(self, component: Component):
329
+ """Establece el componente raíz de la aplicación (modo single-page retrocompatible)"""
330
+ self.root = component
331
+
332
+ def add_page(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
333
+ """
334
+ Agrega una página multipágina a la app.
335
+ name es el slug/clave, root el componente raíz.
336
+ Si index=True, esta página será la principal (exportada como index.html).
337
+ Si varias páginas tienen index=True, la última registrada será la principal.
338
+ """
339
+ if name in self._pages:
340
+ raise ValueError(f"Ya existe una página con el nombre '{name}'")
341
+ self._pages[name] = Page(name, root, title, meta, index=index)
342
+ if index:
343
+ self._index_page = name
344
+
345
+
346
+ def get_page(self, name: str) -> 'Page':
347
+ """Obtiene una página registrada por su nombre."""
348
+ return self._pages.get(name)
349
+
350
+ def get_index_page(self) -> 'Page':
351
+ """
352
+ Devuelve la página marcada como index, o la primera registrada si ninguna tiene index=True.
353
+ """
354
+ # Prioridad: explícita, luego la primera
355
+ if hasattr(self, '_index_page') and self._index_page and self._index_page in self._pages:
356
+ return self._pages[self._index_page]
357
+ for page in self._pages.values():
358
+ if getattr(page, 'index', False):
359
+ return page
360
+ # Si ninguna marcada, devolver la primera
361
+ if self._pages:
362
+ return list(self._pages.values())[0]
363
+ return None
364
+
365
+
366
+ @property
367
+ def pages(self) -> Dict[str, 'Page']:
368
+ """Devuelve el diccionario de páginas registradas (multipágina)."""
369
+ return self._pages
370
+
371
+ def is_multipage(self) -> bool:
372
+ """Indica si la app está en modo multipágina (True si hay páginas registradas)."""
373
+ return bool(self._pages)
374
+
375
+ def add_script(self, script: 'Script'):
376
+ """Agrega un script a la aplicación"""
377
+ self.scripts.append(script)
378
+
379
+ def add_global_style(self, selector: str, styles: Dict[str, Any]):
380
+ """Agrega estilos globales a la aplicación"""
381
+ self.global_styles[selector] = styles
382
+
383
+ def set_theme(self, theme: str):
384
+ """Establece el tema de la aplicación"""
385
+ self.config['theme'] = theme
386
+
387
+ def set_favicon(self, favicon_path: str):
388
+ """Establece el favicon de la aplicación"""
389
+ self.favicon = favicon_path
390
+
391
+ def set_icon(self, icon_path: str):
392
+ """Establece el icono principal de la aplicación"""
393
+ self.icon = icon_path
394
+
395
+ def set_apple_touch_icon(self, icon_path: str):
396
+ """Establece el icono para dispositivos Apple"""
397
+ self.apple_touch_icon = icon_path
398
+
399
+ def set_manifest(self, manifest_path: str):
400
+ """Establece el archivo manifest para PWA"""
401
+ self.manifest = manifest_path
402
+
403
+ def add_keyword(self, keyword: str):
404
+ """Añade una palabra clave para SEO"""
405
+ if keyword not in self.keywords:
406
+ self.keywords.append(keyword)
407
+
408
+ def add_keywords(self, keywords: List[str]):
409
+ """Añade múltiples palabras clave para SEO"""
410
+ for keyword in keywords:
411
+ self.add_keyword(keyword)
412
+
413
+ def set_open_graph(self, **og_data):
414
+ """Configura propiedades Open Graph para redes sociales"""
415
+ if 'title' in og_data:
416
+ self.og_title = og_data['title']
417
+ if 'description' in og_data:
418
+ self.og_description = og_data['description']
419
+ if 'image' in og_data:
420
+ self.og_image = og_data['image']
421
+ if 'url' in og_data:
422
+ self.og_url = og_data['url']
423
+ if 'type' in og_data:
424
+ self.og_type = og_data['type']
425
+ if 'site_name' in og_data:
426
+ self.og_site_name = og_data['site_name']
427
+
428
+ def set_twitter_card(self, card_type: str = 'summary', site: str = '', creator: str = ''):
429
+ """Configura Twitter Card meta tags"""
430
+ self.twitter_card = card_type
431
+ if site:
432
+ self.twitter_site = site
433
+ if creator:
434
+ self.twitter_creator = creator
435
+
436
+ def enable_pwa(self, name: str = None, short_name: str = None, display: str = 'standalone'):
437
+ """Habilita configuración PWA (Progressive Web App)"""
438
+ self.pwa_enabled = True
439
+ if name:
440
+ self.pwa_name = name
441
+ if short_name:
442
+ self.pwa_short_name = short_name
443
+ self.pwa_display = display
444
+
445
+ def set_theme_colors(self, theme_color: str, background_color: str = None):
446
+ """Establece colores del tema para PWA y navegadores"""
447
+ self.theme_color = theme_color
448
+ if background_color:
449
+ self.background_color = background_color
450
+
451
+ def get_meta_tags(self) -> Dict[str, str]:
452
+ """Obtiene todos los meta tags configurados como diccionario"""
453
+ meta_tags = {}
454
+
455
+ # Meta tags básicos
456
+ if self.description:
457
+ meta_tags['description'] = self.description
458
+ if self.author:
459
+ meta_tags['author'] = self.author
460
+ if self.keywords:
461
+ meta_tags['keywords'] = ', '.join(self.keywords)
462
+ if self.robots:
463
+ meta_tags['robots'] = self.robots
464
+
465
+ # Viewport
466
+ viewport_parts = []
467
+ for key, value in self.config['viewport'].items():
468
+ if key == 'initial_scale':
469
+ viewport_parts.append(f'initial-scale={value}')
470
+ elif key == 'user_scalable':
471
+ viewport_parts.append(f'user-scalable={value}')
472
+ else:
473
+ viewport_parts.append(f'{key.replace("_", "-")}={value}')
474
+ meta_tags['viewport'] = ', '.join(viewport_parts)
475
+
476
+ # PWA y tema
477
+ meta_tags['theme-color'] = self.theme_color
478
+ if self.pwa_enabled:
479
+ meta_tags['mobile-web-app-capable'] = 'yes'
480
+ meta_tags['apple-mobile-web-app-capable'] = 'yes'
481
+ meta_tags['apple-mobile-web-app-status-bar-style'] = 'default'
482
+ meta_tags['apple-mobile-web-app-title'] = self.pwa_short_name
483
+
484
+ return meta_tags
485
+
486
+ def get_open_graph_tags(self) -> Dict[str, str]:
487
+ """Obtiene todos los tags Open Graph configurados"""
488
+ og_tags = {}
489
+
490
+ if self.og_title:
491
+ og_tags['og:title'] = self.og_title
492
+ if self.og_description:
493
+ og_tags['og:description'] = self.og_description
494
+ if self.og_image:
495
+ og_tags['og:image'] = self.og_image
496
+ if self.og_url:
497
+ og_tags['og:url'] = self.og_url
498
+ if self.og_type:
499
+ og_tags['og:type'] = self.og_type
500
+ if self.og_site_name:
501
+ og_tags['og:site_name'] = self.og_site_name
502
+
503
+ return og_tags
504
+
505
+ def get_twitter_tags(self) -> Dict[str, str]:
506
+ """Obtiene todos los tags de Twitter Card configurados"""
507
+ twitter_tags = {}
508
+
509
+ if self.twitter_card:
510
+ twitter_tags['twitter:card'] = self.twitter_card
511
+ if self.twitter_site:
512
+ twitter_tags['twitter:site'] = self.twitter_site
513
+ if self.twitter_creator:
514
+ twitter_tags['twitter:creator'] = self.twitter_creator
515
+
516
+ return twitter_tags
517
+
518
+ def export(self, exporter: 'Exporter', output_path: str) -> bool:
519
+ """Exporta la aplicación usando el exportador especificado"""
520
+ if not self.root:
521
+ raise ValueError("No se ha establecido un componente raíz")
522
+
523
+ return exporter.export(self, output_path)
524
+
525
+ def validate(self) -> List[str]:
526
+ """Valida la aplicación y retorna una lista de errores"""
527
+ errors = []
528
+
529
+ if not self.root:
530
+ errors.append("No se ha establecido un componente raíz")
531
+
532
+ if not self.title:
533
+ errors.append("El título de la aplicación no puede estar vacío")
534
+
535
+ # Validar componentes recursivamente
536
+ if self.root:
537
+ errors.extend(self._validate_component(self.root))
538
+
539
+ return errors
540
+
541
+ def _validate_component(self, component: Component, path: str = "root") -> List[str]:
542
+ """Valida un componente y sus hijos recursivamente"""
543
+ errors = []
544
+
545
+ # Validar que el componente tenga un método render
546
+ if not hasattr(component, 'render'):
547
+ errors.append(f"El componente en {path} no tiene método render")
548
+
549
+ # Validar hijos
550
+ for i, child in enumerate(component.children):
551
+ child_path = f"{path}.children[{i}]"
552
+ errors.extend(self._validate_component(child, child_path))
553
+
554
+ return errors
555
+
556
+ def get_component_tree(self) -> Dict[str, Any]:
557
+ """Retorna la estructura del árbol de componentes"""
558
+ if not self.root:
559
+ return {}
560
+
561
+ return self._component_to_dict(self.root)
562
+
563
+ def _component_to_dict(self, component: Component) -> Dict[str, Any]:
564
+ """Convierte un componente a diccionario para inspección"""
565
+ return {
566
+ 'type': component.__class__.__name__,
567
+ 'id': component.id,
568
+ 'class_name': component.class_name,
569
+ 'props': component.props,
570
+ 'style': component.style,
571
+ 'children': [self._component_to_dict(child) for child in component.children]
572
+ }
573
+
574
+ def find_component_by_id(self, component_id: str) -> Optional[Component]:
575
+ """Busca un componente por su ID"""
576
+ if not self.root:
577
+ return None
578
+
579
+ return self._find_component_recursive(self.root, component_id)
580
+
581
+ def _find_component_recursive(self, component: Component, target_id: str) -> Optional[Component]:
582
+ """Busca un componente recursivamente por ID"""
583
+ if component.id == target_id:
584
+ return component
585
+
586
+ for child in component.children:
587
+ result = self._find_component_recursive(child, target_id)
588
+ if result:
589
+ return result
590
+
591
+ return None
592
+
593
+ def get_stats(self) -> Dict[str, Any]:
594
+ """Retorna estadísticas de la aplicación"""
595
+ if not self.root:
596
+ return {
597
+ 'total_components': 0,
598
+ 'max_depth': 0,
599
+ 'scripts_count': len(self.scripts),
600
+ 'global_styles_count': len(self.global_styles)
601
+ }
602
+
603
+ stats = {
604
+ 'total_components': self._count_components(self.root),
605
+ 'max_depth': self._calculate_max_depth(self.root),
606
+ 'scripts_count': len(self.scripts),
607
+ 'global_styles_count': len(self.global_styles)
608
+ }
609
+
610
+ return stats
611
+
612
+ def _count_components(self, component: Component) -> int:
613
+ """Cuenta el número total de componentes"""
614
+ count = 1
615
+ for child in component.children:
616
+ count += self._count_components(child)
617
+ return count
618
+
619
+ def _calculate_max_depth(self, component: Component, current_depth: int = 0) -> int:
620
+ """Calcula la profundidad máxima del árbol de componentes"""
621
+ if not component.children:
622
+ return current_depth
623
+
624
+ max_child_depth = 0
625
+ for child in component.children:
626
+ child_depth = self._calculate_max_depth(child, current_depth + 1)
627
+ max_child_depth = max(max_child_depth, child_depth)
628
+
629
+ return max_child_depth
630
+
dars/core/component.py ADDED
@@ -0,0 +1,25 @@
1
+ from typing import Dict, Any, List, Optional, Callable
2
+ from abc import ABC, abstractmethod
3
+
4
+ class Component(ABC):
5
+ def __init__(self, **props):
6
+ self.props = props
7
+ self.children: List[Component] = []
8
+ self.parent: Optional[Component] = None
9
+ self.id: Optional[str] = props.get('id')
10
+ self.class_name: Optional[str] = props.get('class_name')
11
+ self.style: Dict[str, Any] = props.get('style', {})
12
+ self.events: Dict[str, Callable] = {}
13
+
14
+ def add_child(self, child: 'Component'):
15
+ child.parent = self
16
+ self.children.append(child)
17
+
18
+ def set_event(self, event_name: str, handler: Callable):
19
+ self.events[event_name] = handler
20
+
21
+ @abstractmethod
22
+ def render(self, exporter: 'Exporter') -> str:
23
+ pass
24
+
25
+