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.
- dars/__init__.py +0 -0
- dars/all.py +52 -0
- dars/cli/__init__.py +0 -0
- dars/cli/hot_reload.py +33 -0
- dars/cli/main.py +637 -0
- dars/cli/preview.py +419 -0
- dars/cli/translations.py +389 -0
- dars/components/__init__.py +0 -0
- dars/components/advanced/__init__.py +8 -0
- dars/components/advanced/accordion.py +21 -0
- dars/components/advanced/card.py +28 -0
- dars/components/advanced/modal.py +40 -0
- dars/components/advanced/navbar.py +31 -0
- dars/components/advanced/table.py +24 -0
- dars/components/advanced/tabs.py +26 -0
- dars/components/basic/__init__.py +34 -0
- dars/components/basic/button.py +29 -0
- dars/components/basic/checkbox.py +34 -0
- dars/components/basic/container.py +23 -0
- dars/components/basic/datepicker.py +139 -0
- dars/components/basic/image.py +36 -0
- dars/components/basic/input.py +50 -0
- dars/components/basic/link.py +31 -0
- dars/components/basic/page.py +20 -0
- dars/components/basic/progressbar.py +17 -0
- dars/components/basic/radiobutton.py +34 -0
- dars/components/basic/select.py +81 -0
- dars/components/basic/slider.py +63 -0
- dars/components/basic/spinner.py +11 -0
- dars/components/basic/text.py +22 -0
- dars/components/basic/textarea.py +46 -0
- dars/components/basic/tooltip.py +18 -0
- dars/components/layout/__init__.py +0 -0
- dars/components/layout/anchor.py +13 -0
- dars/components/layout/flex.py +26 -0
- dars/components/layout/grid.py +45 -0
- dars/core/__init__.py +0 -0
- dars/core/app.py +630 -0
- dars/core/component.py +25 -0
- dars/core/events.py +101 -0
- dars/core/properties.py +127 -0
- dars/docs/__init__.py +0 -0
- dars/exporters/__init__.py +0 -0
- dars/exporters/base.py +69 -0
- dars/exporters/web/__init__.py +0 -0
- dars/exporters/web/html_css_js.py +1406 -0
- dars/scripts/__init__.py +0 -0
- dars/scripts/script.py +38 -0
- dars/templates/__init__.py +0 -0
- dars/templates/examples/advanced/all_components_demo.py +87 -0
- dars/templates/examples/advanced/dashboard.py +440 -0
- dars/templates/examples/advanced/modern_web_app.py +452 -0
- dars/templates/examples/basic/flex_layout_responsive.py +13 -0
- dars/templates/examples/basic/form_components.py +516 -0
- dars/templates/examples/basic/grid_layout_responsive.py +13 -0
- dars/templates/examples/basic/hello_world.py +104 -0
- dars/templates/examples/basic/layout_multipage_demo.py +23 -0
- dars/templates/examples/basic/multipage_example.py +70 -0
- dars/templates/examples/basic/pwa_custom_icons.py +31 -0
- dars/templates/examples/basic/simple_form.py +377 -0
- dars/templates/examples/demo/complete_app.py +720 -0
- dars/templates/html/__init__.py +0 -0
- dars_framework-1.0.0.dist-info/METADATA +146 -0
- dars_framework-1.0.0.dist-info/RECORD +68 -0
- dars_framework-1.0.0.dist-info/WHEEL +5 -0
- dars_framework-1.0.0.dist-info/entry_points.txt +2 -0
- dars_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|