dars-framework 1.2.3__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 +69 -0
- dars/cli/__init__.py +0 -0
- dars/cli/doctor/__init__.py +1 -0
- dars/cli/doctor/detect.py +154 -0
- dars/cli/doctor/doctor.py +176 -0
- dars/cli/doctor/installers.py +100 -0
- dars/cli/doctor/persist.py +62 -0
- dars/cli/doctor/preflight.py +33 -0
- dars/cli/doctor/ui.py +54 -0
- dars/cli/hot_reload.py +33 -0
- dars/cli/main.py +1107 -0
- dars/cli/preview.py +448 -0
- dars/cli/translations.py +531 -0
- dars/components/__init__.py +0 -0
- dars/components/advanced/__init__.py +8 -0
- dars/components/advanced/accordion.py +26 -0
- dars/components/advanced/card.py +33 -0
- dars/components/advanced/modal.py +45 -0
- dars/components/advanced/navbar.py +44 -0
- dars/components/advanced/table.py +25 -0
- dars/components/advanced/tabs.py +31 -0
- dars/components/basic/__init__.py +34 -0
- dars/components/basic/button.py +55 -0
- dars/components/basic/checkbox.py +35 -0
- dars/components/basic/container.py +29 -0
- dars/components/basic/datepicker.py +139 -0
- dars/components/basic/image.py +36 -0
- dars/components/basic/input.py +57 -0
- dars/components/basic/link.py +31 -0
- dars/components/basic/markdown.py +86 -0
- dars/components/basic/page.py +20 -0
- dars/components/basic/progressbar.py +18 -0
- dars/components/basic/radiobutton.py +35 -0
- dars/components/basic/select.py +82 -0
- dars/components/basic/slider.py +63 -0
- dars/components/basic/spinner.py +12 -0
- dars/components/basic/text.py +23 -0
- dars/components/basic/textarea.py +46 -0
- dars/components/basic/tooltip.py +19 -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/config.py +134 -0
- dars/core/__init__.py +0 -0
- dars/core/app.py +957 -0
- dars/core/component.py +284 -0
- dars/core/events.py +102 -0
- dars/core/js_bridge.py +99 -0
- dars/core/properties.py +127 -0
- dars/core/state.py +309 -0
- dars/dars_tests/apps_test/health_check.py +56 -0
- dars/dars_tests/run_tests.py +275 -0
- dars/dars_tests/tests/test_advanced_components.py +69 -0
- dars/dars_tests/tests/test_basic_components.py +88 -0
- dars/dars_tests/tests/test_core_and_cli.py +17 -0
- dars/dars_tests/tests/test_layout_components.py +58 -0
- dars/dars_tests/tests/test_version_check.py +21 -0
- dars/docs/__init__.py +0 -0
- dars/docs/app.md +290 -0
- dars/docs/cli.md +80 -0
- dars/docs/components.md +1679 -0
- dars/docs/custom_components.md +30 -0
- dars/docs/events.md +45 -0
- dars/docs/exporters.md +162 -0
- dars/docs/getting_started.md +79 -0
- dars/docs/index.md +18 -0
- dars/docs/scripts.md +593 -0
- dars/docs/state_management.md +57 -0
- dars/exporters/__init__.py +0 -0
- dars/exporters/base.py +96 -0
- dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
- dars/exporters/web/OLD/html_css_js_old.py +1406 -0
- dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
- dars/exporters/web/__init__.py +0 -0
- dars/exporters/web/html_css_js.py +2675 -0
- dars/exporters/web/vdom.py +251 -0
- dars/js_lib.py +206 -0
- dars/scripts/__init__.py +0 -0
- dars/scripts/dscript.py +26 -0
- dars/scripts/script.py +39 -0
- dars/security.py +195 -0
- dars/templates/__init__.py +0 -0
- dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- dars/templates/examples/README.md +4 -0
- dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
- dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
- dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
- dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
- dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
- dars/templates/examples/advanced/__init__.py +0 -0
- dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
- dars/templates/examples/basic/Forms/form_components.py +516 -0
- dars/templates/examples/basic/Forms/simple_form.py +379 -0
- dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
- dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
- dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
- dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
- dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
- dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
- dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
- dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
- dars/templates/examples/basic/__init__.py +0 -0
- dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
- dars/templates/examples/demo/complete_app.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
- dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
- dars/templates/examples/markdown/__init__.py +0 -0
- dars/templates/html/__init__.py +0 -0
- dars/version.py +2 -0
- dars_framework-1.2.3.dist-info/METADATA +15 -0
- dars_framework-1.2.3.dist-info/RECORD +118 -0
- dars_framework-1.2.3.dist-info/WHEEL +5 -0
- dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
- dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
- dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/core/app.py
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any
|
|
2
|
+
|
|
3
|
+
from dars.exporters.base import Exporter
|
|
4
|
+
from dars.scripts.script import Script
|
|
5
|
+
from .component import Component
|
|
6
|
+
from .events import EventManager
|
|
7
|
+
|
|
8
|
+
class Page:
|
|
9
|
+
"""Represents an individual page in the Dars app (multipage)."""
|
|
10
|
+
def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False, scripts: Optional[List[Any]] = None):
|
|
11
|
+
self.name = name # slug o nombre de la página
|
|
12
|
+
self.root = root # componente raíz de la página
|
|
13
|
+
self.title = title
|
|
14
|
+
self.meta = meta or {}
|
|
15
|
+
self.index = index # ¿Es la página principal?
|
|
16
|
+
self.scripts: List[Any] = list(scripts) if scripts else []
|
|
17
|
+
|
|
18
|
+
def attr(self, **attrs):
|
|
19
|
+
"""Setter/getter for Page attributes, similar to Component.attr().
|
|
20
|
+
If kwargs are provided, sets attributes; otherwise, returns a dict with the editable attributes."""
|
|
21
|
+
|
|
22
|
+
if attrs:
|
|
23
|
+
for key, value in attrs.items():
|
|
24
|
+
if hasattr(self, key):
|
|
25
|
+
setattr(self, key, value)
|
|
26
|
+
else:
|
|
27
|
+
self.meta[key] = value
|
|
28
|
+
return self
|
|
29
|
+
# Getter
|
|
30
|
+
d = dict(self.meta)
|
|
31
|
+
d['name'] = self.name
|
|
32
|
+
d['root'] = self.root
|
|
33
|
+
d['title'] = self.title
|
|
34
|
+
d['index'] = self.index
|
|
35
|
+
d['scripts'] = list(self.scripts)
|
|
36
|
+
return d
|
|
37
|
+
# -----------------------------
|
|
38
|
+
# Métodos para manejar scripts
|
|
39
|
+
# -----------------------------
|
|
40
|
+
def add_script(self, script: Any):
|
|
41
|
+
"""Adds a script to this page.
|
|
42
|
+
- If 'script' is an instance (e.g., InlineScript/FileScript/DScript), it is added as is.
|
|
43
|
+
- If 'script' is a string, it is interpreted as an InlineScript (code).
|
|
44
|
+
- If 'script' is a dict, it is added as is (fallback).
|
|
45
|
+
Returns self to allow call chaining."""
|
|
46
|
+
|
|
47
|
+
# si es str => interpretarlo como inline
|
|
48
|
+
if isinstance(script, str):
|
|
49
|
+
created = self._make_inline_script(script)
|
|
50
|
+
self.scripts.append(created)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
# si es dict => fallback, guardarlo
|
|
54
|
+
if isinstance(script, dict):
|
|
55
|
+
self.scripts.append(script)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
# si ya es una instancia de "Script" (no podemos verificar tipo concreto sin dependencia),
|
|
59
|
+
# asumimos que es un script válido y lo añadimos.
|
|
60
|
+
self.scripts.append(script)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
# alias corto (pedido)
|
|
64
|
+
def addscript(self, script: Any):
|
|
65
|
+
return self.add_script(script)
|
|
66
|
+
|
|
67
|
+
def add_inline_script(self, code: str, **kwargs):
|
|
68
|
+
"""Convenience: adds an InlineScript to the page (code = JS or similar)."""
|
|
69
|
+
s = self._make_inline_script(code, **kwargs)
|
|
70
|
+
self.scripts.append(s)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def add_file_script(self, path: str, **kwargs):
|
|
74
|
+
"""Convenience: adds a FileScript (reference to a .js/.ts/etc. file)."""
|
|
75
|
+
s = self._make_file_script(path, **kwargs)
|
|
76
|
+
self.scripts.append(s)
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def add_dscript(self, obj: Any, **kwargs):
|
|
80
|
+
"""Convenience: attempts to create/add a DScript (if the class exists)."""
|
|
81
|
+
s = self._make_dscript(obj, **kwargs)
|
|
82
|
+
self.scripts.append(s)
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def get_scripts(self) -> List[Any]:
|
|
86
|
+
"""Returns the list of scripts added to the page."""
|
|
87
|
+
return list(self.scripts)
|
|
88
|
+
|
|
89
|
+
# -----------------------------
|
|
90
|
+
# Helpers para construcción segura
|
|
91
|
+
# -----------------------------
|
|
92
|
+
def _make_inline_script(self, code: str, **kwargs) -> Any:
|
|
93
|
+
"""Attempts to create an InlineScript instance if it exists in dars.scripts.*.
|
|
94
|
+
Otherwise, returns a fallback dict: {'type': 'inline', 'code': ..., **kwargs}"""
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# intentamos import común (ajusta según tu layout de módulos si hace falta)
|
|
98
|
+
from dars.scripts.script import InlineScript # type: ignore
|
|
99
|
+
return InlineScript(code, **kwargs)
|
|
100
|
+
except Exception:
|
|
101
|
+
try:
|
|
102
|
+
from dars.scripts.script import InlineScript # type: ignore
|
|
103
|
+
return InlineScript(code, **kwargs)
|
|
104
|
+
except Exception:
|
|
105
|
+
# fallback: dict simple que contiene lo mínimo
|
|
106
|
+
return {'type': 'inline', 'code': code, **kwargs}
|
|
107
|
+
|
|
108
|
+
def _make_file_script(self, path: str, **kwargs) -> Any:
|
|
109
|
+
"""Attempts to create a FileScript instance if it exists. Otherwise, returns a fallback dict."""
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
from dars.scripts.script import FileScript # type: ignore
|
|
113
|
+
return FileScript(path, **kwargs)
|
|
114
|
+
except Exception:
|
|
115
|
+
try:
|
|
116
|
+
from dars.scripts.script import FileScript # type: ignore
|
|
117
|
+
return FileScript(path, **kwargs)
|
|
118
|
+
except Exception:
|
|
119
|
+
return {'type': 'file', 'path': path, **kwargs}
|
|
120
|
+
|
|
121
|
+
def _make_dscript(self, obj: Any, **kwargs) -> Any:
|
|
122
|
+
"""Attempts to create a DScript instance if it exists. Otherwise, stores the object with a marker."""
|
|
123
|
+
try:
|
|
124
|
+
from dars.scripts.dscript import dScript # type: ignore
|
|
125
|
+
return dScript(obj, **kwargs)
|
|
126
|
+
except Exception:
|
|
127
|
+
# si ya es dict o similar, solo anotamos el tipo
|
|
128
|
+
return {'type': 'dscript', 'value': obj, **kwargs}
|
|
129
|
+
|
|
130
|
+
class App:
|
|
131
|
+
"""Main class that represents a Dars application"""
|
|
132
|
+
|
|
133
|
+
def rTimeCompile(self, exporter=None, port=None, add_file_types=".py, .js, .css", watchfiledialog=False):
|
|
134
|
+
"""
|
|
135
|
+
Generates a quick preview of the app on a local server using an exporter
|
|
136
|
+
(default: HTMLCSSJSExporter) and serving the files from a temporary directory.
|
|
137
|
+
Does not open the browser automatically. The server stops with Ctrl+C.
|
|
138
|
+
You can pass the port as a command-line argument: python main.py --port 8080
|
|
139
|
+
"""
|
|
140
|
+
import threading
|
|
141
|
+
import time
|
|
142
|
+
import sys
|
|
143
|
+
import os
|
|
144
|
+
import inspect
|
|
145
|
+
import importlib.util
|
|
146
|
+
from pathlib import Path
|
|
147
|
+
from contextlib import contextmanager
|
|
148
|
+
import shutil
|
|
149
|
+
import traceback
|
|
150
|
+
|
|
151
|
+
self.watchfiledialog = watchfiledialog
|
|
152
|
+
|
|
153
|
+
@contextmanager
|
|
154
|
+
def pushd(path):
|
|
155
|
+
"""Cambia temporalmente el cwd y lo restaura al salir."""
|
|
156
|
+
old = os.getcwd()
|
|
157
|
+
os.chdir(path)
|
|
158
|
+
try:
|
|
159
|
+
yield
|
|
160
|
+
finally:
|
|
161
|
+
os.chdir(old)
|
|
162
|
+
|
|
163
|
+
# Rich para mensajes bonitos
|
|
164
|
+
try:
|
|
165
|
+
from rich.console import Console
|
|
166
|
+
from rich.panel import Panel
|
|
167
|
+
from rich.text import Text
|
|
168
|
+
except ImportError:
|
|
169
|
+
Console = None
|
|
170
|
+
console = Console() if 'Console' in locals() else None
|
|
171
|
+
|
|
172
|
+
# Leer puerto de sys.argv si no se pasa explícito
|
|
173
|
+
if port is None:
|
|
174
|
+
port = 8000
|
|
175
|
+
for i, arg in enumerate(sys.argv):
|
|
176
|
+
if arg in ('--port', '-p') and i + 1 < len(sys.argv):
|
|
177
|
+
try:
|
|
178
|
+
port = int(sys.argv[i + 1])
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
# --- Normalizar add_file_types => lista de extensiones que empiezan con '.' ---
|
|
182
|
+
def _normalize_exts(exts):
|
|
183
|
+
if not exts:
|
|
184
|
+
return ['.py']
|
|
185
|
+
# aceptar string con comas
|
|
186
|
+
if isinstance(exts, str):
|
|
187
|
+
parts = [p.strip() for p in exts.split(',') if p.strip()]
|
|
188
|
+
elif isinstance(exts, (list, tuple, set)):
|
|
189
|
+
parts = [str(p).strip() for p in exts if p]
|
|
190
|
+
else:
|
|
191
|
+
parts = [str(exts).strip()]
|
|
192
|
+
|
|
193
|
+
normalized = []
|
|
194
|
+
for p in parts:
|
|
195
|
+
if not p:
|
|
196
|
+
continue
|
|
197
|
+
if not p.startswith('.'):
|
|
198
|
+
p = '.' + p
|
|
199
|
+
normalized.append(p.lower())
|
|
200
|
+
# siempre incluir .py (comportamiento: .py + los adicionales)
|
|
201
|
+
if '.py' not in normalized:
|
|
202
|
+
normalized.insert(0, '.py')
|
|
203
|
+
# eliminar duplicados preservando orden
|
|
204
|
+
seen = set()
|
|
205
|
+
result = []
|
|
206
|
+
for e in normalized:
|
|
207
|
+
if e not in seen:
|
|
208
|
+
seen.add(e)
|
|
209
|
+
result.append(e)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
# Lista final de extensiones a vigilar (ej: ['.py', '.js', '.css'])
|
|
213
|
+
watch_exts = _normalize_exts(add_file_types)
|
|
214
|
+
|
|
215
|
+
# Importar exportador por defecto si no se pasa
|
|
216
|
+
if exporter is None:
|
|
217
|
+
try:
|
|
218
|
+
from dars.exporters.web.html_css_js import HTMLCSSJSExporter
|
|
219
|
+
except ImportError:
|
|
220
|
+
print("Could not import HTMLCSSJSExporter")
|
|
221
|
+
return
|
|
222
|
+
exporter = HTMLCSSJSExporter()
|
|
223
|
+
|
|
224
|
+
# Importar PreviewServer
|
|
225
|
+
try:
|
|
226
|
+
from dars.cli.preview import PreviewServer
|
|
227
|
+
except ImportError:
|
|
228
|
+
print("Could not import PreviewServer")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
shutdown_event = threading.Event()
|
|
232
|
+
watchers = [] # aquí guardaremos todos los watchers
|
|
233
|
+
|
|
234
|
+
# Debounce / lock para evitar reloads concurrentes
|
|
235
|
+
reload_lock = threading.Lock()
|
|
236
|
+
last_reload_at = 0.0
|
|
237
|
+
MIN_RELOAD_INTERVAL = 0.4 # segundos
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Detectar archivo principal de la app (el que ejecutaste con `python archivo.py`)
|
|
241
|
+
app_file = None
|
|
242
|
+
for frame in inspect.stack():
|
|
243
|
+
if frame.function == "<module>":
|
|
244
|
+
app_file = frame.filename
|
|
245
|
+
break
|
|
246
|
+
if not app_file:
|
|
247
|
+
app_file = sys.argv[0]
|
|
248
|
+
|
|
249
|
+
project_root = os.path.dirname(os.path.abspath(app_file))
|
|
250
|
+
if project_root not in sys.path:
|
|
251
|
+
sys.path.insert(0, project_root)
|
|
252
|
+
|
|
253
|
+
preview_dir = os.path.join(project_root, "dars_preview")
|
|
254
|
+
cwd_original = os.getcwd()
|
|
255
|
+
|
|
256
|
+
# limpiar preview anterior
|
|
257
|
+
if os.path.exists(preview_dir):
|
|
258
|
+
try:
|
|
259
|
+
shutil.rmtree(preview_dir)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
msg = f"Warning: Could not clean previous preview directory: {e}"
|
|
262
|
+
console.print(f"[yellow]{msg}[/yellow]") if console else print(msg)
|
|
263
|
+
|
|
264
|
+
os.makedirs(preview_dir, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
# Advertir si no hay archivo de configuración
|
|
267
|
+
try:
|
|
268
|
+
from dars.config import load_config
|
|
269
|
+
cfg, cfg_found = load_config(project_root)
|
|
270
|
+
except Exception:
|
|
271
|
+
cfg, cfg_found = ({}, False)
|
|
272
|
+
if not cfg_found:
|
|
273
|
+
warn_msg = "[Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it in existing projects."
|
|
274
|
+
if console:
|
|
275
|
+
console.print(f"[yellow]{warn_msg}[/yellow]")
|
|
276
|
+
else:
|
|
277
|
+
print(warn_msg)
|
|
278
|
+
|
|
279
|
+
# export inicial desde el root usando la instancia actual (self)
|
|
280
|
+
with pushd(project_root):
|
|
281
|
+
exporter.export(self, preview_dir, bundle=False)
|
|
282
|
+
|
|
283
|
+
url = f"http://localhost:{port}"
|
|
284
|
+
app_title = getattr(self, 'title', 'Dars App')
|
|
285
|
+
if console:
|
|
286
|
+
panel = Panel(
|
|
287
|
+
Text(f"✔ App running successfully\n\nName: {app_title}\nPreview available at: {url}\n\nPress Ctrl+C to stop the server.",
|
|
288
|
+
style="bold green", justify="center"),
|
|
289
|
+
title="Dars Preview", border_style="cyan")
|
|
290
|
+
console.print(panel)
|
|
291
|
+
else:
|
|
292
|
+
print(f"[Dars] App '{app_title}' running. Preview at {url}")
|
|
293
|
+
|
|
294
|
+
server = PreviewServer(preview_dir, port)
|
|
295
|
+
try:
|
|
296
|
+
if not server.start():
|
|
297
|
+
(console.print("[red]Could not start preview server.[/red]")
|
|
298
|
+
if console else print("Could not start preview server."))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# --- HOT RELOAD ---
|
|
302
|
+
from dars.cli.hot_reload import FileWatcher
|
|
303
|
+
|
|
304
|
+
def _collect_project_files_by_ext(root, exts):
|
|
305
|
+
files = []
|
|
306
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
307
|
+
# excluir preview_dir, .git y __pycache__
|
|
308
|
+
if os.path.abspath(dirpath).startswith(os.path.abspath(preview_dir)):
|
|
309
|
+
continue
|
|
310
|
+
if '.git' in dirpath or '__pycache__' in dirpath:
|
|
311
|
+
continue
|
|
312
|
+
for fname in filenames:
|
|
313
|
+
for ext in exts:
|
|
314
|
+
if fname.lower().endswith(ext):
|
|
315
|
+
files.append(os.path.join(dirpath, fname))
|
|
316
|
+
break
|
|
317
|
+
return files
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reload_and_export(changed_file=None):
|
|
321
|
+
nonlocal last_reload_at
|
|
322
|
+
now = time.time()
|
|
323
|
+
# debounce rápido
|
|
324
|
+
if now - last_reload_at < MIN_RELOAD_INTERVAL:
|
|
325
|
+
return
|
|
326
|
+
with reload_lock:
|
|
327
|
+
last_reload_at = time.time()
|
|
328
|
+
if console:
|
|
329
|
+
console.print(f"[yellow]Detected change in {changed_file}. Reloading...[/yellow]")
|
|
330
|
+
else:
|
|
331
|
+
print(f"[Dars] Detected change in {changed_file}. Reloading...")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
if project_root not in sys.path:
|
|
335
|
+
sys.path.insert(0, project_root)
|
|
336
|
+
|
|
337
|
+
with pushd(project_root):
|
|
338
|
+
# --- Limpiar del cache todos los módulos que pertenecen al proyecto ---
|
|
339
|
+
to_remove = []
|
|
340
|
+
for name, mod in list(sys.modules.items()):
|
|
341
|
+
try:
|
|
342
|
+
mod_file = getattr(mod, '__file__', None)
|
|
343
|
+
if not mod_file:
|
|
344
|
+
continue
|
|
345
|
+
# normalizar paths
|
|
346
|
+
mod_file_abs = os.path.abspath(mod_file)
|
|
347
|
+
if mod_file_abs.startswith(os.path.abspath(project_root)):
|
|
348
|
+
to_remove.append(name)
|
|
349
|
+
except Exception:
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
for name in to_remove:
|
|
353
|
+
try:
|
|
354
|
+
del sys.modules[name]
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
# también borrar cualquier nombre temporal 'dars_app' si existiese
|
|
359
|
+
sys.modules.pop("dars_app", None)
|
|
360
|
+
|
|
361
|
+
# Importar el archivo principal en un nombre único (para limpieza segura)
|
|
362
|
+
unique_name = f"dars_app_reload_{int(time.time()*1000)}"
|
|
363
|
+
spec = importlib.util.spec_from_file_location(unique_name, app_file)
|
|
364
|
+
module = importlib.util.module_from_spec(spec)
|
|
365
|
+
spec.loader.exec_module(module)
|
|
366
|
+
|
|
367
|
+
# Buscar nueva instancia App en el módulo recargado
|
|
368
|
+
new_app = None
|
|
369
|
+
for v in vars(module).values():
|
|
370
|
+
try:
|
|
371
|
+
if isinstance(v, App):
|
|
372
|
+
new_app = v
|
|
373
|
+
break
|
|
374
|
+
except Exception:
|
|
375
|
+
# si isinstance falla por alguna razón, ignorar
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
# fallback por nombre de clase (por si App es distinto objeto)
|
|
379
|
+
if not new_app:
|
|
380
|
+
for v in vars(module).values():
|
|
381
|
+
try:
|
|
382
|
+
if hasattr(v, '__class__') and v.__class__.__name__ == 'App':
|
|
383
|
+
new_app = v
|
|
384
|
+
break
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
if not new_app:
|
|
389
|
+
(console.print("[red]No App instance found after reload.")
|
|
390
|
+
if console else print("[Dars] No App instance found after reload."))
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
# Exportar la nueva instancia
|
|
394
|
+
exporter.export(new_app, preview_dir, bundle=False)
|
|
395
|
+
|
|
396
|
+
(console.print("[green]App reloaded and re-exported successfully.[/green]")
|
|
397
|
+
if console else print("[Dars] App reloaded and re-exported successfully."))
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
tb = traceback.format_exc()
|
|
401
|
+
(console.print(f"[red]Hot reload failed: {e}\n{tb}[/red]")
|
|
402
|
+
if console else print(f"[Dars] Hot reload failed: {e}\n{tb}"))
|
|
403
|
+
|
|
404
|
+
# --- Crear watchers para todos los archivos .py dentro del proyecto (recursivo) ---
|
|
405
|
+
files_to_watch = _collect_project_files_by_ext(project_root, watch_exts)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Si no hay archivos detectados (raro), al menos mirar app_file
|
|
409
|
+
if not files_to_watch:
|
|
410
|
+
files_to_watch = [app_file]
|
|
411
|
+
|
|
412
|
+
for f in files_to_watch:
|
|
413
|
+
try:
|
|
414
|
+
# FileWatcher espera una función sin argumentos; usamos lambda que captura f
|
|
415
|
+
w = FileWatcher(f, lambda f=f: reload_and_export(f))
|
|
416
|
+
w.start()
|
|
417
|
+
watchers.append(w)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
if console:
|
|
420
|
+
console.print(f"[yellow]Warning: could not watch {f}: {e}[/yellow]")
|
|
421
|
+
else:
|
|
422
|
+
print(f"[Dars] Warning: could not watch {f}: {e}")
|
|
423
|
+
|
|
424
|
+
if console:
|
|
425
|
+
# Mostrar rutas relativas para que no sea tan largo
|
|
426
|
+
rel_paths = [os.path.relpath(f, project_root) for f in files_to_watch]
|
|
427
|
+
max_show = 80 # número máximo de líneas a mostrar
|
|
428
|
+
if len(rel_paths) > max_show:
|
|
429
|
+
shown = rel_paths[:max_show]
|
|
430
|
+
shown.append(f"... (+{len(rel_paths)-max_show} más)")
|
|
431
|
+
else:
|
|
432
|
+
shown = rel_paths or ["(ninguno)"]
|
|
433
|
+
|
|
434
|
+
from rich.table import Table
|
|
435
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
436
|
+
table.add_column("Files", style="bold")
|
|
437
|
+
for p in shown:
|
|
438
|
+
table.add_row(p)
|
|
439
|
+
|
|
440
|
+
panel = Panel(
|
|
441
|
+
table,
|
|
442
|
+
title=f"Watching {len(files_to_watch)} files · Exts: {', '.join(watch_exts)}",
|
|
443
|
+
subtitle=f"Project root: {os.path.basename(project_root)}",
|
|
444
|
+
border_style="magenta"
|
|
445
|
+
)
|
|
446
|
+
if self.watchfiledialog:
|
|
447
|
+
console.print(panel)
|
|
448
|
+
else:
|
|
449
|
+
if self.watchfiledialog:
|
|
450
|
+
print(f"[Dars] Watching {len(files_to_watch)} files in {project_root}:")
|
|
451
|
+
for f in files_to_watch:
|
|
452
|
+
print(" -", os.path.relpath(f, project_root))
|
|
453
|
+
|
|
454
|
+
# Loop principal: espera a Ctrl+C
|
|
455
|
+
while not shutdown_event.is_set():
|
|
456
|
+
shutdown_event.wait(timeout=1) # Espera sin consumir CPU
|
|
457
|
+
|
|
458
|
+
except KeyboardInterrupt:
|
|
459
|
+
shutdown_event.set()
|
|
460
|
+
for w in watchers:
|
|
461
|
+
try:
|
|
462
|
+
w.stop()
|
|
463
|
+
except Exception:
|
|
464
|
+
pass
|
|
465
|
+
(console.print("\n[cyan]Stopping preview and watcher...[/cyan]")
|
|
466
|
+
if console else print("\n[Dars] Stopping preview and watcher..."))
|
|
467
|
+
finally:
|
|
468
|
+
# Detener watchers y servidor
|
|
469
|
+
try:
|
|
470
|
+
server.stop()
|
|
471
|
+
except Exception:
|
|
472
|
+
pass
|
|
473
|
+
for w in watchers:
|
|
474
|
+
try:
|
|
475
|
+
w.stop()
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|
|
478
|
+
(console.print("[green]Preview stopped.[/green]")
|
|
479
|
+
if console else print("[Dars] Preview stopped."))
|
|
480
|
+
|
|
481
|
+
except PermissionError as e:
|
|
482
|
+
msg = f"Warning: Could not clean temp directory due to permissions: {e}"
|
|
483
|
+
console.print(f"[yellow]{msg}[/yellow]") if console else print(msg)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
msg = f"Unexpected error in fast preview: {e}\n{traceback.format_exc()}"
|
|
486
|
+
console.print(f"[red]{msg}[/red]") if console else print(msg)
|
|
487
|
+
finally:
|
|
488
|
+
# Restaurar cwd y limpiar preview
|
|
489
|
+
try:
|
|
490
|
+
os.chdir(cwd_original)
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
try:
|
|
494
|
+
shutil.rmtree(preview_dir)
|
|
495
|
+
(console.print("[yellow]Preview files deleted.[/yellow]")
|
|
496
|
+
if console else print("Preview files deleted."))
|
|
497
|
+
except Exception as e:
|
|
498
|
+
msg = f"Could not delete preview directory: {e}"
|
|
499
|
+
console.print(f"[red]{msg}[/red]") if console else print(msg)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def __init__(
|
|
503
|
+
self,
|
|
504
|
+
title: str = "Dars App",
|
|
505
|
+
description: str = "",
|
|
506
|
+
author: str = "",
|
|
507
|
+
keywords: List[str] = None,
|
|
508
|
+
language: str = "en",
|
|
509
|
+
favicon: str = "",
|
|
510
|
+
icon: str = "",
|
|
511
|
+
apple_touch_icon: str = "",
|
|
512
|
+
manifest: str = "",
|
|
513
|
+
theme_color: str = "#000000",
|
|
514
|
+
background_color: str = "#ffffff",
|
|
515
|
+
service_worker_path: str = "",
|
|
516
|
+
service_worker_enabled: bool = False,
|
|
517
|
+
**config
|
|
518
|
+
):
|
|
519
|
+
# Propiedades básicas de la aplicación
|
|
520
|
+
self.title = title
|
|
521
|
+
self.description = description
|
|
522
|
+
self.author = author
|
|
523
|
+
self.keywords = keywords or []
|
|
524
|
+
self.language = language
|
|
525
|
+
|
|
526
|
+
# Iconos y favicon
|
|
527
|
+
self.favicon = favicon
|
|
528
|
+
self.icon = icon # Para PWA y meta tags
|
|
529
|
+
self.apple_touch_icon = apple_touch_icon
|
|
530
|
+
self.manifest = manifest # Para PWA manifest.json
|
|
531
|
+
|
|
532
|
+
# Colores para PWA y tema
|
|
533
|
+
self.icons = config.get('icons', [])
|
|
534
|
+
self.theme_color = theme_color
|
|
535
|
+
self.background_color = background_color
|
|
536
|
+
self.service_worker_path = service_worker_path
|
|
537
|
+
self.service_worker_enabled = service_worker_enabled
|
|
538
|
+
|
|
539
|
+
# Propiedades Open Graph (para redes sociales)
|
|
540
|
+
|
|
541
|
+
#
|
|
542
|
+
# [RECOMENDACIÓN DARS]
|
|
543
|
+
# Para lanzar la compilación/preview rápido de tu app, añade al final de tu archivo principal:
|
|
544
|
+
# if __name__ == "__main__":
|
|
545
|
+
# app.rTimeCompile() # o app.timeCompile()
|
|
546
|
+
# Así tendrás preview instantáneo y control explícito, sin efectos colaterales.
|
|
547
|
+
#
|
|
548
|
+
self.og_title = config.get('og_title', title)
|
|
549
|
+
self.og_description = config.get('og_description', description)
|
|
550
|
+
self.og_image = config.get('og_image', '')
|
|
551
|
+
self.og_url = config.get('og_url', '')
|
|
552
|
+
self.og_type = config.get('og_type', 'website')
|
|
553
|
+
self.og_site_name = config.get('og_site_name', '')
|
|
554
|
+
|
|
555
|
+
# Twitter Cards
|
|
556
|
+
self.twitter_card = config.get('twitter_card', 'summary')
|
|
557
|
+
self.twitter_site = config.get('twitter_site', '')
|
|
558
|
+
self.twitter_creator = config.get('twitter_creator', '')
|
|
559
|
+
|
|
560
|
+
# SEO y robots
|
|
561
|
+
self.robots = config.get('robots', 'index, follow')
|
|
562
|
+
self.canonical_url = config.get('canonical_url', '')
|
|
563
|
+
|
|
564
|
+
# PWA configuración
|
|
565
|
+
self.pwa_enabled = config.get('pwa_enabled', False)
|
|
566
|
+
self.pwa_name = config.get('pwa_name', title)
|
|
567
|
+
self.pwa_short_name = config.get('pwa_short_name', title[:12])
|
|
568
|
+
self.pwa_display = config.get('pwa_display', 'standalone')
|
|
569
|
+
self.pwa_orientation = config.get('pwa_orientation', 'portrait')
|
|
570
|
+
|
|
571
|
+
# Propiedades del framework
|
|
572
|
+
self.root: Optional[Component] = None # Single-page mode
|
|
573
|
+
self._pages: Dict[str, Page] = {} # Multipage mode
|
|
574
|
+
self._index_page: str = None # Nombre de la página principal (si existe)
|
|
575
|
+
self.scripts: List['Script'] = []
|
|
576
|
+
self.global_styles: Dict[str, Any] = {}
|
|
577
|
+
self.global_style_files: List[str] = []
|
|
578
|
+
self.event_manager = EventManager()
|
|
579
|
+
self.config = config
|
|
580
|
+
|
|
581
|
+
# Configuración por defecto
|
|
582
|
+
self.config.setdefault('viewport', {
|
|
583
|
+
'width': 'device-width',
|
|
584
|
+
'initial_scale': 1.0,
|
|
585
|
+
'user_scalable': 'yes'
|
|
586
|
+
})
|
|
587
|
+
self.config.setdefault('theme', 'light')
|
|
588
|
+
self.config.setdefault('responsive', True)
|
|
589
|
+
self.config.setdefault('charset', 'UTF-8')
|
|
590
|
+
|
|
591
|
+
def set_root(self, component: Component):
|
|
592
|
+
"""Sets the root component of the application (backward-compatible single-page mode)."""
|
|
593
|
+
self.root = component
|
|
594
|
+
|
|
595
|
+
def add_page(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
|
|
596
|
+
"""
|
|
597
|
+
Adds a multipage page to the app.
|
|
598
|
+
`name` is the slug/key, `root` the root component.
|
|
599
|
+
If `index=True`, this page will be the main one (exported as index.html).
|
|
600
|
+
If multiple pages have `index=True`, the last registered one will be the main page.
|
|
601
|
+
"""
|
|
602
|
+
if name in self._pages:
|
|
603
|
+
raise ValueError(f"Page already exists with this name: '{name}'")
|
|
604
|
+
self._pages[name] = Page(name, root, title, meta, index=index)
|
|
605
|
+
if index:
|
|
606
|
+
self._index_page = name
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def get_page(self, name: str) -> 'Page':
|
|
610
|
+
"""Obtain one registered page by name."""
|
|
611
|
+
return self._pages.get(name)
|
|
612
|
+
|
|
613
|
+
def get_index_page(self) -> 'Page':
|
|
614
|
+
"""
|
|
615
|
+
Returns the index page, or the first one if none has index=True.
|
|
616
|
+
"""
|
|
617
|
+
# Prioridad: explícita, luego la primera
|
|
618
|
+
if hasattr(self, '_index_page') and self._index_page and self._index_page in self._pages:
|
|
619
|
+
return self._pages[self._index_page]
|
|
620
|
+
for page in self._pages.values():
|
|
621
|
+
if getattr(page, 'index', False):
|
|
622
|
+
return page
|
|
623
|
+
# Si ninguna marcada, devolver la primera
|
|
624
|
+
if self._pages:
|
|
625
|
+
return list(self._pages.values())[0]
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def pages(self) -> Dict[str, 'Page']:
|
|
631
|
+
"""Returns the registered pages dictionary (multipage)."""
|
|
632
|
+
return self._pages
|
|
633
|
+
|
|
634
|
+
def is_multipage(self) -> bool:
|
|
635
|
+
"""Indicate if the app is in multipage mode."""
|
|
636
|
+
return bool(self._pages)
|
|
637
|
+
|
|
638
|
+
def add_script(self, script: 'Script'):
|
|
639
|
+
"""Adds a script to the app"""
|
|
640
|
+
self.scripts.append(script)
|
|
641
|
+
|
|
642
|
+
def add_global_style(self, selector: str = None, styles: Dict[str, Any] = None, file_path: str = None):
|
|
643
|
+
"""
|
|
644
|
+
Adds a global style to the app.
|
|
645
|
+
|
|
646
|
+
- If file_path is provided, the CSS file is read and stored.
|
|
647
|
+
- If selector and styles are provided, they are stored as inline CSS rules.
|
|
648
|
+
- It is invalid to mix file_path with selector/styles.
|
|
649
|
+
"""
|
|
650
|
+
if file_path:
|
|
651
|
+
if selector or styles:
|
|
652
|
+
raise ValueError("Cannot use selector/styles when file_path is provided.")
|
|
653
|
+
if file_path not in self.global_style_files:
|
|
654
|
+
self.global_style_files.append(file_path)
|
|
655
|
+
return self
|
|
656
|
+
|
|
657
|
+
if not selector or not styles:
|
|
658
|
+
raise ValueError("Must provide selector and styles when file_path is not used.")
|
|
659
|
+
|
|
660
|
+
self.global_styles[selector] = styles
|
|
661
|
+
return self
|
|
662
|
+
|
|
663
|
+
def set_theme(self, theme: str):
|
|
664
|
+
"""Set the theme for the app"""
|
|
665
|
+
self.config['theme'] = theme
|
|
666
|
+
|
|
667
|
+
def set_favicon(self, favicon_path: str):
|
|
668
|
+
"""Set the favicon for the app"""
|
|
669
|
+
self.favicon = favicon_path
|
|
670
|
+
|
|
671
|
+
def set_icon(self, icon_path: str):
|
|
672
|
+
"""Set the principal icon for the app"""
|
|
673
|
+
self.icon = icon_path
|
|
674
|
+
|
|
675
|
+
def set_apple_touch_icon(self, icon_path: str):
|
|
676
|
+
"""Set de icon for apple devices"""
|
|
677
|
+
self.apple_touch_icon = icon_path
|
|
678
|
+
|
|
679
|
+
def set_manifest(self, manifest_path: str):
|
|
680
|
+
"""Set the manifes for PWA"""
|
|
681
|
+
self.manifest = manifest_path
|
|
682
|
+
|
|
683
|
+
def add_keyword(self, keyword: str):
|
|
684
|
+
"""Add a keyword for SEO"""
|
|
685
|
+
if keyword not in self.keywords:
|
|
686
|
+
self.keywords.append(keyword)
|
|
687
|
+
|
|
688
|
+
def add_keywords(self, keywords: List[str]):
|
|
689
|
+
"""Add multiple keywords for SEO"""
|
|
690
|
+
for keyword in keywords:
|
|
691
|
+
self.add_keyword(keyword)
|
|
692
|
+
|
|
693
|
+
def set_open_graph(self, **og_data):
|
|
694
|
+
"""Configure properties of Open Graph for social media sharing"""
|
|
695
|
+
if 'title' in og_data:
|
|
696
|
+
self.og_title = og_data['title']
|
|
697
|
+
if 'description' in og_data:
|
|
698
|
+
self.og_description = og_data['description']
|
|
699
|
+
if 'image' in og_data:
|
|
700
|
+
self.og_image = og_data['image']
|
|
701
|
+
if 'url' in og_data:
|
|
702
|
+
self.og_url = og_data['url']
|
|
703
|
+
if 'type' in og_data:
|
|
704
|
+
self.og_type = og_data['type']
|
|
705
|
+
if 'site_name' in og_data:
|
|
706
|
+
self.og_site_name = og_data['site_name']
|
|
707
|
+
|
|
708
|
+
def set_twitter_card(self, card_type: str = 'summary', site: str = '', creator: str = ''):
|
|
709
|
+
"""Set the Twitter Card meta tags"""
|
|
710
|
+
self.twitter_card = card_type
|
|
711
|
+
if site:
|
|
712
|
+
self.twitter_site = site
|
|
713
|
+
if creator:
|
|
714
|
+
self.twitter_creator = creator
|
|
715
|
+
|
|
716
|
+
def enable_pwa(self, name: str = None, short_name: str = None, display: str = 'standalone'):
|
|
717
|
+
"""Enable PWA settings (Progressive Web App)"""
|
|
718
|
+
self.pwa_enabled = True
|
|
719
|
+
if name:
|
|
720
|
+
self.pwa_name = name
|
|
721
|
+
if short_name:
|
|
722
|
+
self.pwa_short_name = short_name
|
|
723
|
+
self.pwa_display = display
|
|
724
|
+
|
|
725
|
+
def set_theme_colors(self, theme_color: str, background_color: str = None):
|
|
726
|
+
"""Select the theme color of the PWA theme and browsers themes """
|
|
727
|
+
self.theme_color = theme_color
|
|
728
|
+
if background_color:
|
|
729
|
+
self.background_color = background_color
|
|
730
|
+
|
|
731
|
+
def get_meta_tags(self) -> Dict[str, str]:
|
|
732
|
+
"""Obtain all tags of as a dictionary"""
|
|
733
|
+
meta_tags = {}
|
|
734
|
+
|
|
735
|
+
# Meta tags básicos
|
|
736
|
+
if self.description:
|
|
737
|
+
meta_tags['description'] = self.description
|
|
738
|
+
if self.author:
|
|
739
|
+
meta_tags['author'] = self.author
|
|
740
|
+
if self.keywords:
|
|
741
|
+
meta_tags['keywords'] = ', '.join(self.keywords)
|
|
742
|
+
if self.robots:
|
|
743
|
+
meta_tags['robots'] = self.robots
|
|
744
|
+
|
|
745
|
+
# Viewport
|
|
746
|
+
viewport_parts = []
|
|
747
|
+
for key, value in self.config['viewport'].items():
|
|
748
|
+
if key == 'initial_scale':
|
|
749
|
+
viewport_parts.append(f'initial-scale={value}')
|
|
750
|
+
elif key == 'user_scalable':
|
|
751
|
+
viewport_parts.append(f'user-scalable={value}')
|
|
752
|
+
else:
|
|
753
|
+
viewport_parts.append(f'{key.replace("_", "-")}={value}')
|
|
754
|
+
meta_tags['viewport'] = ', '.join(viewport_parts)
|
|
755
|
+
|
|
756
|
+
# PWA y tema
|
|
757
|
+
meta_tags['theme-color'] = self.theme_color
|
|
758
|
+
if self.pwa_enabled:
|
|
759
|
+
meta_tags['mobile-web-app-capable'] = 'yes'
|
|
760
|
+
meta_tags['apple-mobile-web-app-capable'] = 'yes'
|
|
761
|
+
meta_tags['apple-mobile-web-app-status-bar-style'] = 'default'
|
|
762
|
+
meta_tags['apple-mobile-web-app-title'] = self.pwa_short_name
|
|
763
|
+
|
|
764
|
+
return meta_tags
|
|
765
|
+
|
|
766
|
+
def get_open_graph_tags(self) -> Dict[str, str]:
|
|
767
|
+
""" Obtain all tags of Open Graph"""
|
|
768
|
+
og_tags = {}
|
|
769
|
+
|
|
770
|
+
if self.og_title:
|
|
771
|
+
og_tags['og:title'] = self.og_title
|
|
772
|
+
if self.og_description:
|
|
773
|
+
og_tags['og:description'] = self.og_description
|
|
774
|
+
if self.og_image:
|
|
775
|
+
og_tags['og:image'] = self.og_image
|
|
776
|
+
if self.og_url:
|
|
777
|
+
og_tags['og:url'] = self.og_url
|
|
778
|
+
if self.og_type:
|
|
779
|
+
og_tags['og:type'] = self.og_type
|
|
780
|
+
if self.og_site_name:
|
|
781
|
+
og_tags['og:site_name'] = self.og_site_name
|
|
782
|
+
|
|
783
|
+
return og_tags
|
|
784
|
+
|
|
785
|
+
def get_twitter_tags(self) -> Dict[str, str]:
|
|
786
|
+
"""Obtain all tags of Twitter Cards"""
|
|
787
|
+
twitter_tags = {}
|
|
788
|
+
|
|
789
|
+
if self.twitter_card:
|
|
790
|
+
twitter_tags['twitter:card'] = self.twitter_card
|
|
791
|
+
if self.twitter_site:
|
|
792
|
+
twitter_tags['twitter:site'] = self.twitter_site
|
|
793
|
+
if self.twitter_creator:
|
|
794
|
+
twitter_tags['twitter:creator'] = self.twitter_creator
|
|
795
|
+
|
|
796
|
+
return twitter_tags
|
|
797
|
+
|
|
798
|
+
def export(self, exporter: 'Exporter', output_path: str) -> bool:
|
|
799
|
+
"""Exports the application to the specified path using the exporter"""
|
|
800
|
+
if not self.root:
|
|
801
|
+
raise ValueError("No se ha establecido un componente raíz")
|
|
802
|
+
|
|
803
|
+
return exporter.export(self, output_path)
|
|
804
|
+
|
|
805
|
+
def validate(self) -> List[str]:
|
|
806
|
+
"""Validate the applicatiob and return a error lines"""
|
|
807
|
+
errors = []
|
|
808
|
+
|
|
809
|
+
# Validar título
|
|
810
|
+
if not self.title:
|
|
811
|
+
errors.append("The application title can't be empty.")
|
|
812
|
+
|
|
813
|
+
# Validación single-page y multipage
|
|
814
|
+
if self.is_multipage():
|
|
815
|
+
if not self._pages:
|
|
816
|
+
errors.append("The app is on multipage mode but there are no pages registered.")
|
|
817
|
+
for name, page in self._pages.items():
|
|
818
|
+
if not page.root:
|
|
819
|
+
errors.append(f"The page '{name}' hasn't a root component.")
|
|
820
|
+
else:
|
|
821
|
+
errors.extend(self._validate_component(page.root, path=f"pages['{name}']"))
|
|
822
|
+
else:
|
|
823
|
+
if not self.root:
|
|
824
|
+
errors.append("Can't find a root component (single-page mode)")
|
|
825
|
+
else:
|
|
826
|
+
errors.extend(self._validate_component(self.root))
|
|
827
|
+
|
|
828
|
+
return errors
|
|
829
|
+
|
|
830
|
+
def _validate_component(self, component: Component, path: str = "root") -> List[str]:
|
|
831
|
+
"""Validate a component and its children recursively"""
|
|
832
|
+
errors = []
|
|
833
|
+
|
|
834
|
+
# Validar que el componente tenga un método render
|
|
835
|
+
if not hasattr(component, 'render'):
|
|
836
|
+
errors.append(f"The component in {path} doesn't have render method")
|
|
837
|
+
|
|
838
|
+
# Validar hijos
|
|
839
|
+
for i, child in enumerate(component.children):
|
|
840
|
+
child_path = f"{path}.children[{i}]"
|
|
841
|
+
errors.extend(self._validate_component(child, child_path))
|
|
842
|
+
|
|
843
|
+
return errors
|
|
844
|
+
|
|
845
|
+
def _count_components(self, component: Component) -> int:
|
|
846
|
+
"""Count the total number of components in the app"""
|
|
847
|
+
count = 1
|
|
848
|
+
for child in component.children:
|
|
849
|
+
count += self._count_components(child)
|
|
850
|
+
return count
|
|
851
|
+
def get_component_tree(self) -> str:
|
|
852
|
+
"""
|
|
853
|
+
Returns a legible representation of the component tree.
|
|
854
|
+
"""
|
|
855
|
+
def tree_str(component, indent=0):
|
|
856
|
+
pad = ' ' * indent
|
|
857
|
+
s = f"{pad}- {component.__class__.__name__} (id={getattr(component, 'id', None)})"
|
|
858
|
+
for child in getattr(component, 'children', []):
|
|
859
|
+
s += '\n' + tree_str(child, indent + 1)
|
|
860
|
+
return s
|
|
861
|
+
|
|
862
|
+
if self.is_multipage():
|
|
863
|
+
if not self._pages:
|
|
864
|
+
return "[Dars] No pages registered."
|
|
865
|
+
result = []
|
|
866
|
+
for name, page in self._pages.items():
|
|
867
|
+
result.append(f"Página: {name} (title={page.title})\n" + tree_str(page.root))
|
|
868
|
+
return '\n\n'.join(result)
|
|
869
|
+
elif self.root:
|
|
870
|
+
return tree_str(self.root)
|
|
871
|
+
else:
|
|
872
|
+
return "[Dars] No root component defined."
|
|
873
|
+
|
|
874
|
+
def _component_to_dict(self, component: Component) -> Dict[str, Any]:
|
|
875
|
+
"""Convert a component to a dictionary for inspection"""
|
|
876
|
+
return {
|
|
877
|
+
'type': component.__class__.__name__,
|
|
878
|
+
'id': component.id,
|
|
879
|
+
'class_name': component.class_name,
|
|
880
|
+
'props': component.props,
|
|
881
|
+
'style': component.style,
|
|
882
|
+
'children': [self._component_to_dict(child) for child in component.children]
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
def find_component_by_id(self, component_id: str) -> Optional[Component]:
|
|
886
|
+
"""Find a component by its ID (soporta multipage y single-page)"""
|
|
887
|
+
if self.is_multipage():
|
|
888
|
+
for page in self._pages.values():
|
|
889
|
+
result = self._find_component_recursive(page.root, component_id)
|
|
890
|
+
if result:
|
|
891
|
+
return result
|
|
892
|
+
return None
|
|
893
|
+
elif self.root:
|
|
894
|
+
return self._find_component_recursive(self.root, component_id)
|
|
895
|
+
else:
|
|
896
|
+
return None
|
|
897
|
+
|
|
898
|
+
def _find_component_recursive(self, component: Component, target_id: str) -> Optional[Component]:
|
|
899
|
+
"""Search components recursively by ID"""
|
|
900
|
+
if component.id == target_id:
|
|
901
|
+
return component
|
|
902
|
+
for child in getattr(component, 'children', []):
|
|
903
|
+
result = self._find_component_recursive(child, target_id)
|
|
904
|
+
if result:
|
|
905
|
+
return result
|
|
906
|
+
return None
|
|
907
|
+
|
|
908
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
909
|
+
"""Return application stadistics (single-page and multipage)"""
|
|
910
|
+
if self.is_multipage():
|
|
911
|
+
total_components = 0
|
|
912
|
+
max_depth = 0
|
|
913
|
+
for page in self._pages.values():
|
|
914
|
+
if page.root:
|
|
915
|
+
total_components += self._count_components(page.root)
|
|
916
|
+
depth = self._calculate_max_depth(page.root)
|
|
917
|
+
max_depth = max(max_depth, depth)
|
|
918
|
+
return {
|
|
919
|
+
'total_components': total_components,
|
|
920
|
+
'max_depth': max_depth,
|
|
921
|
+
'scripts_count': len(self.scripts),
|
|
922
|
+
'global_styles_count': len(self.global_styles),
|
|
923
|
+
'total_pages': len(self._pages)
|
|
924
|
+
}
|
|
925
|
+
elif self.root:
|
|
926
|
+
return {
|
|
927
|
+
'total_components': self._count_components(self.root),
|
|
928
|
+
'max_depth': self._calculate_max_depth(self.root),
|
|
929
|
+
'scripts_count': len(self.scripts),
|
|
930
|
+
'global_styles_count': len(self.global_styles),
|
|
931
|
+
'total_pages': 1
|
|
932
|
+
}
|
|
933
|
+
else:
|
|
934
|
+
return {
|
|
935
|
+
'total_components': 0,
|
|
936
|
+
'max_depth': 0,
|
|
937
|
+
'scripts_count': len(self.scripts),
|
|
938
|
+
'global_styles_count': len(self.global_styles),
|
|
939
|
+
'total_pages': 0
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
def calculate_max_depth(self) -> int:
|
|
943
|
+
"""Calculates the maximun depth of a component tree (single page and multipage)"""
|
|
944
|
+
if self.is_multipage():
|
|
945
|
+
return max((self._calculate_max_depth(page.root) for page in self._pages.values() if page.root), default=0)
|
|
946
|
+
elif self.root:
|
|
947
|
+
return self._calculate_max_depth(self.root)
|
|
948
|
+
else:
|
|
949
|
+
return 0
|
|
950
|
+
|
|
951
|
+
def _calculate_max_depth(self, component: Component, current_depth: int = 0) -> int:
|
|
952
|
+
"""Calculates the maximun depth of a component tree (internal use)"""
|
|
953
|
+
if not component or not getattr(component, 'children', []):
|
|
954
|
+
return current_depth
|
|
955
|
+
return max(self._calculate_max_depth(child, current_depth + 1) for child in component.children)
|
|
956
|
+
|
|
957
|
+
|