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
|
@@ -0,0 +1,2675 @@
|
|
|
1
|
+
from dars.exporters.base import Exporter
|
|
2
|
+
from dars.scripts.dscript import dScript
|
|
3
|
+
from dars.core.app import App
|
|
4
|
+
from dars.core.component import Component
|
|
5
|
+
from dars.components.basic.text import Text
|
|
6
|
+
from dars.components.basic.button import Button
|
|
7
|
+
from dars.components.basic.input import Input
|
|
8
|
+
from dars.components.basic.container import Container
|
|
9
|
+
from dars.components.basic.image import Image
|
|
10
|
+
from dars.components.basic.link import Link
|
|
11
|
+
from dars.components.basic.textarea import Textarea
|
|
12
|
+
from dars.components.basic.checkbox import Checkbox
|
|
13
|
+
from dars.components.basic.radiobutton import RadioButton
|
|
14
|
+
from dars.components.basic.select import Select
|
|
15
|
+
from dars.components.basic.slider import Slider
|
|
16
|
+
from dars.components.basic.datepicker import DatePicker
|
|
17
|
+
from dars.components.advanced.card import Card
|
|
18
|
+
from dars.components.advanced.modal import Modal
|
|
19
|
+
from dars.components.advanced.navbar import Navbar
|
|
20
|
+
from dars.components.advanced.table import Table
|
|
21
|
+
from dars.components.advanced.tabs import Tabs
|
|
22
|
+
from dars.components.advanced.accordion import Accordion
|
|
23
|
+
from dars.components.basic.progressbar import ProgressBar
|
|
24
|
+
from dars.components.basic.spinner import Spinner
|
|
25
|
+
from dars.components.basic.tooltip import Tooltip
|
|
26
|
+
from dars.components.basic.markdown import Markdown
|
|
27
|
+
from typing import Dict, Any
|
|
28
|
+
import os
|
|
29
|
+
from bs4 import BeautifulSoup
|
|
30
|
+
from dars.exporters.web.vdom import VDomBuilder
|
|
31
|
+
from dars.config import load_config, resolve_paths, copy_public_dir
|
|
32
|
+
|
|
33
|
+
class HTMLCSSJSExporter(Exporter):
|
|
34
|
+
"""Exportador para HTML, CSS y JavaScript"""
|
|
35
|
+
|
|
36
|
+
def get_platform(self) -> str:
|
|
37
|
+
return "html"
|
|
38
|
+
|
|
39
|
+
def export(self, app: App, output_path: str, bundle: bool = False) -> bool:
|
|
40
|
+
"""Exporta la aplicación a HTML/CSS/JS (soporta multipágina)."""
|
|
41
|
+
try:
|
|
42
|
+
# Initialize obfuscation context for this export
|
|
43
|
+
# Keep original IDs to avoid breaking CSS/anchors. We still obfuscate types and events.
|
|
44
|
+
self._hash_ids = False
|
|
45
|
+
self._id_hash_map = {}
|
|
46
|
+
self._type_obfuscation = bool(bundle)
|
|
47
|
+
self._type_map = {}
|
|
48
|
+
self._type_seq = 0
|
|
49
|
+
self.create_output_directory(output_path)
|
|
50
|
+
|
|
51
|
+
# --- Copiar recursos adicionales desde la carpeta del proyecto ---
|
|
52
|
+
import inspect, shutil
|
|
53
|
+
import sys
|
|
54
|
+
# Determinar la raíz del proyecto desde el archivo fuente de la app
|
|
55
|
+
app_source = getattr(app, '__source__', None)
|
|
56
|
+
if app_source is None and hasattr(app, 'source_file'):
|
|
57
|
+
app_source = app.source_file
|
|
58
|
+
if app_source is None:
|
|
59
|
+
# Fallback: usar root del componente, pero esto no es robusto
|
|
60
|
+
project_root = os.getcwd()
|
|
61
|
+
else:
|
|
62
|
+
project_root = os.path.dirname(os.path.abspath(app_source))
|
|
63
|
+
|
|
64
|
+
# --- Escribir librería de reactividad (dars.min.js) embebida ---
|
|
65
|
+
try:
|
|
66
|
+
lib_dir = os.path.join(output_path, 'lib')
|
|
67
|
+
os.makedirs(lib_dir, exist_ok=True)
|
|
68
|
+
dest_js = os.path.join(lib_dir, 'dars.min.js')
|
|
69
|
+
from dars.js_lib import DARS_MIN_JS
|
|
70
|
+
with open(dest_js, 'w', encoding='utf-8') as f:
|
|
71
|
+
f.write(DARS_MIN_JS)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# --- Cargar configuración si existe y copiar public/assets ---
|
|
76
|
+
try:
|
|
77
|
+
cfg, cfg_found = load_config(project_root)
|
|
78
|
+
except Exception:
|
|
79
|
+
cfg, cfg_found = ({}, False)
|
|
80
|
+
try:
|
|
81
|
+
resolved = resolve_paths(cfg if cfg else {}, project_root)
|
|
82
|
+
except Exception:
|
|
83
|
+
resolved = {"public_abs": None, "include": [], "exclude": []}
|
|
84
|
+
|
|
85
|
+
# Copiar public/assets completos al output (tanto en preview como en bundle)
|
|
86
|
+
try:
|
|
87
|
+
public_abs = resolved.get("public_abs")
|
|
88
|
+
include = cfg.get("include", []) if cfg else []
|
|
89
|
+
exclude = cfg.get("exclude", []) if cfg else []
|
|
90
|
+
if not public_abs:
|
|
91
|
+
# autodetect simple si no viene en config
|
|
92
|
+
cand_public = os.path.join(project_root, "public")
|
|
93
|
+
cand_assets = os.path.join(project_root, "assets")
|
|
94
|
+
if os.path.isdir(cand_public):
|
|
95
|
+
public_abs = cand_public
|
|
96
|
+
elif os.path.isdir(cand_assets):
|
|
97
|
+
public_abs = cand_assets
|
|
98
|
+
if public_abs and os.path.isdir(public_abs):
|
|
99
|
+
copy_public_dir(public_abs, output_path, include=include, exclude=exclude)
|
|
100
|
+
except Exception:
|
|
101
|
+
# Mejor esfuerzo, no romper export
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
os.makedirs(output_path, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
# Copiar solo recursos explícitos usados por la app
|
|
107
|
+
# 1) Favicon
|
|
108
|
+
favicon = getattr(app, 'favicon', None)
|
|
109
|
+
if favicon and os.path.isfile(os.path.join(project_root, favicon)):
|
|
110
|
+
shutil.copy2(os.path.join(project_root, favicon), os.path.join(output_path, os.path.basename(favicon)))
|
|
111
|
+
# 2) Iconos PWA
|
|
112
|
+
icons = getattr(app, 'icons', None)
|
|
113
|
+
if icons:
|
|
114
|
+
icons_dir = os.path.join(output_path, 'icons')
|
|
115
|
+
os.makedirs(icons_dir, exist_ok=True)
|
|
116
|
+
for icon in icons:
|
|
117
|
+
src = icon.get('src') if isinstance(icon, dict) else icon
|
|
118
|
+
if src and os.path.isfile(os.path.join(project_root, src)):
|
|
119
|
+
shutil.copy2(os.path.join(project_root, src), os.path.join(icons_dir, os.path.basename(src)))
|
|
120
|
+
# 3) Service Worker
|
|
121
|
+
sw_path = getattr(app, 'service_worker_path', None)
|
|
122
|
+
if sw_path and os.path.isfile(os.path.join(project_root, sw_path)):
|
|
123
|
+
shutil.copy2(os.path.join(project_root, sw_path), os.path.join(output_path, 'sw.js'))
|
|
124
|
+
# 4) Archivos estáticos definidos por el usuario
|
|
125
|
+
static_files = getattr(app, 'static_files', [])
|
|
126
|
+
for static in static_files:
|
|
127
|
+
src = static.get('src') if isinstance(static, dict) else static
|
|
128
|
+
if src and os.path.isfile(os.path.join(project_root, src)):
|
|
129
|
+
shutil.copy2(os.path.join(project_root, src), os.path.join(output_path, os.path.basename(src)))
|
|
130
|
+
# NOTA: No copiar ejecutables ni nada fuera del proyecto
|
|
131
|
+
|
|
132
|
+
base_css_content = self.generate_base_css() # Nuevo método para estilos base
|
|
133
|
+
custom_css_content = self.generate_custom_css(app) # Nuevo método para estilos personalizados
|
|
134
|
+
|
|
135
|
+
self.write_file(os.path.join(output_path, "runtime_css.css"), base_css_content)
|
|
136
|
+
self.write_file(os.path.join(output_path, "styles.css"), custom_css_content)
|
|
137
|
+
|
|
138
|
+
# Multipágina: exportar un HTML, CSS y JS por cada página registrada
|
|
139
|
+
if hasattr(app, "is_multipage") and app.is_multipage():
|
|
140
|
+
import copy
|
|
141
|
+
index_page = None
|
|
142
|
+
if hasattr(app, 'get_index_page'):
|
|
143
|
+
index_page = app.get_index_page()
|
|
144
|
+
|
|
145
|
+
# Exportar cada página
|
|
146
|
+
for slug, page in app.pages.items():
|
|
147
|
+
page_app = copy.copy(app)
|
|
148
|
+
page_app.root = page.root
|
|
149
|
+
if page.title:
|
|
150
|
+
page_app.title = page.title
|
|
151
|
+
if page.meta:
|
|
152
|
+
for k, v in page.meta.items():
|
|
153
|
+
setattr(page_app, k, v)
|
|
154
|
+
|
|
155
|
+
# Asegurar que root sea Container si es lista
|
|
156
|
+
from dars.components.basic.container import Container
|
|
157
|
+
if isinstance(page_app.root, list):
|
|
158
|
+
page_app.root = Container(children=page_app.root)
|
|
159
|
+
|
|
160
|
+
# Generar runtime específico para esta página
|
|
161
|
+
runtime_js = self.generate_javascript(page_app, page.root)
|
|
162
|
+
runtime_name = f"runtime_dars_{slug}.js" if slug != "index" else "runtime_dars.js"
|
|
163
|
+
self.write_file(os.path.join(output_path, runtime_name), runtime_js)
|
|
164
|
+
# Generar VDOM Tree JS (externo)
|
|
165
|
+
vdom_name = None
|
|
166
|
+
try:
|
|
167
|
+
vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(page_app.root)
|
|
168
|
+
if bundle:
|
|
169
|
+
vdom_dict = self._obfuscate_vdom(vdom_dict)
|
|
170
|
+
import json
|
|
171
|
+
vdom_js = "window.__DARS_VDOM__ = " + json.dumps(vdom_dict, ensure_ascii=False, separators=(",", ":")) + ";\n"
|
|
172
|
+
except Exception:
|
|
173
|
+
vdom_js = "window.__DARS_VDOM__ = { };\n"
|
|
174
|
+
vdom_name = f"vdom_tree_{slug}.js" if slug != "index" else "vdom_tree.js"
|
|
175
|
+
self.write_file(os.path.join(output_path, vdom_name), vdom_js)
|
|
176
|
+
# Fase 2: escribir snapshot/version por página (solo en dev, no bundle)
|
|
177
|
+
if not bundle:
|
|
178
|
+
try:
|
|
179
|
+
vdom_json = self.generate_vdom_snapshot(page_app.root)
|
|
180
|
+
except Exception:
|
|
181
|
+
vdom_json = '{}'
|
|
182
|
+
if slug != 'index':
|
|
183
|
+
snapshot_name = f"snapshot_{slug}.json"
|
|
184
|
+
version_name = f"version_{slug}.txt"
|
|
185
|
+
else:
|
|
186
|
+
snapshot_name = "snapshot.json"
|
|
187
|
+
version_name = "version.txt"
|
|
188
|
+
self.write_file(os.path.join(output_path, snapshot_name), vdom_json)
|
|
189
|
+
try:
|
|
190
|
+
import time
|
|
191
|
+
version_val = str(int(time.time()*1000))
|
|
192
|
+
except Exception:
|
|
193
|
+
version_val = "1"
|
|
194
|
+
self.write_file(os.path.join(output_path, version_name), version_val)
|
|
195
|
+
|
|
196
|
+
# Generar scripts específicos de esta página
|
|
197
|
+
page_scripts = []
|
|
198
|
+
|
|
199
|
+
# Scripts globales de la app
|
|
200
|
+
page_scripts.extend(getattr(app, 'scripts', []))
|
|
201
|
+
|
|
202
|
+
# Scripts específicos de esta página
|
|
203
|
+
if hasattr(page, 'scripts'):
|
|
204
|
+
page_scripts.extend(page.scripts)
|
|
205
|
+
|
|
206
|
+
# Scripts de componentes dentro de la página
|
|
207
|
+
if hasattr(page_app.root, 'get_scripts'):
|
|
208
|
+
page_scripts.extend(page_app.root.get_scripts())
|
|
209
|
+
|
|
210
|
+
# Generar script.js específico para esta página
|
|
211
|
+
# Preparar y copiar scripts: combinados + externos
|
|
212
|
+
combined_js, external_srcs, combined_is_module = self._prepare_page_scripts(page_scripts, output_path, project_root)
|
|
213
|
+
|
|
214
|
+
if index_page is not None and page is index_page:
|
|
215
|
+
# Página index
|
|
216
|
+
self.write_file(os.path.join(output_path, "script.js"), combined_js)
|
|
217
|
+
html_content = self.generate_html(page_app, css_file="styles.css",
|
|
218
|
+
script_file="script.js",
|
|
219
|
+
runtime_file="runtime_dars.js",
|
|
220
|
+
extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
|
|
221
|
+
script_is_module=combined_is_module)
|
|
222
|
+
filename = "index.html"
|
|
223
|
+
else:
|
|
224
|
+
# Otras páginas
|
|
225
|
+
script_name = f"script_{slug}.js"
|
|
226
|
+
self.write_file(os.path.join(output_path, script_name), combined_js)
|
|
227
|
+
html_content = self.generate_html(page_app, css_file="styles.css",
|
|
228
|
+
script_file=script_name,
|
|
229
|
+
runtime_file=runtime_name,
|
|
230
|
+
extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
|
|
231
|
+
script_is_module=combined_is_module)
|
|
232
|
+
filename = f"{slug}.html"
|
|
233
|
+
|
|
234
|
+
# Mejorar formato HTML si es posible
|
|
235
|
+
try:
|
|
236
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
237
|
+
html_content = soup.prettify()
|
|
238
|
+
except ImportError:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
self.write_file(os.path.join(output_path, filename), html_content)
|
|
242
|
+
else:
|
|
243
|
+
# Single-page clásico (mantener comportamiento existente)
|
|
244
|
+
runtime_js = self.generate_javascript(app, app.root)
|
|
245
|
+
self.write_file(os.path.join(output_path, "runtime_dars.js"), runtime_js)
|
|
246
|
+
|
|
247
|
+
user_scripts = list(getattr(app, 'scripts', []))
|
|
248
|
+
combined_js, external_srcs, combined_is_module = self._prepare_page_scripts(user_scripts, output_path, project_root)
|
|
249
|
+
self.write_file(os.path.join(output_path, "script.js"), combined_js)
|
|
250
|
+
# Generar VDOM Tree JS (externo)
|
|
251
|
+
vdom_name = None
|
|
252
|
+
try:
|
|
253
|
+
vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(app.root)
|
|
254
|
+
if bundle:
|
|
255
|
+
vdom_dict = self._obfuscate_vdom(vdom_dict)
|
|
256
|
+
import json
|
|
257
|
+
vdom_js = "window.__DARS_VDOM__ = " + json.dumps(vdom_dict, ensure_ascii=False, separators=(",", ":")) + ";\n"
|
|
258
|
+
except Exception:
|
|
259
|
+
vdom_js = "window.__DARS_VDOM__ = { };\n"
|
|
260
|
+
vdom_name = "vdom_tree.js"
|
|
261
|
+
self.write_file(os.path.join(output_path, vdom_name), vdom_js)
|
|
262
|
+
|
|
263
|
+
html_content = self.generate_html(app, css_file="styles.css",
|
|
264
|
+
script_file="script.js",
|
|
265
|
+
runtime_file="runtime_dars.js",
|
|
266
|
+
extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
|
|
267
|
+
script_is_module=combined_is_module)
|
|
268
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
269
|
+
html_content = soup.prettify()
|
|
270
|
+
|
|
271
|
+
self.write_file(os.path.join(output_path, "index.html"), html_content)
|
|
272
|
+
# Fase 2: snapshot/version para single-page (solo en dev, no bundle)
|
|
273
|
+
if not bundle:
|
|
274
|
+
try:
|
|
275
|
+
vdom_json = self.generate_vdom_snapshot(app.root)
|
|
276
|
+
except Exception:
|
|
277
|
+
vdom_json = '{}'
|
|
278
|
+
self.write_file(os.path.join(output_path, "snapshot.json"), vdom_json)
|
|
279
|
+
try:
|
|
280
|
+
import time
|
|
281
|
+
version_val = str(int(time.time()*1000))
|
|
282
|
+
except Exception:
|
|
283
|
+
version_val = "1"
|
|
284
|
+
self.write_file(os.path.join(output_path, "version.txt"), version_val)
|
|
285
|
+
|
|
286
|
+
# Generar archivos PWA si está habilitado
|
|
287
|
+
if getattr(app, 'pwa_enabled', False):
|
|
288
|
+
self._generate_pwa_files(app, output_path)
|
|
289
|
+
|
|
290
|
+
# Limpiar bootstrap de estado para evitar duplicados en siguientes exports
|
|
291
|
+
try:
|
|
292
|
+
from dars.core.state import STATE_BOOTSTRAP
|
|
293
|
+
if isinstance(STATE_BOOTSTRAP, list):
|
|
294
|
+
STATE_BOOTSTRAP.clear()
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
return True
|
|
299
|
+
except Exception as e:
|
|
300
|
+
print(f"Error al exportar: {e}")
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _generate_pwa_files(self, app: 'App', output_path: str) -> None:
|
|
305
|
+
"""Genera manifest.json, iconos y service worker para PWA"""
|
|
306
|
+
import json, os
|
|
307
|
+
# Manifest
|
|
308
|
+
self._generate_manifest_json(app, output_path)
|
|
309
|
+
# Iconos por defecto (placeholder, puedes mejorar esto)
|
|
310
|
+
self._generate_default_icons(output_path)
|
|
311
|
+
# Service worker
|
|
312
|
+
sw_path = getattr(app, 'service_worker_path', None)
|
|
313
|
+
sw_enabled = getattr(app, 'service_worker_enabled', True)
|
|
314
|
+
if sw_enabled:
|
|
315
|
+
if sw_path:
|
|
316
|
+
# Copiar el personalizado
|
|
317
|
+
import shutil
|
|
318
|
+
shutil.copy(sw_path, os.path.join(output_path, 'sw.js'))
|
|
319
|
+
else:
|
|
320
|
+
self._generate_basic_service_worker(output_path)
|
|
321
|
+
|
|
322
|
+
def _generate_manifest_json(self, app: 'App', output_path: str) -> None:
|
|
323
|
+
import json, os, shutil
|
|
324
|
+
manifest = {
|
|
325
|
+
"name": getattr(app, 'pwa_name', getattr(app, 'title', 'Dars App')),
|
|
326
|
+
"short_name": getattr(app, 'pwa_short_name', 'Dars'),
|
|
327
|
+
"description": getattr(app, 'description', 'Aplicación web progresiva creada con Dars'),
|
|
328
|
+
"start_url": ".",
|
|
329
|
+
"display": getattr(app, 'pwa_display', 'standalone'),
|
|
330
|
+
"background_color": getattr(app, 'background_color', '#ffffff'),
|
|
331
|
+
"theme_color": getattr(app, 'theme_color', '#4a90e2'),
|
|
332
|
+
"orientation": getattr(app, 'pwa_orientation', 'portrait')
|
|
333
|
+
}
|
|
334
|
+
icons = self._get_icons_manifest(app, output_path)
|
|
335
|
+
if icons is not None:
|
|
336
|
+
manifest["icons"] = icons
|
|
337
|
+
manifest_path = os.path.join(output_path, "manifest.json")
|
|
338
|
+
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
339
|
+
json.dump(manifest, f, indent=2)
|
|
340
|
+
|
|
341
|
+
def _get_icons_manifest(self, app: 'App', output_path: str) -> list:
|
|
342
|
+
import os, shutil
|
|
343
|
+
user_icons = getattr(app, 'icons', None)
|
|
344
|
+
if user_icons is not None:
|
|
345
|
+
# Si el usuario define icons=[] explícito, no ponemos icons
|
|
346
|
+
if isinstance(user_icons, list) and len(user_icons) == 0:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
# Obtener project_root para rutas relativas
|
|
350
|
+
app_source = getattr(app, '__source__', None)
|
|
351
|
+
if app_source is None and hasattr(app, 'source_file'):
|
|
352
|
+
app_source = app.source_file
|
|
353
|
+
if app_source is None:
|
|
354
|
+
project_root = os.getcwd()
|
|
355
|
+
else:
|
|
356
|
+
project_root = os.path.dirname(os.path.abspath(app_source))
|
|
357
|
+
|
|
358
|
+
# Si el usuario define iconos personalizados
|
|
359
|
+
icons_manifest = []
|
|
360
|
+
icons_dir = os.path.join(output_path, "icons")
|
|
361
|
+
os.makedirs(icons_dir, exist_ok=True)
|
|
362
|
+
|
|
363
|
+
for icon in user_icons:
|
|
364
|
+
if isinstance(icon, dict):
|
|
365
|
+
src = icon.get("src")
|
|
366
|
+
if src:
|
|
367
|
+
# Resolver ruta relativa a project_root
|
|
368
|
+
src_path = os.path.join(project_root, src) if not os.path.isabs(src) else src
|
|
369
|
+
if os.path.isfile(src_path):
|
|
370
|
+
# Copiamos el icono al output
|
|
371
|
+
dest_path = os.path.join(icons_dir, os.path.basename(src))
|
|
372
|
+
shutil.copy(src_path, dest_path)
|
|
373
|
+
icon_copy = icon.copy() # No modificar el original
|
|
374
|
+
icon_copy["src"] = f"/icons/{os.path.basename(src)}"
|
|
375
|
+
icons_manifest.append(icon_copy)
|
|
376
|
+
else:
|
|
377
|
+
# Si no existe, usar la ruta tal cual (podría ser URL)
|
|
378
|
+
icons_manifest.append(icon)
|
|
379
|
+
else:
|
|
380
|
+
icons_manifest.append(icon)
|
|
381
|
+
elif isinstance(icon, str):
|
|
382
|
+
# Si solo es una ruta, la copiamos y generamos el dict
|
|
383
|
+
src_path = os.path.join(project_root, icon) if not os.path.isabs(icon) else icon
|
|
384
|
+
if os.path.isfile(src_path):
|
|
385
|
+
dest_path = os.path.join(icons_dir, os.path.basename(icon))
|
|
386
|
+
shutil.copy(src_path, dest_path)
|
|
387
|
+
icons_manifest.append({
|
|
388
|
+
"src": f"icons/{os.path.basename(icon)}",
|
|
389
|
+
"sizes": "192x192",
|
|
390
|
+
"type": "image/png",
|
|
391
|
+
"purpose": "any maskable"
|
|
392
|
+
})
|
|
393
|
+
else:
|
|
394
|
+
# Si no existe, asumir que es URL
|
|
395
|
+
icons_manifest.append({
|
|
396
|
+
"src": icon,
|
|
397
|
+
"sizes": "192x192",
|
|
398
|
+
"type": "image/png"
|
|
399
|
+
})
|
|
400
|
+
return icons_manifest if icons_manifest else None
|
|
401
|
+
|
|
402
|
+
# Si no hay icons definidos, poner por defecto
|
|
403
|
+
return [
|
|
404
|
+
{
|
|
405
|
+
"src": "icons/icon-192x192.png",
|
|
406
|
+
"sizes": "192x192",
|
|
407
|
+
"type": "image/png",
|
|
408
|
+
"purpose": "any maskable"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"src": "icons/icon-512x512.png",
|
|
412
|
+
"sizes": "512x512",
|
|
413
|
+
"type": "image/png"
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
def _generate_default_icons(self, output_path: str) -> None:
|
|
418
|
+
import os, shutil
|
|
419
|
+
# Ruta de los iconos PWA por defecto incluidos en el framework
|
|
420
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
421
|
+
default_icons_dir = os.path.join(base_dir, "icons", "pwa")
|
|
422
|
+
icons_dir = os.path.join(output_path, "icons")
|
|
423
|
+
os.makedirs(icons_dir, exist_ok=True)
|
|
424
|
+
# Copiar icon-192x192.png y icon-512x512.png si existen
|
|
425
|
+
for fname in ["icon-192x192.png", "icon-512x512.png"]:
|
|
426
|
+
src = os.path.join(default_icons_dir, fname)
|
|
427
|
+
dst = os.path.join(icons_dir, fname)
|
|
428
|
+
if os.path.isfile(src):
|
|
429
|
+
shutil.copy(src, dst)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _generate_basic_service_worker(self, output_path: str) -> None:
|
|
433
|
+
sw_content = '''// Service Worker básico para Dars PWA
|
|
434
|
+
const CACHE_NAME = 'dars-pwa-cache-v1';
|
|
435
|
+
const urlsToCache = [
|
|
436
|
+
'/',
|
|
437
|
+
'/index.html',
|
|
438
|
+
'/styles.css',
|
|
439
|
+
'/script.js'
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
self.addEventListener('install', event => {
|
|
443
|
+
event.waitUntil(
|
|
444
|
+
caches.open(CACHE_NAME)
|
|
445
|
+
.then(cache => {
|
|
446
|
+
console.log('Cache abierto');
|
|
447
|
+
return cache.addAll(urlsToCache);
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
self.addEventListener('fetch', event => {
|
|
453
|
+
event.respondWith(
|
|
454
|
+
caches.match(event.request)
|
|
455
|
+
.then(response => {
|
|
456
|
+
if (response) {
|
|
457
|
+
return response;
|
|
458
|
+
}
|
|
459
|
+
return fetch(event.request);
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
'''
|
|
464
|
+
sw_path = os.path.join(output_path, "sw.js")
|
|
465
|
+
with open(sw_path, 'w', encoding='utf-8') as f:
|
|
466
|
+
f.write(sw_content)
|
|
467
|
+
|
|
468
|
+
def _prepare_page_scripts(self, scripts, output_path: str, project_root: str):
|
|
469
|
+
"""
|
|
470
|
+
Toma la lista mixta `scripts` y:
|
|
471
|
+
- concatena todo el JS inline en un único string (combined_js)
|
|
472
|
+
- copia los file scripts al output_path y devuelve la lista de src relativos (external_srcs)
|
|
473
|
+
Compatibilidades:
|
|
474
|
+
- objetos con get_code()
|
|
475
|
+
- dicts {'type':'inline','code':...} o {'type':'file','path':...}
|
|
476
|
+
- objetos con attribute 'path' o 'src' (se interpretan como file script)
|
|
477
|
+
- strings -> treated as inline code
|
|
478
|
+
"""
|
|
479
|
+
combined_lines = []
|
|
480
|
+
external_srcs = [] # list of tuples (src, is_module)
|
|
481
|
+
combined_is_module = False
|
|
482
|
+
import shutil
|
|
483
|
+
|
|
484
|
+
for script in scripts or []:
|
|
485
|
+
# Instancia con get_code()
|
|
486
|
+
try:
|
|
487
|
+
if hasattr(script, 'get_code'):
|
|
488
|
+
code = script.get_code()
|
|
489
|
+
if code:
|
|
490
|
+
combined_lines.append(f"// Script: {getattr(script, '__class__', type(script)).__name__}\n{code.strip()}\n")
|
|
491
|
+
try:
|
|
492
|
+
if getattr(script, 'module', False):
|
|
493
|
+
combined_is_module = True
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
continue
|
|
497
|
+
except Exception:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
# Dict fallback
|
|
501
|
+
if isinstance(script, dict):
|
|
502
|
+
stype = script.get('type', '').lower()
|
|
503
|
+
is_module = bool(script.get('module'))
|
|
504
|
+
if stype == 'inline' or ('code' in script and not stype):
|
|
505
|
+
code = script.get('code') or script.get('value') or ''
|
|
506
|
+
if code:
|
|
507
|
+
combined_lines.append(f"// Inline dict script\n{code.strip()}\n")
|
|
508
|
+
if is_module:
|
|
509
|
+
combined_is_module = True
|
|
510
|
+
continue
|
|
511
|
+
if stype == 'file' or 'path' in script:
|
|
512
|
+
path = script.get('path') or script.get('src') or script.get('value')
|
|
513
|
+
if path:
|
|
514
|
+
# Resolver ruta relativa a project_root
|
|
515
|
+
src_path = os.path.join(project_root, path) if not os.path.isabs(path) else path
|
|
516
|
+
if os.path.isfile(src_path):
|
|
517
|
+
dest_name = os.path.basename(src_path)
|
|
518
|
+
dest_path = os.path.join(output_path, dest_name)
|
|
519
|
+
try:
|
|
520
|
+
shutil.copy2(src_path, dest_path)
|
|
521
|
+
external_srcs.append((dest_name, is_module))
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|
|
524
|
+
else:
|
|
525
|
+
# si no existe en disco, asumimos que es una URL o ya accesible: usar tal cual
|
|
526
|
+
external_srcs.append((path, is_module))
|
|
527
|
+
continue
|
|
528
|
+
# Otros dicts con code
|
|
529
|
+
if 'code' in script:
|
|
530
|
+
code = script.get('code')
|
|
531
|
+
if code:
|
|
532
|
+
combined_lines.append(f"// Inline dict script\n{code.strip()}\n")
|
|
533
|
+
if is_module:
|
|
534
|
+
combined_is_module = True
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
# String -> inline code
|
|
538
|
+
if isinstance(script, str):
|
|
539
|
+
combined_lines.append(f"// Inline string script\n{script.strip()}\n")
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
# Objetos con .path o .src (file scripts)
|
|
543
|
+
path_attr = None
|
|
544
|
+
for candidate in ('path', 'src', 'file'):
|
|
545
|
+
if hasattr(script, candidate):
|
|
546
|
+
try:
|
|
547
|
+
path_attr = getattr(script, candidate)
|
|
548
|
+
break
|
|
549
|
+
except Exception:
|
|
550
|
+
continue
|
|
551
|
+
if path_attr:
|
|
552
|
+
path = path_attr
|
|
553
|
+
src_path = os.path.join(project_root, path) if not os.path.isabs(path) else path
|
|
554
|
+
if os.path.isfile(src_path):
|
|
555
|
+
dest_name = os.path.basename(src_path)
|
|
556
|
+
dest_path = os.path.join(output_path, dest_name)
|
|
557
|
+
try:
|
|
558
|
+
shutil.copy2(src_path, dest_path)
|
|
559
|
+
is_module = False
|
|
560
|
+
try:
|
|
561
|
+
if getattr(script, 'module', False):
|
|
562
|
+
is_module = True
|
|
563
|
+
except Exception:
|
|
564
|
+
pass
|
|
565
|
+
external_srcs.append((dest_name, is_module))
|
|
566
|
+
except Exception:
|
|
567
|
+
pass
|
|
568
|
+
else:
|
|
569
|
+
is_module = False
|
|
570
|
+
try:
|
|
571
|
+
if getattr(script, 'module', False):
|
|
572
|
+
is_module = True
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
external_srcs.append((path, is_module))
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
# Si no sabemos qué es, intentar str() y añadir como inline (fallback)
|
|
579
|
+
try:
|
|
580
|
+
s = str(script)
|
|
581
|
+
if s:
|
|
582
|
+
combined_lines.append(f"// Fallback script: {type(script).__name__}\n{s}\n")
|
|
583
|
+
except Exception:
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
combined_js = "// Scripts específicos de esta página (combinados)\n" + "\n".join(combined_lines)
|
|
587
|
+
|
|
588
|
+
return combined_js, external_srcs, combined_is_module
|
|
589
|
+
def _generate_combined_script_js(self, scripts):
|
|
590
|
+
"""Deprecated internal wrapper: usa _prepare_page_scripts sin copiar archivos.
|
|
591
|
+
Conserva compatibilidad devolviendo solo el combined JS (sin external refs)."""
|
|
592
|
+
combined_js, external_srcs = self._prepare_page_scripts(scripts, output_path=os.getcwd(), project_root=os.getcwd())
|
|
593
|
+
return combined_js
|
|
594
|
+
|
|
595
|
+
def generate_html(self, app: App, css_file: str = "styles.css",
|
|
596
|
+
script_file: str = "script.js", runtime_file: str = "runtime_dars.js", extra_script_srcs: list = None, bundle: bool = False, vdom_script: str = "vdom_tree.js", script_is_module: bool = False) -> str:
|
|
597
|
+
"""Genera el contenido HTML con todas las propiedades de la aplicación"""
|
|
598
|
+
body_content = ""
|
|
599
|
+
from dars.components.basic.container import Container
|
|
600
|
+
root_component = app.root
|
|
601
|
+
# Protección: si root es lista, envolver en Container correctamente
|
|
602
|
+
if isinstance(root_component, list):
|
|
603
|
+
root_component = Container(*root_component)
|
|
604
|
+
if root_component:
|
|
605
|
+
body_content = self.render_component(root_component)
|
|
606
|
+
|
|
607
|
+
# VDOM snapshot ahora se sirve desde un archivo externo (vdom_script)
|
|
608
|
+
|
|
609
|
+
# Generar meta tags
|
|
610
|
+
meta_tags_html = self._generate_meta_tags(app)
|
|
611
|
+
|
|
612
|
+
# Generar links (favicon, manifest, etc.)
|
|
613
|
+
links_html = self._generate_links(app)
|
|
614
|
+
|
|
615
|
+
# Generar Open Graph tags
|
|
616
|
+
og_tags_html = self._generate_open_graph_tags(app)
|
|
617
|
+
|
|
618
|
+
# Generar Twitter Card tags
|
|
619
|
+
twitter_tags_html = self._generate_twitter_tags(app)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# Construir string de scripts externos (extra_script_srcs)
|
|
623
|
+
extra_scripts_html = ""
|
|
624
|
+
if extra_script_srcs:
|
|
625
|
+
for item in extra_script_srcs:
|
|
626
|
+
# item can be string (backward compat) or tuple (src, is_module)
|
|
627
|
+
if isinstance(item, tuple):
|
|
628
|
+
src, is_module = item
|
|
629
|
+
else:
|
|
630
|
+
src, is_module = item, False
|
|
631
|
+
type_attr = ' type="module"' if is_module else ''
|
|
632
|
+
extra_scripts_html += f' <script src="{src}"{type_attr}></script>\n'
|
|
633
|
+
# Incluir dars.min.js (ESM) antes de runtime/script
|
|
634
|
+
dars_lib_tag = '<script type="module" src="lib/dars.min.js" defer data-dars-lib></script>'
|
|
635
|
+
|
|
636
|
+
# State bootstrap: emit JSON (+ obfuscation in bundle) + module to register states
|
|
637
|
+
bootstrap_json_tag = ""
|
|
638
|
+
bootstrap_init_tag = ""
|
|
639
|
+
try:
|
|
640
|
+
from dars.core.state import STATE_BOOTSTRAP
|
|
641
|
+
if STATE_BOOTSTRAP:
|
|
642
|
+
import json as _json
|
|
643
|
+
from copy import deepcopy as _deepcopy
|
|
644
|
+
try:
|
|
645
|
+
from dars.scripts.script import Script as _Script
|
|
646
|
+
except Exception:
|
|
647
|
+
_Script = None
|
|
648
|
+
|
|
649
|
+
def _ser(v):
|
|
650
|
+
try:
|
|
651
|
+
if _Script and isinstance(v, _Script):
|
|
652
|
+
return {"code": v.get_code()}
|
|
653
|
+
if isinstance(v, dict):
|
|
654
|
+
return {k: _ser(val) for k, val in v.items()}
|
|
655
|
+
if isinstance(v, list):
|
|
656
|
+
return [_ser(x) for x in v]
|
|
657
|
+
return v
|
|
658
|
+
except Exception:
|
|
659
|
+
return v
|
|
660
|
+
|
|
661
|
+
_clean = _ser(_deepcopy(STATE_BOOTSTRAP))
|
|
662
|
+
bootstrap_json = _json.dumps(_clean, ensure_ascii=False)
|
|
663
|
+
if bundle:
|
|
664
|
+
# Obfuscate: base64-encode the bootstrap JSON
|
|
665
|
+
import base64 as _b64
|
|
666
|
+
_b64data = _b64.b64encode(bootstrap_json.encode('utf-8')).decode('ascii')
|
|
667
|
+
bootstrap_json_tag = f'<script type="application/octet-stream" id="dars-state-bootstrap-b64">{_b64data}</script>'
|
|
668
|
+
bootstrap_init_tag = (
|
|
669
|
+
"<script type=\"module\">\n"
|
|
670
|
+
"(async () => {\n"
|
|
671
|
+
" if (window.__DARS_STATE_BOOTSTRAPPED__) return;\n"
|
|
672
|
+
" const el = document.getElementById('dars-state-bootstrap-b64');\n"
|
|
673
|
+
" if (!el) { window.__DARS_STATE_BOOTSTRAPPED__ = true; return; }\n"
|
|
674
|
+
" let arr = [];\n"
|
|
675
|
+
" try {\n"
|
|
676
|
+
" const b64 = el.textContent || '';\n"
|
|
677
|
+
" let json = '';\n"
|
|
678
|
+
" if (typeof atob === 'function') json = atob(b64);\n"
|
|
679
|
+
" else if (typeof Buffer !== 'undefined') json = Buffer.from(b64, 'base64').toString('utf8');\n"
|
|
680
|
+
" arr = JSON.parse(json||'[]');\n"
|
|
681
|
+
" } catch(_) { arr = []; }\n"
|
|
682
|
+
" try {\n"
|
|
683
|
+
" const m = await import('./lib/dars.min.js');\n"
|
|
684
|
+
" const reg = m.registerState || (m.default && m.default.registerState);\n"
|
|
685
|
+
" if (typeof reg === 'function') { arr.forEach(s => reg(s.name, s)); }\n"
|
|
686
|
+
" } catch (e) {\n"
|
|
687
|
+
" const D = window.Dars; if (D && typeof D.registerState==='function') { arr.forEach(s => D.registerState(s.name, s)); }\n"
|
|
688
|
+
" }\n"
|
|
689
|
+
" window.__DARS_STATE_BOOTSTRAPPED__ = true;\n"
|
|
690
|
+
"})();\n"
|
|
691
|
+
"</script>"
|
|
692
|
+
)
|
|
693
|
+
else:
|
|
694
|
+
# Dev: keep readable JSON
|
|
695
|
+
bootstrap_json_tag = f'<script type="application/json" id="dars-state-bootstrap">{bootstrap_json}</script>'
|
|
696
|
+
bootstrap_init_tag = (
|
|
697
|
+
"<script type=\"module\">\n"
|
|
698
|
+
"(async () => {\n"
|
|
699
|
+
" if (window.__DARS_STATE_BOOTSTRAPPED__) return;\n"
|
|
700
|
+
" const el = document.getElementById('dars-state-bootstrap');\n"
|
|
701
|
+
" if (!el) { window.__DARS_STATE_BOOTSTRAPPED__ = true; return; }\n"
|
|
702
|
+
" const arr = JSON.parse(el.textContent||'[]');\n"
|
|
703
|
+
" try {\n"
|
|
704
|
+
" const m = await import('./lib/dars.min.js');\n"
|
|
705
|
+
" const reg = m.registerState || (m.default && m.default.registerState);\n"
|
|
706
|
+
" if (typeof reg === 'function') { arr.forEach(s => reg(s.name, s)); }\n"
|
|
707
|
+
" } catch (e) {\n"
|
|
708
|
+
" const D = window.Dars; if (D && typeof D.registerState==='function') { arr.forEach(s => D.registerState(s.name, s)); }\n"
|
|
709
|
+
" }\n"
|
|
710
|
+
" window.__DARS_STATE_BOOTSTRAPPED__ = true;\n"
|
|
711
|
+
"})();\n"
|
|
712
|
+
"</script>"
|
|
713
|
+
)
|
|
714
|
+
except Exception:
|
|
715
|
+
pass
|
|
716
|
+
|
|
717
|
+
# Derivar nombres para hot-reload incremental (opcional)
|
|
718
|
+
def _derive_snapshot_and_version(runtime_name: str):
|
|
719
|
+
if runtime_name == 'runtime_dars.js':
|
|
720
|
+
return ('snapshot.json', 'version.txt')
|
|
721
|
+
if runtime_name.startswith('runtime_dars_') and runtime_name.endswith('.js'):
|
|
722
|
+
slug = runtime_name[len('runtime_dars_'):-3]
|
|
723
|
+
return (f'snapshot_{slug}.json', f'version_{slug}.txt')
|
|
724
|
+
return ('snapshot.json', 'version.txt')
|
|
725
|
+
|
|
726
|
+
snapshot_name, version_name = _derive_snapshot_and_version(runtime_file)
|
|
727
|
+
# Incluir variables de hot-reload solo en modo dev (no bundle)
|
|
728
|
+
version_vars_html = ""
|
|
729
|
+
if not bundle:
|
|
730
|
+
version_vars_html = f"<script>window.__DARS_SNAPSHOT_URL = '{snapshot_name}'; window.__DARS_VERSION_URL = '{version_name}';</script>"
|
|
731
|
+
|
|
732
|
+
vdom_script_tag = ''
|
|
733
|
+
if vdom_script:
|
|
734
|
+
vdom_script_tag = f'<script src="{vdom_script}"></script>'
|
|
735
|
+
|
|
736
|
+
html_template = f"""<!DOCTYPE html>
|
|
737
|
+
<html lang="{app.language}">
|
|
738
|
+
<head>
|
|
739
|
+
<meta charset="{app.config.get('charset', 'UTF-8')}">
|
|
740
|
+
{meta_tags_html}
|
|
741
|
+
<title>{app.title}</title>
|
|
742
|
+
{links_html}
|
|
743
|
+
{og_tags_html}
|
|
744
|
+
{twitter_tags_html}
|
|
745
|
+
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css\">\n <link rel=\"stylesheet\" href=\"runtime_css.css\">\n <link rel=\"stylesheet\" href=\"{css_file}\">
|
|
746
|
+
</head>
|
|
747
|
+
<body>
|
|
748
|
+
{body_content}
|
|
749
|
+
{vdom_script_tag}
|
|
750
|
+
{version_vars_html}
|
|
751
|
+
{bootstrap_json_tag}
|
|
752
|
+
{dars_lib_tag}
|
|
753
|
+
{bootstrap_init_tag}
|
|
754
|
+
<script src=\"{runtime_file}\"{' type=\"module\"' if script_is_module else ''} defer></script>\n{extra_scripts_html} <script src=\"{script_file}\"{' type=\"module\"' if script_is_module else ''}></script>
|
|
755
|
+
</body>
|
|
756
|
+
</html>"""
|
|
757
|
+
|
|
758
|
+
return html_template
|
|
759
|
+
|
|
760
|
+
def _obfuscate_vdom(self, vnode: dict) -> dict:
|
|
761
|
+
"""Produce a minimal VDOM structure keeping events but hiding code.
|
|
762
|
+
- Keeps: type, id, key, class, text, children, events
|
|
763
|
+
- Events: { evName: {t:'i', b:'<base64>'} }
|
|
764
|
+
- Strips: style, props, any other keys
|
|
765
|
+
Recurses through children.
|
|
766
|
+
"""
|
|
767
|
+
if not isinstance(vnode, dict):
|
|
768
|
+
return vnode
|
|
769
|
+
import base64
|
|
770
|
+
kept = {}
|
|
771
|
+
# type (obfuscated when enabled)
|
|
772
|
+
t = vnode.get('type')
|
|
773
|
+
if t is not None:
|
|
774
|
+
kept['type'] = self._obf_type(t) if getattr(self, '_type_obfuscation', False) else t
|
|
775
|
+
# id and key
|
|
776
|
+
if 'id' in vnode and vnode['id']:
|
|
777
|
+
kept['id'] = self._hash_id(str(vnode['id'])) if getattr(self, '_hash_ids', False) else vnode['id']
|
|
778
|
+
if 'key' in vnode and vnode['key']:
|
|
779
|
+
kept['key'] = str(vnode['key'])
|
|
780
|
+
# class: drop in obfuscated VDOM to avoid leaking names
|
|
781
|
+
if not getattr(self, '_type_obfuscation', False):
|
|
782
|
+
if 'class' in vnode:
|
|
783
|
+
kept['class'] = vnode.get('class')
|
|
784
|
+
# text retained (non-sensitive content remains visible by choice)
|
|
785
|
+
if 'text' in vnode:
|
|
786
|
+
kept['text'] = vnode.get('text')
|
|
787
|
+
# Obfuscate events
|
|
788
|
+
evs = vnode.get('events') or None
|
|
789
|
+
if isinstance(evs, dict) and evs:
|
|
790
|
+
obf = {}
|
|
791
|
+
for ev, spec in evs.items():
|
|
792
|
+
try:
|
|
793
|
+
code = None
|
|
794
|
+
if isinstance(spec, dict):
|
|
795
|
+
# existing shapes: {type:'inline', code:'...'} or short-forms
|
|
796
|
+
code = spec.get('code') or spec.get('value')
|
|
797
|
+
elif isinstance(spec, str):
|
|
798
|
+
code = spec
|
|
799
|
+
if code:
|
|
800
|
+
b64 = base64.b64encode(code.encode('utf-8')).decode('ascii')
|
|
801
|
+
obf[ev] = {'t': 'i', 'b': b64}
|
|
802
|
+
except Exception:
|
|
803
|
+
# if anything fails, skip this event
|
|
804
|
+
pass
|
|
805
|
+
kept['events'] = obf if obf else None
|
|
806
|
+
# Recurse children
|
|
807
|
+
ch = vnode.get('children') or []
|
|
808
|
+
if ch:
|
|
809
|
+
kept['children'] = [self._obfuscate_vdom(c) for c in ch]
|
|
810
|
+
return kept
|
|
811
|
+
|
|
812
|
+
def _obf_type(self, name: str) -> str:
|
|
813
|
+
m = getattr(self, '_type_map', None)
|
|
814
|
+
if m is None:
|
|
815
|
+
self._type_map = {}
|
|
816
|
+
self._type_seq = 0
|
|
817
|
+
m = self._type_map
|
|
818
|
+
if name in m:
|
|
819
|
+
return m[name]
|
|
820
|
+
self._type_seq += 1
|
|
821
|
+
obf = f"T{self._type_seq}"
|
|
822
|
+
m[name] = obf
|
|
823
|
+
return obf
|
|
824
|
+
|
|
825
|
+
def generate_custom_css(self, app: App) -> str:
|
|
826
|
+
"""Genera solo los estilos personalizados de la aplicación"""
|
|
827
|
+
css_content = ""
|
|
828
|
+
|
|
829
|
+
# Agregar estilos globales de la aplicación definidos por el usuario
|
|
830
|
+
for selector, styles in app.global_styles.items():
|
|
831
|
+
css_content += f"{selector} {{\n"
|
|
832
|
+
css_content += f" {self.render_styles(styles)}\n"
|
|
833
|
+
css_content += "}\n\n"
|
|
834
|
+
|
|
835
|
+
# Agregar contenido de archivos CSS globales
|
|
836
|
+
for file_path in app.global_style_files:
|
|
837
|
+
try:
|
|
838
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
839
|
+
css_content += f.read() + "\n\n"
|
|
840
|
+
except Exception as e:
|
|
841
|
+
print(f"[Dars] Warning: could not read CSS file '{file_path}': {e}")
|
|
842
|
+
|
|
843
|
+
return css_content
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _generate_meta_tags(self, app: App) -> str:
|
|
847
|
+
"""Genera todos los meta tags de la aplicación"""
|
|
848
|
+
meta_tags = app.get_meta_tags()
|
|
849
|
+
meta_html = []
|
|
850
|
+
|
|
851
|
+
for name, content in meta_tags.items():
|
|
852
|
+
if content:
|
|
853
|
+
meta_html.append(f' <meta name="{name}" content="{content}">')
|
|
854
|
+
|
|
855
|
+
# Añadir canonical URL si está configurado
|
|
856
|
+
if app.canonical_url:
|
|
857
|
+
meta_html.append(f' <link rel="canonical" href="{app.canonical_url}">')
|
|
858
|
+
|
|
859
|
+
return '\n'.join(meta_html)
|
|
860
|
+
|
|
861
|
+
def _generate_links(self, app: App) -> str:
|
|
862
|
+
"""Genera los enlaces en el head del HTML"""
|
|
863
|
+
links = []
|
|
864
|
+
|
|
865
|
+
# Favicon
|
|
866
|
+
if hasattr(app, 'favicon'):
|
|
867
|
+
links.append(f'<link rel="icon" href="{app.favicon}" type="image/x-icon">')
|
|
868
|
+
|
|
869
|
+
# Manifest
|
|
870
|
+
if getattr(app, 'pwa_enabled', False):
|
|
871
|
+
links.append('<link rel="manifest" href="manifest.json">')
|
|
872
|
+
# Registrar service worker si está habilitado
|
|
873
|
+
if getattr(app, 'service_worker_enabled', True):
|
|
874
|
+
links.append("""
|
|
875
|
+
<script>
|
|
876
|
+
if ('serviceWorker' in navigator) {
|
|
877
|
+
window.addEventListener('load', () => {
|
|
878
|
+
navigator.serviceWorker.register('sw.js')
|
|
879
|
+
.then(registration => {
|
|
880
|
+
console.log('ServiceWorker registration successful');
|
|
881
|
+
})
|
|
882
|
+
.catch(err => {
|
|
883
|
+
console.log('ServiceWorker registration failed: ', err);
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
</script>
|
|
888
|
+
""")
|
|
889
|
+
return "\n ".join(links)
|
|
890
|
+
def generate_custom_css(self, app: App) -> str:
|
|
891
|
+
"""Genera solo los estilos personalizados de la aplicación"""
|
|
892
|
+
css_content = ""
|
|
893
|
+
|
|
894
|
+
# Agregar estilos globales de la aplicación definidos por el usuario
|
|
895
|
+
for selector, styles in app.global_styles.items():
|
|
896
|
+
css_content += f"{selector} {{\n"
|
|
897
|
+
css_content += f" {self.render_styles(styles)}\n"
|
|
898
|
+
css_content += "}\n\n"
|
|
899
|
+
|
|
900
|
+
# Agregar contenido de archivos CSS globales
|
|
901
|
+
for file_path in app.global_style_files:
|
|
902
|
+
try:
|
|
903
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
904
|
+
css_content += f.read() + "\n\n"
|
|
905
|
+
except Exception as e:
|
|
906
|
+
print(f"[Dars] Warning: could not read CSS file '{file_path}': {e}")
|
|
907
|
+
|
|
908
|
+
return css_content
|
|
909
|
+
|
|
910
|
+
def _generate_open_graph_tags(self, app: App) -> str:
|
|
911
|
+
"""Genera todos los tags Open Graph para redes sociales"""
|
|
912
|
+
og_tags = app.get_open_graph_tags()
|
|
913
|
+
og_html = []
|
|
914
|
+
|
|
915
|
+
for property_name, content in og_tags.items():
|
|
916
|
+
if content:
|
|
917
|
+
og_html.append(f' <meta property="{property_name}" content="{content}">')
|
|
918
|
+
|
|
919
|
+
return '\n'.join(og_html)
|
|
920
|
+
|
|
921
|
+
def _generate_twitter_tags(self, app: App) -> str:
|
|
922
|
+
"""Genera todos los tags de Twitter Card"""
|
|
923
|
+
twitter_tags = app.get_twitter_tags()
|
|
924
|
+
twitter_html = []
|
|
925
|
+
|
|
926
|
+
for name, content in twitter_tags.items():
|
|
927
|
+
if content:
|
|
928
|
+
twitter_html.append(f' <meta name="{name}" content="{content}">')
|
|
929
|
+
|
|
930
|
+
return '\n'.join(twitter_html)
|
|
931
|
+
|
|
932
|
+
def generate_base_css(self) -> str:
|
|
933
|
+
"""Genera el contenido CSS base"""
|
|
934
|
+
return """/* Estilos base de Dars */
|
|
935
|
+
* {
|
|
936
|
+
box-sizing: border-box;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
body {
|
|
940
|
+
margin: 0;
|
|
941
|
+
padding: 0;
|
|
942
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/* Estilos de componentes Dars */
|
|
946
|
+
.dars-container {
|
|
947
|
+
display: block;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.dars-text {
|
|
951
|
+
display: inline-block;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.dars-button {
|
|
955
|
+
display: inline-block;
|
|
956
|
+
padding: 8px 16px;
|
|
957
|
+
border: 1px solid #ccc;
|
|
958
|
+
background-color: #f8f9fa;
|
|
959
|
+
color: #333;
|
|
960
|
+
cursor: pointer;
|
|
961
|
+
border-radius: 4px;
|
|
962
|
+
font-size: 14px;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.dars-button:hover {
|
|
966
|
+
background-color: #e9ecef;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.dars-button:disabled {
|
|
970
|
+
opacity: 0.6;
|
|
971
|
+
cursor: not-allowed;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.dars-input {
|
|
975
|
+
display: inline-block;
|
|
976
|
+
padding: 8px 12px;
|
|
977
|
+
border: 1px solid #ccc;
|
|
978
|
+
border-radius: 4px;
|
|
979
|
+
font-size: 14px;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.dars-input:focus {
|
|
983
|
+
outline: none;
|
|
984
|
+
border-color: #007bff;
|
|
985
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.dars-image {
|
|
989
|
+
max-width: 100%;
|
|
990
|
+
height: auto;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.dars-link {
|
|
994
|
+
color: #007bff;
|
|
995
|
+
text-decoration: none;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.dars-link:hover {
|
|
999
|
+
text-decoration: underline;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.dars-textarea {
|
|
1003
|
+
width: 100%;
|
|
1004
|
+
padding: 8px 12px;
|
|
1005
|
+
border: 1px solid #ccc;
|
|
1006
|
+
border-radius: 4px;
|
|
1007
|
+
font-size: 14px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.dars-textarea:focus {
|
|
1011
|
+
outline: none;
|
|
1012
|
+
border-color: #007bff;
|
|
1013
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.dars-card {
|
|
1017
|
+
background-color: white;
|
|
1018
|
+
border-radius: 8px;
|
|
1019
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1020
|
+
padding: 20px;
|
|
1021
|
+
margin-bottom: 20px;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.dars-card h2 {
|
|
1025
|
+
margin-top: 0;
|
|
1026
|
+
margin-bottom: 15px;
|
|
1027
|
+
font-size: 24px;
|
|
1028
|
+
color: #333;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* Table */
|
|
1032
|
+
.dars-table {
|
|
1033
|
+
width: 100%;
|
|
1034
|
+
border-collapse: collapse;
|
|
1035
|
+
margin-bottom: 20px;
|
|
1036
|
+
background: white;
|
|
1037
|
+
}
|
|
1038
|
+
.dars-table th, .dars-table td {
|
|
1039
|
+
border: 1px solid #ddd;
|
|
1040
|
+
padding: 8px 12px;
|
|
1041
|
+
text-align: left;
|
|
1042
|
+
}
|
|
1043
|
+
.dars-table th {
|
|
1044
|
+
background: #f5f5f5;
|
|
1045
|
+
font-weight: bold;
|
|
1046
|
+
}
|
|
1047
|
+
.dars-table tr:nth-child(even) {
|
|
1048
|
+
background: #fafbfc;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/* Tabs */
|
|
1052
|
+
.dars-tabs {
|
|
1053
|
+
margin-bottom: 20px;
|
|
1054
|
+
}
|
|
1055
|
+
.dars-tabs-header {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
border-bottom: 2px solid #eee;
|
|
1058
|
+
margin-bottom: 10px;
|
|
1059
|
+
}
|
|
1060
|
+
.dars-tab {
|
|
1061
|
+
background: none;
|
|
1062
|
+
border: none;
|
|
1063
|
+
padding: 10px 20px;
|
|
1064
|
+
cursor: pointer;
|
|
1065
|
+
font-size: 16px;
|
|
1066
|
+
color: #555;
|
|
1067
|
+
border-bottom: 2px solid transparent;
|
|
1068
|
+
transition: border 0.2s, color 0.2s;
|
|
1069
|
+
}
|
|
1070
|
+
.dars-tab-active {
|
|
1071
|
+
color: #007bff;
|
|
1072
|
+
border-bottom: 2px solid #007bff;
|
|
1073
|
+
font-weight: bold;
|
|
1074
|
+
}
|
|
1075
|
+
.dars-tab-panel {
|
|
1076
|
+
display: none;
|
|
1077
|
+
padding: 16px 0;
|
|
1078
|
+
}
|
|
1079
|
+
.dars-tab-panel-active {
|
|
1080
|
+
display: block;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/* Accordion */
|
|
1084
|
+
.dars-accordion {
|
|
1085
|
+
border-radius: 8px;
|
|
1086
|
+
overflow: hidden;
|
|
1087
|
+
background: #fff;
|
|
1088
|
+
margin-bottom: 20px;
|
|
1089
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
1090
|
+
}
|
|
1091
|
+
.dars-accordion-section {
|
|
1092
|
+
border-bottom: 1px solid #eee;
|
|
1093
|
+
}
|
|
1094
|
+
.dars-accordion-title {
|
|
1095
|
+
padding: 14px 20px;
|
|
1096
|
+
background: #f7f7f7;
|
|
1097
|
+
cursor: pointer;
|
|
1098
|
+
font-weight: 500;
|
|
1099
|
+
transition: background 0.2s;
|
|
1100
|
+
}
|
|
1101
|
+
.dars-accordion-section.dars-accordion-open .dars-accordion-title {
|
|
1102
|
+
background: #e9ecef;
|
|
1103
|
+
}
|
|
1104
|
+
.dars-accordion-content {
|
|
1105
|
+
display: none;
|
|
1106
|
+
padding: 16px 20px;
|
|
1107
|
+
background: #fafbfc;
|
|
1108
|
+
}
|
|
1109
|
+
.dars-accordion-section.dars-accordion-open .dars-accordion-content {
|
|
1110
|
+
display: block;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/* ProgressBar */
|
|
1114
|
+
.dars-progressbar {
|
|
1115
|
+
width: 100%;
|
|
1116
|
+
background: #e9ecef;
|
|
1117
|
+
border-radius: 8px;
|
|
1118
|
+
overflow: hidden;
|
|
1119
|
+
height: 20px;
|
|
1120
|
+
margin-bottom: 20px;
|
|
1121
|
+
}
|
|
1122
|
+
.dars-progressbar-bar {
|
|
1123
|
+
height: 100%;
|
|
1124
|
+
background: linear-gradient(90deg, #007bff, #4a90e2);
|
|
1125
|
+
transition: width 0.3s;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/* Spinner */
|
|
1129
|
+
.dars-spinner {
|
|
1130
|
+
border: 4px solid #e9ecef;
|
|
1131
|
+
border-top: 4px solid #007bff;
|
|
1132
|
+
border-radius: 50%;
|
|
1133
|
+
width: 36px;
|
|
1134
|
+
height: 36px;
|
|
1135
|
+
animation: dars-spin 1s linear infinite;
|
|
1136
|
+
margin: 10px auto;
|
|
1137
|
+
}
|
|
1138
|
+
@keyframes dars-spin {
|
|
1139
|
+
0% { transform: rotate(0deg); }
|
|
1140
|
+
100% { transform: rotate(360deg); }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/* Tooltip */
|
|
1144
|
+
.dars-tooltip {
|
|
1145
|
+
position: relative;
|
|
1146
|
+
display: inline-block;
|
|
1147
|
+
cursor: pointer;
|
|
1148
|
+
}
|
|
1149
|
+
.dars-tooltip .dars-tooltip-text {
|
|
1150
|
+
visibility: hidden;
|
|
1151
|
+
width: max-content;
|
|
1152
|
+
background: #333;
|
|
1153
|
+
color: #fff;
|
|
1154
|
+
text-align: center;
|
|
1155
|
+
border-radius: 4px;
|
|
1156
|
+
padding: 6px 10px;
|
|
1157
|
+
position: absolute;
|
|
1158
|
+
z-index: 10;
|
|
1159
|
+
opacity: 0;
|
|
1160
|
+
transition: opacity 0.2s;
|
|
1161
|
+
font-size: 13px;
|
|
1162
|
+
pointer-events: none;
|
|
1163
|
+
}
|
|
1164
|
+
.dars-tooltip:hover .dars-tooltip-text,
|
|
1165
|
+
.dars-tooltip:focus .dars-tooltip-text {
|
|
1166
|
+
visibility: visible;
|
|
1167
|
+
opacity: 1;
|
|
1168
|
+
}
|
|
1169
|
+
.dars-tooltip-top .dars-tooltip-text {
|
|
1170
|
+
bottom: 125%;
|
|
1171
|
+
left: 50%;
|
|
1172
|
+
transform: translateX(-50%);
|
|
1173
|
+
margin-bottom: 6px;
|
|
1174
|
+
}
|
|
1175
|
+
.dars-tooltip-bottom .dars-tooltip-text {
|
|
1176
|
+
top: 125%;
|
|
1177
|
+
left: 50%;
|
|
1178
|
+
transform: translateX(-50%);
|
|
1179
|
+
margin-top: 6px;
|
|
1180
|
+
}
|
|
1181
|
+
.dars-tooltip-left .dars-tooltip-text {
|
|
1182
|
+
right: 125%;
|
|
1183
|
+
top: 50%;
|
|
1184
|
+
transform: translateY(-50%);
|
|
1185
|
+
margin-right: 6px;
|
|
1186
|
+
}
|
|
1187
|
+
.dars-tooltip-right .dars-tooltip-text {
|
|
1188
|
+
left: 125%;
|
|
1189
|
+
display: none; /* Hidden by default */
|
|
1190
|
+
position: fixed; /* Stay in place */
|
|
1191
|
+
z-index: 1; /* Sit on top */
|
|
1192
|
+
left: 0;
|
|
1193
|
+
top: 0;
|
|
1194
|
+
width: 100%; /* Full width */
|
|
1195
|
+
height: 100%; /* Full height */
|
|
1196
|
+
overflow: auto; /* Enable scroll if needed */
|
|
1197
|
+
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
|
1198
|
+
justify-content: center;
|
|
1199
|
+
align-items: center;
|
|
1200
|
+
}
|
|
1201
|
+
.dars-modal-hidden {
|
|
1202
|
+
display: none !important;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.dars-modal-content {
|
|
1206
|
+
background-color: #fefefe;
|
|
1207
|
+
margin: auto;
|
|
1208
|
+
padding: 20px;
|
|
1209
|
+
border: 1px solid #888;
|
|
1210
|
+
width: 80%;
|
|
1211
|
+
max-width: 500px;
|
|
1212
|
+
border-radius: 8px;
|
|
1213
|
+
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.dars-navbar {
|
|
1217
|
+
display: flex;
|
|
1218
|
+
justify-content: space-between;
|
|
1219
|
+
align-items: center;
|
|
1220
|
+
padding: 1rem;
|
|
1221
|
+
background-color: #f8f9fa;
|
|
1222
|
+
border-bottom: 1px solid #dee2e6;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.dars-navbar-brand {
|
|
1226
|
+
font-weight: bold;
|
|
1227
|
+
font-size: 1.25rem;
|
|
1228
|
+
color: #333;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.dars-navbar-nav {
|
|
1232
|
+
display: flex;
|
|
1233
|
+
gap: 1rem;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.dars-navbar-nav a {
|
|
1237
|
+
color: #007bff;
|
|
1238
|
+
text-decoration: none;
|
|
1239
|
+
padding: 0.5rem 1rem;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.dars-navbar-nav a:hover {
|
|
1243
|
+
background-color: #e9ecef;
|
|
1244
|
+
border-radius: 4px;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/* Estilos para nuevos componentes básicos */
|
|
1248
|
+
|
|
1249
|
+
/* Checkbox */
|
|
1250
|
+
.dars-checkbox-wrapper {
|
|
1251
|
+
display: flex;
|
|
1252
|
+
align-items: center;
|
|
1253
|
+
gap: 8px;
|
|
1254
|
+
margin: 4px 0;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.dars-checkbox {
|
|
1258
|
+
width: 16px;
|
|
1259
|
+
height: 16px;
|
|
1260
|
+
cursor: pointer;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.dars-checkbox:disabled {
|
|
1264
|
+
opacity: 0.6;
|
|
1265
|
+
cursor: not-allowed;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.dars-checkbox-wrapper label {
|
|
1269
|
+
cursor: pointer;
|
|
1270
|
+
user-select: none;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/* RadioButton */
|
|
1274
|
+
.dars-radio-wrapper {
|
|
1275
|
+
display: flex;
|
|
1276
|
+
align-items: center;
|
|
1277
|
+
gap: 8px;
|
|
1278
|
+
margin: 4px 0;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.dars-radio {
|
|
1282
|
+
width: 16px;
|
|
1283
|
+
height: 16px;
|
|
1284
|
+
cursor: pointer;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.dars-radio:disabled {
|
|
1288
|
+
opacity: 0.6;
|
|
1289
|
+
cursor: not-allowed;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.dars-radio-wrapper label {
|
|
1293
|
+
cursor: pointer;
|
|
1294
|
+
user-select: none;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* Select */
|
|
1298
|
+
.dars-select {
|
|
1299
|
+
display: inline-block;
|
|
1300
|
+
padding: 8px 12px;
|
|
1301
|
+
border: 1px solid #ccc;
|
|
1302
|
+
border-radius: 4px;
|
|
1303
|
+
font-size: 14px;
|
|
1304
|
+
background-color: white;
|
|
1305
|
+
cursor: pointer;
|
|
1306
|
+
min-width: 120px;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.dars-select:focus {
|
|
1310
|
+
outline: none;
|
|
1311
|
+
border-color: #007bff;
|
|
1312
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.dars-select:disabled {
|
|
1316
|
+
opacity: 0.6;
|
|
1317
|
+
cursor: not-allowed;
|
|
1318
|
+
background-color: #f8f9fa;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.dars-select option:disabled {
|
|
1322
|
+
color: #6c757d;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/* Slider */
|
|
1326
|
+
.dars-slider-wrapper {
|
|
1327
|
+
display: flex;
|
|
1328
|
+
align-items: center;
|
|
1329
|
+
gap: 12px;
|
|
1330
|
+
margin: 8px 0;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.dars-slider-wrapper.dars-slider-vertical {
|
|
1334
|
+
flex-direction: column;
|
|
1335
|
+
align-items: stretch;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.dars-slider {
|
|
1339
|
+
flex: 1;
|
|
1340
|
+
cursor: pointer;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.dars-slider-horizontal .dars-slider {
|
|
1344
|
+
width: 100%;
|
|
1345
|
+
height: 6px;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.dars-slider-vertical input[type="range"] {
|
|
1349
|
+
width: 8px;
|
|
1350
|
+
height: 160px;
|
|
1351
|
+
writing-mode: vertical-lr;
|
|
1352
|
+
direction: rtl;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
.dars-slider:disabled {
|
|
1356
|
+
opacity: 0.6;
|
|
1357
|
+
cursor: not-allowed;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.dars-slider-value {
|
|
1361
|
+
font-weight: bold;
|
|
1362
|
+
min-width: 40px;
|
|
1363
|
+
text-align: center;
|
|
1364
|
+
padding: 4px 8px;
|
|
1365
|
+
background-color: #f8f9fa;
|
|
1366
|
+
border-radius: 4px;
|
|
1367
|
+
font-size: 12px;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.dars-slider-wrapper label {
|
|
1371
|
+
font-weight: 500;
|
|
1372
|
+
margin-bottom: 4px;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* DatePicker */
|
|
1376
|
+
.dars-datepicker {
|
|
1377
|
+
display: inline-block;
|
|
1378
|
+
padding: 8px 12px;
|
|
1379
|
+
border: 1px solid #ccc;
|
|
1380
|
+
border-radius: 4px;
|
|
1381
|
+
font-size: 14px;
|
|
1382
|
+
background-color: white;
|
|
1383
|
+
cursor: pointer;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.dars-datepicker:focus {
|
|
1387
|
+
outline: none;
|
|
1388
|
+
border-color: #007bff;
|
|
1389
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.dars-datepicker:disabled {
|
|
1393
|
+
opacity: 0.6;
|
|
1394
|
+
cursor: not-allowed;
|
|
1395
|
+
background-color: #f8f9fa;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.dars-datepicker:readonly {
|
|
1399
|
+
background-color: #f8f9fa;
|
|
1400
|
+
cursor: default;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
.dars-datepicker-inline {
|
|
1404
|
+
display: inline-block;
|
|
1405
|
+
border: 1px solid #ccc;
|
|
1406
|
+
border-radius: 4px;
|
|
1407
|
+
padding: 12px;
|
|
1408
|
+
background-color: white;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.dars-datepicker-inline .dars-datepicker {
|
|
1412
|
+
border: none;
|
|
1413
|
+
padding: 0;
|
|
1414
|
+
}
|
|
1415
|
+
/* Markdown Styles */
|
|
1416
|
+
.dars-markdown {
|
|
1417
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
1418
|
+
line-height: 1.6;
|
|
1419
|
+
color: #333;
|
|
1420
|
+
background-color: #ffffff;
|
|
1421
|
+
padding: 20px;
|
|
1422
|
+
border-radius: 8px;
|
|
1423
|
+
transition: all 0.3s ease;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/* Dark Theme */
|
|
1427
|
+
.dars-markdown-dark {
|
|
1428
|
+
color: #e0e0e0;
|
|
1429
|
+
background-color: #1e1e1e;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
.dars-markdown h1,
|
|
1433
|
+
.dars-markdown h2,
|
|
1434
|
+
.dars-markdown h3,
|
|
1435
|
+
.dars-markdown h4,
|
|
1436
|
+
.dars-markdown h5,
|
|
1437
|
+
.dars-markdown h6 {
|
|
1438
|
+
margin-top: 1.5em;
|
|
1439
|
+
margin-bottom: 0.5em;
|
|
1440
|
+
font-weight: 600;
|
|
1441
|
+
line-height: 1.25;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.dars-markdown-dark h1,
|
|
1445
|
+
.dars-markdown-dark h2,
|
|
1446
|
+
.dars-markdown-dark h3,
|
|
1447
|
+
.dars-markdown-dark h4,
|
|
1448
|
+
.dars-markdown-dark h5,
|
|
1449
|
+
.dars-markdown-dark h6 {
|
|
1450
|
+
color: #ffffff;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.dars-markdown h1 { font-size: 2em; }
|
|
1454
|
+
.dars-markdown h2 { font-size: 1.5em; }
|
|
1455
|
+
.dars-markdown h3 { font-size: 1.25em; }
|
|
1456
|
+
.dars-markdown h4 { font-size: 1em; }
|
|
1457
|
+
.dars-markdown h5 { font-size: 0.875em; }
|
|
1458
|
+
.dars-markdown h6 { font-size: 0.85em; }
|
|
1459
|
+
|
|
1460
|
+
.dars-markdown p {
|
|
1461
|
+
margin-bottom: 1em;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.dars-markdown-dark p {
|
|
1465
|
+
color: #cccccc;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.dars-markdown strong {
|
|
1469
|
+
font-weight: 600;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.dars-markdown em {
|
|
1473
|
+
font-style: italic;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
.dars-markdown ul,
|
|
1477
|
+
.dars-markdown ol {
|
|
1478
|
+
margin-bottom: 1em;
|
|
1479
|
+
padding-left: 2em;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.dars-markdown-dark ul,
|
|
1483
|
+
.dars-markdown-dark ol {
|
|
1484
|
+
color: #cccccc;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.dars-markdown li {
|
|
1488
|
+
margin-bottom: 0.5em;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
.dars-markdown code {
|
|
1492
|
+
background-color: #f6f8fa;
|
|
1493
|
+
padding: 0.2em 0.4em;
|
|
1494
|
+
border-radius: 3px;
|
|
1495
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
1496
|
+
font-size: 0.85em;
|
|
1497
|
+
color: #333;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.dars-markdown-dark code {
|
|
1501
|
+
background-color: #2d2d2d;
|
|
1502
|
+
color: #e0e0e0;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.dars-markdown pre {
|
|
1506
|
+
background-color: #f6f8fa;
|
|
1507
|
+
padding: 1em;
|
|
1508
|
+
border-radius: 3px;
|
|
1509
|
+
overflow: auto;
|
|
1510
|
+
margin-bottom: 1em;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.dars-markdown-dark pre {
|
|
1514
|
+
background-color: #2d2d2d;
|
|
1515
|
+
border: 1px solid #404040;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
.dars-markdown pre code {
|
|
1519
|
+
background: none;
|
|
1520
|
+
padding: 0;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
.dars-markdown blockquote {
|
|
1524
|
+
border-left: 4px solid #ddd;
|
|
1525
|
+
padding-left: 1em;
|
|
1526
|
+
margin-left: 0;
|
|
1527
|
+
color: #666;
|
|
1528
|
+
font-style: italic;
|
|
1529
|
+
background-color: #f9f9f9;
|
|
1530
|
+
padding: 10px 15px;
|
|
1531
|
+
border-radius: 4px;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
.dars-markdown-dark blockquote {
|
|
1535
|
+
border-left-color: #555;
|
|
1536
|
+
color: #bbb;
|
|
1537
|
+
background-color: #2a2a2a;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
.dars-markdown table {
|
|
1541
|
+
border-collapse: collapse;
|
|
1542
|
+
width: 100%;
|
|
1543
|
+
margin-bottom: 1em;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
.dars-markdown-dark table {
|
|
1547
|
+
border-color: #444;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.dars-markdown th,
|
|
1551
|
+
.dars-markdown td {
|
|
1552
|
+
border: 1px solid #ddd;
|
|
1553
|
+
padding: 0.5em;
|
|
1554
|
+
text-align: left;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.dars-markdown-dark th,
|
|
1558
|
+
.dars-markdown-dark td {
|
|
1559
|
+
border-color: #444;
|
|
1560
|
+
color: #e0e0e0;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
.dars-markdown th {
|
|
1564
|
+
background-color: #f6f8fa;
|
|
1565
|
+
font-weight: 600;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.dars-markdown-dark th {
|
|
1569
|
+
background-color: #333;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
.dars-markdown a {
|
|
1573
|
+
color: #0366d6;
|
|
1574
|
+
text-decoration: none;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
.dars-markdown-dark a {
|
|
1578
|
+
color: #4da6ff;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
.dars-markdown a:hover {
|
|
1582
|
+
text-decoration: underline;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
.dars-markdown-dark a:hover {
|
|
1586
|
+
color: #66b3ff;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.dars-markdown img {
|
|
1590
|
+
max-width: 100%;
|
|
1591
|
+
height: auto;
|
|
1592
|
+
border-radius: 4px;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.dars-markdown-dark img {
|
|
1596
|
+
filter: brightness(0.9);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/* Horizontal Rule */
|
|
1600
|
+
.dars-markdown hr {
|
|
1601
|
+
border: none;
|
|
1602
|
+
height: 1px;
|
|
1603
|
+
background-color: #ddd;
|
|
1604
|
+
margin: 2em 0;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.dars-markdown-dark hr {
|
|
1608
|
+
background-color: #444;
|
|
1609
|
+
}
|
|
1610
|
+
"""
|
|
1611
|
+
|
|
1612
|
+
def build_vdom_tree(self, component: Component) -> dict:
|
|
1613
|
+
"""Serializa un componente Dars a un VNode (snapshot VDOM para hidratación)."""
|
|
1614
|
+
try:
|
|
1615
|
+
comp_type = component.__class__.__name__
|
|
1616
|
+
except Exception:
|
|
1617
|
+
comp_type = 'Component'
|
|
1618
|
+
|
|
1619
|
+
comp_id = self.get_component_id(component)
|
|
1620
|
+
|
|
1621
|
+
# Serializar eventos (solo inline ejecutable en cliente)
|
|
1622
|
+
events_payload = {}
|
|
1623
|
+
try:
|
|
1624
|
+
events = getattr(component, 'events', {}) or {}
|
|
1625
|
+
for ev_name, handler in events.items():
|
|
1626
|
+
code = None
|
|
1627
|
+
try:
|
|
1628
|
+
if hasattr(handler, 'get_code'):
|
|
1629
|
+
code = handler.get_code()
|
|
1630
|
+
elif hasattr(handler, 'code'):
|
|
1631
|
+
code = getattr(handler, 'code')
|
|
1632
|
+
elif hasattr(handler, 'to_js'):
|
|
1633
|
+
code = handler.to_js()
|
|
1634
|
+
elif isinstance(handler, str):
|
|
1635
|
+
code = handler
|
|
1636
|
+
except Exception:
|
|
1637
|
+
code = None
|
|
1638
|
+
if code:
|
|
1639
|
+
try:
|
|
1640
|
+
code_str = str(code)
|
|
1641
|
+
except Exception:
|
|
1642
|
+
code_str = ''
|
|
1643
|
+
if code_str:
|
|
1644
|
+
events_payload[ev_name] = { 'type': 'inline', 'code': code_str }
|
|
1645
|
+
except Exception:
|
|
1646
|
+
events_payload = {}
|
|
1647
|
+
|
|
1648
|
+
# Props seguros (evitar funciones y objetos no serializables)
|
|
1649
|
+
safe_props = {}
|
|
1650
|
+
try:
|
|
1651
|
+
for k, v in (getattr(component, 'props', {}) or {}).items():
|
|
1652
|
+
if callable(v):
|
|
1653
|
+
continue
|
|
1654
|
+
if isinstance(v, (str, int, float, bool)) or v is None:
|
|
1655
|
+
safe_props[k] = v
|
|
1656
|
+
except Exception:
|
|
1657
|
+
pass
|
|
1658
|
+
|
|
1659
|
+
# Soporte para componentes de texto
|
|
1660
|
+
text_value = None
|
|
1661
|
+
try:
|
|
1662
|
+
if comp_type == 'Text' and hasattr(component, 'text'):
|
|
1663
|
+
text_value = component.text
|
|
1664
|
+
except Exception:
|
|
1665
|
+
pass
|
|
1666
|
+
|
|
1667
|
+
# Hijos
|
|
1668
|
+
children_nodes = []
|
|
1669
|
+
try:
|
|
1670
|
+
for child in getattr(component, 'children', []) or []:
|
|
1671
|
+
if child is None:
|
|
1672
|
+
continue
|
|
1673
|
+
children_nodes.append(self.build_vdom_tree(child))
|
|
1674
|
+
except Exception:
|
|
1675
|
+
children_nodes = []
|
|
1676
|
+
|
|
1677
|
+
vnode = {
|
|
1678
|
+
'type': comp_type,
|
|
1679
|
+
'id': comp_id,
|
|
1680
|
+
'key': getattr(component, 'key', None),
|
|
1681
|
+
'class': getattr(component, 'class_name', None),
|
|
1682
|
+
'style': getattr(component, 'style', {}) or {},
|
|
1683
|
+
'props': safe_props,
|
|
1684
|
+
'events': events_payload if events_payload else None,
|
|
1685
|
+
'children': children_nodes if children_nodes else []
|
|
1686
|
+
}
|
|
1687
|
+
if text_value is not None:
|
|
1688
|
+
vnode['text'] = text_value
|
|
1689
|
+
return vnode
|
|
1690
|
+
|
|
1691
|
+
def generate_vdom_snapshot(self, root_component: Component) -> str:
|
|
1692
|
+
"""Genera el snapshot VDOM (JSON) a partir del componente raíz.
|
|
1693
|
+
Usa VDomBuilder para mantener consistencia con el vdom_tree.js externo.
|
|
1694
|
+
"""
|
|
1695
|
+
import json
|
|
1696
|
+
try:
|
|
1697
|
+
vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(root_component)
|
|
1698
|
+
except Exception:
|
|
1699
|
+
vdom_dict = {'type': 'Root', 'id': None, 'children': []}
|
|
1700
|
+
return json.dumps(vdom_dict, ensure_ascii=False)
|
|
1701
|
+
|
|
1702
|
+
def generate_javascript(self, app: App, page_root: Component) -> str:
|
|
1703
|
+
"""Genera un runtime modular: hidratación + delegación de eventos + diff/patch + hot-reload incremental (polling)."""
|
|
1704
|
+
runtime = r"""// Dars Runtime (Hydration + Delegated Events + Diff/Patch + Hot Reload)
|
|
1705
|
+
(function(){
|
|
1706
|
+
const eventMap = new Map(); // id -> {ev: fn}
|
|
1707
|
+
let currentSnapshot = null;
|
|
1708
|
+
let currentVersion = null;
|
|
1709
|
+
|
|
1710
|
+
// Registro de componentes (skeleton). En siguientes iteraciones añadiremos create/patch por tipo built-in
|
|
1711
|
+
const registry = {
|
|
1712
|
+
// Implementación mínima segura para crear nodos cuando se agregan hijos
|
|
1713
|
+
'Text': {
|
|
1714
|
+
create(v){
|
|
1715
|
+
if(!v || v.isIsland) return null;
|
|
1716
|
+
const el = document.createElement('span');
|
|
1717
|
+
if(v.id) el.id = v.id;
|
|
1718
|
+
if(v.class) el.className = v.class;
|
|
1719
|
+
if(v.style){ for(const k in v.style){ try{ el.style.setProperty(k.replace(/_/g,'-'), String(v.style[k])); }catch{} } }
|
|
1720
|
+
if(Object.prototype.hasOwnProperty.call(v,'text')){ el.textContent = String(v.text||''); }
|
|
1721
|
+
// props
|
|
1722
|
+
if(v.props){ for(const k in v.props){ const val=v.props[k]; try{ if(val===false||val===null||typeof val==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(val)); } }catch{} } }
|
|
1723
|
+
return el;
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
'Container': {
|
|
1727
|
+
create(v){
|
|
1728
|
+
if(!v || v.isIsland) return null;
|
|
1729
|
+
const el = document.createElement('div');
|
|
1730
|
+
if(v.id) el.id = v.id;
|
|
1731
|
+
const base = 'dars-container';
|
|
1732
|
+
el.className = (v.class ? (base + ' ' + v.class) : base);
|
|
1733
|
+
if(v.style){ for(const k in v.style){ try{ el.style.setProperty(k.replace(/_/g,'-'), String(v.style[k])); }catch{} } }
|
|
1734
|
+
// props
|
|
1735
|
+
if(v.props){ for(const k in v.props){ const val=v.props[k]; try{ if(val===false||val===null||typeof val==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(val)); } }catch{} } }
|
|
1736
|
+
return el;
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
function walk(v, fn){
|
|
1742
|
+
if(!v) return;
|
|
1743
|
+
fn(v);
|
|
1744
|
+
const ch = v.children || [];
|
|
1745
|
+
for(let i=0;i<ch.length;i++){ walk(ch[i], fn); }
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function _decodeCodeB64(b64){
|
|
1749
|
+
try {
|
|
1750
|
+
if (typeof atob === 'function') return atob(b64);
|
|
1751
|
+
if (typeof Buffer !== 'undefined') { return Buffer.from(b64, 'base64').toString('utf8'); }
|
|
1752
|
+
} catch(_){ }
|
|
1753
|
+
return '';
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function _compileHandlerFromSpec(spec){
|
|
1757
|
+
try {
|
|
1758
|
+
if (!spec) return null;
|
|
1759
|
+
if (spec && spec.type === 'inline' && spec.code) {
|
|
1760
|
+
return new Function('event', spec.code);
|
|
1761
|
+
}
|
|
1762
|
+
const b64 = (spec && (spec.b || spec.code_b64)) || null;
|
|
1763
|
+
if (b64){
|
|
1764
|
+
const code = _decodeCodeB64(b64);
|
|
1765
|
+
if (code) return new Function('event', code);
|
|
1766
|
+
}
|
|
1767
|
+
} catch(_){ }
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function bindEventsFromVNode(snapshot){
|
|
1772
|
+
// Construir tabla de eventos a partir del snapshot
|
|
1773
|
+
walk(snapshot, (v)=>{
|
|
1774
|
+
if(v && v.id && v.events){
|
|
1775
|
+
const handlers = {};
|
|
1776
|
+
for(const ev in v.events){
|
|
1777
|
+
const spec = v.events[ev];
|
|
1778
|
+
const fn = _compileHandlerFromSpec(spec);
|
|
1779
|
+
if (fn) { handlers[ev] = fn; }
|
|
1780
|
+
}
|
|
1781
|
+
if(Object.keys(handlers).length){ eventMap.set(v.id, handlers); } else { eventMap.delete(v.id); }
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// Utilities
|
|
1787
|
+
function setProps(el, props){
|
|
1788
|
+
if(!el || !props) return;
|
|
1789
|
+
for(const [k,v] of Object.entries(props)){
|
|
1790
|
+
try {
|
|
1791
|
+
if(v === false || v === null || typeof v === 'undefined'){
|
|
1792
|
+
el.removeAttribute(k);
|
|
1793
|
+
} else {
|
|
1794
|
+
el.setAttribute(k, String(v));
|
|
1795
|
+
}
|
|
1796
|
+
} catch(err) { /* ignore */ }
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function diffProps(el, oldP={}, newP={}){
|
|
1800
|
+
// remove
|
|
1801
|
+
for(const k in oldP){ if(!(k in newP)){ try{ el.removeAttribute(k); }catch{} } }
|
|
1802
|
+
// add/update
|
|
1803
|
+
for(const k in newP){ const v=newP[k]; try{ if(v===false||v===null||typeof v==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(v)); } }catch{} }
|
|
1804
|
+
}
|
|
1805
|
+
function diffStyles(el, oldS={}, newS={}){
|
|
1806
|
+
for(const k in oldS){ if(!(k in newS)){ try{ el.style.removeProperty(k.replace(/_/g,'-')); }catch{} } }
|
|
1807
|
+
for(const k in newS){ const v=newS[k]; try{ el.style.setProperty(k.replace(/_/g,'-'), String(v)); }catch{} }
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Event delegation helper (restored)
|
|
1811
|
+
function delegate(eventName, root){
|
|
1812
|
+
(root||document).addEventListener(eventName, function(e){
|
|
1813
|
+
let node = e.target;
|
|
1814
|
+
const boundary = root||document;
|
|
1815
|
+
while(node && node !== boundary){
|
|
1816
|
+
const id = node.id;
|
|
1817
|
+
if(id && eventMap.has(id)){
|
|
1818
|
+
const handlers = eventMap.get(id);
|
|
1819
|
+
const h = handlers[eventName];
|
|
1820
|
+
// If there is a dynamic handler attached on this node for the same event, let it handle and skip default
|
|
1821
|
+
if(node && node.__darsEv && node.__darsEv[eventName]){
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
if(typeof h === 'function'){
|
|
1825
|
+
try { h.call(node, e); } catch(err){ console.error('[Dars] handler error', err); }
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
node = node.parentNode;
|
|
1830
|
+
}
|
|
1831
|
+
}, true);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function typesDiffer(a,b){ return (a && b) ? a.type !== b.type : a!==b; }
|
|
1835
|
+
|
|
1836
|
+
// Elimina un subárbol del DOM (y del mapa de eventos) usando los ids del VDOM
|
|
1837
|
+
function removeSubtree(v){
|
|
1838
|
+
if(!v) return;
|
|
1839
|
+
// eliminar hijos primero (postorden)
|
|
1840
|
+
const ch = (v.children||[]);
|
|
1841
|
+
for(let i=0;i<ch.length;i++){ removeSubtree(ch[i]); }
|
|
1842
|
+
// limpiar handlers
|
|
1843
|
+
if(v.id){ eventMap.delete(v.id); }
|
|
1844
|
+
// quitar elemento del DOM
|
|
1845
|
+
if(v.id){ const el = document.getElementById(v.id); if(el && el.parentNode){ try{ el.parentNode.removeChild(el); }catch(_){} }}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function updateNode(oldV, newV){
|
|
1849
|
+
if(!newV || !newV.id){ return { ok:false, reason:'missing-new' }; }
|
|
1850
|
+
let el = document.getElementById(newV.id);
|
|
1851
|
+
if(!el){
|
|
1852
|
+
// Fallback: si cambió el id entre snapshots pero es el mismo nodo lógico, reasignamos id
|
|
1853
|
+
const oldEl = (oldV && oldV.id) ? document.getElementById(oldV.id) : null;
|
|
1854
|
+
if(oldEl){ try { oldEl.id = newV.id; el = oldEl; } catch(_){} }
|
|
1855
|
+
}
|
|
1856
|
+
if(!el){ return { ok:false, reason:'missing-el' }; }
|
|
1857
|
+
|
|
1858
|
+
// Si cambia el tipo, estructura u orden de hijos, pedimos reload completo (fase 2 simplificada)
|
|
1859
|
+
if(typesDiffer(oldV, newV)){
|
|
1860
|
+
return { ok:false, reason:'type-changed' };
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
const isIsland = !!newV.isIsland;
|
|
1864
|
+
|
|
1865
|
+
// class -> atributo className
|
|
1866
|
+
if(!isIsland && newV.class){ el.className = newV.class; }
|
|
1867
|
+
|
|
1868
|
+
// props
|
|
1869
|
+
if(!isIsland){ diffProps(el, (oldV&&oldV.props)||{}, newV.props||{}); }
|
|
1870
|
+
|
|
1871
|
+
// styles
|
|
1872
|
+
if(!isIsland){ diffStyles(el, (oldV&&oldV.style)||{}, newV.style||{}); }
|
|
1873
|
+
|
|
1874
|
+
// text
|
|
1875
|
+
if(!isIsland && Object.prototype.hasOwnProperty.call(newV, 'text')){
|
|
1876
|
+
if(el.textContent !== String(newV.text||'')){
|
|
1877
|
+
el.textContent = String(newV.text||'');
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// events
|
|
1882
|
+
if(newV.events){
|
|
1883
|
+
const handlers = {};
|
|
1884
|
+
for(const ev in newV.events){
|
|
1885
|
+
const spec = newV.events[ev];
|
|
1886
|
+
const fn = _compileHandlerFromSpec(spec);
|
|
1887
|
+
if (fn) { handlers[ev] = fn; }
|
|
1888
|
+
}
|
|
1889
|
+
if(Object.keys(handlers).length){ eventMap.set(newV.id, handlers); } else { eventMap.delete(newV.id); }
|
|
1890
|
+
} else {
|
|
1891
|
+
eventMap.delete(newV.id);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// hijos (reconciliación por id/key). Para islas, tratamos el subárbol como opaco.
|
|
1895
|
+
if(isIsland){ return { ok:true }; }
|
|
1896
|
+
|
|
1897
|
+
// Permitimos REMOCIONES sin recarga.
|
|
1898
|
+
const oldC = (oldV && oldV.children) ? oldV.children : [];
|
|
1899
|
+
const newC = (newV.children) ? newV.children : [];
|
|
1900
|
+
|
|
1901
|
+
// Construir índice de hijos viejos por id o key
|
|
1902
|
+
const oldIndex = new Map(); // clave -> vnode viejo
|
|
1903
|
+
for(let i=0;i<oldC.length;i++){
|
|
1904
|
+
const k = (oldC[i] && (oldC[i].id || oldC[i].key)) || null;
|
|
1905
|
+
if(k){ oldIndex.set(String(k), oldC[i]); }
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// Seguimiento de cuáles viejos fueron actualizados
|
|
1909
|
+
const seenOld = new Set();
|
|
1910
|
+
|
|
1911
|
+
// Actualizar/validar hijos nuevos
|
|
1912
|
+
for(let i=0;i<newC.length;i++){
|
|
1913
|
+
const newChild = newC[i];
|
|
1914
|
+
const k = (newChild && (newChild.id || newChild.key)) || null;
|
|
1915
|
+
if(!k){
|
|
1916
|
+
// sin id/key fiable: conservador => usar reconciliación por índice si existe par
|
|
1917
|
+
if(i < oldC.length){
|
|
1918
|
+
const r = updateNode(oldC[i], newChild);
|
|
1919
|
+
if(!r.ok){ return r; }
|
|
1920
|
+
seenOld.add(oldC[i]);
|
|
1921
|
+
continue;
|
|
1922
|
+
} else {
|
|
1923
|
+
// no podemos crear de forma segura
|
|
1924
|
+
return { ok:false, reason:'children-added' };
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const oldChild = oldIndex.get(String(k));
|
|
1928
|
+
if(oldChild){
|
|
1929
|
+
const r = updateNode(oldChild, newChild);
|
|
1930
|
+
if(!r.ok){ return r; }
|
|
1931
|
+
seenOld.add(oldChild);
|
|
1932
|
+
} else {
|
|
1933
|
+
// Fallback conservador: si hay viejo en la misma posición y el tipo coincide, lo reutilizamos
|
|
1934
|
+
if(i < oldC.length){
|
|
1935
|
+
const candidate = oldC[i];
|
|
1936
|
+
if(!typesDiffer(candidate, newChild)){
|
|
1937
|
+
const r = updateNode(candidate, newChild);
|
|
1938
|
+
if(!r.ok){ return r; }
|
|
1939
|
+
seenOld.add(candidate);
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
// Intentar crear subárbol si es un tipo soportado por el registry (no isla)
|
|
1944
|
+
const subtree = createSubtree(newChild);
|
|
1945
|
+
if(subtree){
|
|
1946
|
+
// insertar en la posición i dentro del DOM
|
|
1947
|
+
const refChildVNode = (i < oldC.length) ? oldC[i] : null;
|
|
1948
|
+
if(refChildVNode && refChildVNode.id){
|
|
1949
|
+
const refEl = document.getElementById(refChildVNode.id);
|
|
1950
|
+
if(refEl && refEl.parentNode){ refEl.parentNode.insertBefore(subtree, refEl); }
|
|
1951
|
+
else { el.appendChild(subtree); }
|
|
1952
|
+
} else {
|
|
1953
|
+
el.appendChild(subtree);
|
|
1954
|
+
}
|
|
1955
|
+
// marcar como visto (no había old), nada que añadir a seenOld
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
// hijo nuevo de tipo no soportado => recarga por seguridad
|
|
1959
|
+
return { ok:false, reason:'children-added' };
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Eliminar los viejos no vistos (removidos)
|
|
1964
|
+
for(let i=0;i<oldC.length;i++){
|
|
1965
|
+
const v = oldC[i];
|
|
1966
|
+
if(!seenOld.has(v)){
|
|
1967
|
+
removeSubtree(v);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return { ok:true };
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function schedule(fn){
|
|
1974
|
+
if(typeof requestAnimationFrame === 'function'){
|
|
1975
|
+
requestAnimationFrame(fn);
|
|
1976
|
+
} else { setTimeout(fn, 16); }
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function update(newSnapshot){
|
|
1980
|
+
const old = currentSnapshot;
|
|
1981
|
+
if(!old){
|
|
1982
|
+
// primera vez: solo (re)hidratar eventos
|
|
1983
|
+
bindEventsFromVNode(newSnapshot);
|
|
1984
|
+
currentSnapshot = newSnapshot;
|
|
1985
|
+
try{ window.__DARS_VDOM__ = newSnapshot; }catch(_){ /* ignore */ }
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
schedule(()=>{
|
|
1989
|
+
const res = updateNode(old, newSnapshot);
|
|
1990
|
+
if(!res.ok){
|
|
1991
|
+
console.warn('[Dars] Structural change detected (', res.reason, '), reloading...');
|
|
1992
|
+
try { location.reload(); } catch(e) { /* ignore */ }
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
// Re-vincular mapa de eventos por si cambió
|
|
1996
|
+
bindEventsFromVNode(newSnapshot);
|
|
1997
|
+
currentSnapshot = newSnapshot;
|
|
1998
|
+
try{ window.__DARS_VDOM__ = newSnapshot; }catch(_){ /* ignore */ }
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function hydrate(snapshot){
|
|
2003
|
+
bindEventsFromVNode(snapshot);
|
|
2004
|
+
currentSnapshot = snapshot;
|
|
2005
|
+
try{ window.__DARS_VDOM__ = snapshot; }catch(_){ /* ignore */ }
|
|
2006
|
+
|
|
2007
|
+
// Delegar eventos comunes (extendido)
|
|
2008
|
+
const delegated = [
|
|
2009
|
+
'click','dblclick',
|
|
2010
|
+
'mousedown','mouseup','mouseenter','mouseleave','mousemove',
|
|
2011
|
+
'keydown','keyup','keypress',
|
|
2012
|
+
'change','input','submit',
|
|
2013
|
+
'focus','blur'
|
|
2014
|
+
];
|
|
2015
|
+
delegated.forEach(ev => delegate(ev, document));
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function startHotReload(){
|
|
2019
|
+
const vurl = (window.__DARS_VERSION_URL || 'version.txt');
|
|
2020
|
+
let timer = null;
|
|
2021
|
+
let warnedVersionMissing = false;
|
|
2022
|
+
|
|
2023
|
+
function httpGet(url, onSuccess, onError, responseType){
|
|
2024
|
+
try{
|
|
2025
|
+
const xhr = new XMLHttpRequest();
|
|
2026
|
+
if(responseType){ xhr.responseType = responseType; }
|
|
2027
|
+
xhr.open('GET', url, true);
|
|
2028
|
+
xhr.timeout = 5000;
|
|
2029
|
+
xhr.onreadystatechange = function(){
|
|
2030
|
+
if(xhr.readyState === 4){
|
|
2031
|
+
if(xhr.status >= 200 && xhr.status < 300){
|
|
2032
|
+
onSuccess(xhr.response);
|
|
2033
|
+
} else {
|
|
2034
|
+
onError();
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
xhr.onerror = onError;
|
|
2039
|
+
xhr.ontimeout = onError;
|
|
2040
|
+
xhr.setRequestHeader('Cache-Control', 'no-store');
|
|
2041
|
+
xhr.send();
|
|
2042
|
+
}catch(e){ onError(); }
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function tick(){
|
|
2046
|
+
httpGet(vurl, function(text){
|
|
2047
|
+
let ver = (text || '').toString().trim();
|
|
2048
|
+
if(ver){ warnedVersionMissing = false; }
|
|
2049
|
+
if(!currentVersion){ currentVersion = ver; }
|
|
2050
|
+
if(ver && ver !== currentVersion){
|
|
2051
|
+
currentVersion = ver;
|
|
2052
|
+
// Política solicitada: siempre recargar por completo al detectar nueva versión
|
|
2053
|
+
try { location.reload(); } catch(_) {}
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
timer = setTimeout(tick, 600);
|
|
2057
|
+
}, function(){
|
|
2058
|
+
if(!warnedVersionMissing){ console.log('[Dars] waiting for version.txt'); warnedVersionMissing = true; }
|
|
2059
|
+
timer = setTimeout(tick, 600);
|
|
2060
|
+
}, 'text');
|
|
2061
|
+
}
|
|
2062
|
+
tick();
|
|
2063
|
+
return ()=>{ if(timer) clearTimeout(timer); };
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
document.addEventListener('DOMContentLoaded', function(){
|
|
2067
|
+
if(window.__DARS_VDOM__){
|
|
2068
|
+
hydrate(window.__DARS_VDOM__);
|
|
2069
|
+
} else {
|
|
2070
|
+
console.warn('[Dars] No VDOM snapshot found for hydration');
|
|
2071
|
+
}
|
|
2072
|
+
// Activar hot-reload incremental en dev si hay URLs definidas
|
|
2073
|
+
if(window.__DARS_VERSION_URL && window.__DARS_SNAPSHOT_URL){
|
|
2074
|
+
startHotReload();
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
})();
|
|
2078
|
+
"""
|
|
2079
|
+
return runtime
|
|
2080
|
+
|
|
2081
|
+
def get_component_id(self, component, prefix="comp"):
|
|
2082
|
+
"""
|
|
2083
|
+
Devuelve el id del componente.
|
|
2084
|
+
- Si el componente ya tiene id definido, se respeta.
|
|
2085
|
+
- Si no tiene, se genera uno único y se asigna al objeto (para consistencia).
|
|
2086
|
+
"""
|
|
2087
|
+
comp_id = getattr(component, "id", None)
|
|
2088
|
+
if not comp_id:
|
|
2089
|
+
comp_id = self.generate_unique_id(component, prefix=prefix)
|
|
2090
|
+
try:
|
|
2091
|
+
component.id = comp_id
|
|
2092
|
+
except Exception:
|
|
2093
|
+
# si el objeto no permite asignar, seguimos usando comp_id local
|
|
2094
|
+
pass
|
|
2095
|
+
# Hash IDs in bundle mode consistently
|
|
2096
|
+
if getattr(self, '_hash_ids', False) and comp_id:
|
|
2097
|
+
hid = self._hash_id(comp_id)
|
|
2098
|
+
try:
|
|
2099
|
+
component.id = hid
|
|
2100
|
+
except Exception:
|
|
2101
|
+
pass
|
|
2102
|
+
return hid
|
|
2103
|
+
return comp_id
|
|
2104
|
+
|
|
2105
|
+
def _hash_id(self, original: str) -> str:
|
|
2106
|
+
import hashlib
|
|
2107
|
+
m = getattr(self, '_id_hash_map', None)
|
|
2108
|
+
if m is None:
|
|
2109
|
+
self._id_hash_map = {}
|
|
2110
|
+
m = self._id_hash_map
|
|
2111
|
+
if original in m:
|
|
2112
|
+
return m[original]
|
|
2113
|
+
h = hashlib.sha256(original.encode('utf-8')).hexdigest()[:12]
|
|
2114
|
+
obf = 'd' + h
|
|
2115
|
+
m[original] = obf
|
|
2116
|
+
return obf
|
|
2117
|
+
|
|
2118
|
+
def render_component(self, component: Component) -> str:
|
|
2119
|
+
if not isinstance(component, Component):
|
|
2120
|
+
raise TypeError(f"render_component wait to recived an instance of Component, but recive an {component}")
|
|
2121
|
+
"""Render an HTML component"""
|
|
2122
|
+
from dars.components.basic.page import Page
|
|
2123
|
+
from dars.components.layout.grid import GridLayout
|
|
2124
|
+
from dars.components.layout.flex import FlexLayout
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
# Lista de componentes built-in de Dars que NO deben usar su propio método render()
|
|
2128
|
+
builtin_components = [
|
|
2129
|
+
Page, GridLayout, FlexLayout, Text, Button, Input, Container, Image, Link,
|
|
2130
|
+
Textarea, Card, Modal, Navbar, Checkbox, RadioButton, Select, Slider,
|
|
2131
|
+
DatePicker, Table, Tabs, Accordion, ProgressBar, Spinner, Tooltip, Markdown,
|
|
2132
|
+
]
|
|
2133
|
+
|
|
2134
|
+
# Verificar si es un componente personalizado (no built-in)
|
|
2135
|
+
is_custom_component = True
|
|
2136
|
+
for builtin_type in builtin_components:
|
|
2137
|
+
if isinstance(component, builtin_type):
|
|
2138
|
+
is_custom_component = False
|
|
2139
|
+
break
|
|
2140
|
+
|
|
2141
|
+
if isinstance(component, Component) and is_custom_component:
|
|
2142
|
+
if hasattr(component, 'render') and callable(component.render):
|
|
2143
|
+
try:
|
|
2144
|
+
return component.render(self)
|
|
2145
|
+
except Exception as e:
|
|
2146
|
+
print(f"Error at rendering component {component.__class__.__name__}: {e}")
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
if isinstance(component, Page):
|
|
2150
|
+
return self.render_page(component)
|
|
2151
|
+
if isinstance(component, GridLayout):
|
|
2152
|
+
return self.render_grid(component)
|
|
2153
|
+
if isinstance(component, FlexLayout):
|
|
2154
|
+
return self.render_flex(component)
|
|
2155
|
+
if isinstance(component, Text):
|
|
2156
|
+
return self.render_text(component)
|
|
2157
|
+
elif isinstance(component, Button):
|
|
2158
|
+
return self.render_button(component)
|
|
2159
|
+
elif isinstance(component, Input):
|
|
2160
|
+
return self.render_input(component)
|
|
2161
|
+
elif isinstance(component, Container):
|
|
2162
|
+
return self.render_container(component)
|
|
2163
|
+
elif isinstance(component, Image):
|
|
2164
|
+
return self.render_image(component)
|
|
2165
|
+
elif isinstance(component, Link):
|
|
2166
|
+
return self.render_link(component)
|
|
2167
|
+
elif isinstance(component, Textarea):
|
|
2168
|
+
return self.render_textarea(component)
|
|
2169
|
+
elif isinstance(component, Card):
|
|
2170
|
+
return self.render_card(component)
|
|
2171
|
+
elif isinstance(component, Modal):
|
|
2172
|
+
return self.render_modal(component)
|
|
2173
|
+
elif isinstance(component, Navbar):
|
|
2174
|
+
return self.render_navbar(component)
|
|
2175
|
+
elif isinstance(component, Checkbox):
|
|
2176
|
+
return self.render_checkbox(component)
|
|
2177
|
+
elif isinstance(component, RadioButton):
|
|
2178
|
+
return self.render_radiobutton(component)
|
|
2179
|
+
elif isinstance(component, Select):
|
|
2180
|
+
return self.render_select(component)
|
|
2181
|
+
elif isinstance(component, Slider):
|
|
2182
|
+
return self.render_slider(component)
|
|
2183
|
+
elif isinstance(component, DatePicker):
|
|
2184
|
+
return self.render_datepicker(component)
|
|
2185
|
+
elif isinstance(component, Table):
|
|
2186
|
+
return self.render_table(component)
|
|
2187
|
+
elif isinstance(component, Tabs):
|
|
2188
|
+
return self.render_tabs(component)
|
|
2189
|
+
elif isinstance(component, Accordion):
|
|
2190
|
+
return self.render_accordion(component)
|
|
2191
|
+
elif isinstance(component, ProgressBar):
|
|
2192
|
+
return self.render_progressbar(component)
|
|
2193
|
+
elif isinstance(component, Spinner):
|
|
2194
|
+
return self.render_spinner(component)
|
|
2195
|
+
elif isinstance(component, Tooltip):
|
|
2196
|
+
return self.render_tooltip(component)
|
|
2197
|
+
elif isinstance(component, Markdown):
|
|
2198
|
+
return self.render_markdown(component)
|
|
2199
|
+
else:
|
|
2200
|
+
# Componente genérico
|
|
2201
|
+
return self.render_generic_component(component)
|
|
2202
|
+
|
|
2203
|
+
def render_grid(self, grid):
|
|
2204
|
+
"""Renderiza un GridLayout como un div con CSS grid."""
|
|
2205
|
+
component_id = self.get_component_id(grid, prefix="grid")
|
|
2206
|
+
class_attr = f'class="dars-grid {grid.class_name or ""}"'
|
|
2207
|
+
style = f'display: grid; grid-template-rows: repeat({grid.rows}, 1fr); grid-template-columns: repeat({grid.cols}, 1fr); gap: {getattr(grid, "gap", "16px")};'
|
|
2208
|
+
# Render anchors/positions
|
|
2209
|
+
children_html = ""
|
|
2210
|
+
layout_info = getattr(grid, 'get_child_layout', lambda: [])()
|
|
2211
|
+
for child_info in layout_info:
|
|
2212
|
+
child = child_info['child']
|
|
2213
|
+
row = child_info.get('row', 0) + 1
|
|
2214
|
+
col = child_info.get('col', 0) + 1
|
|
2215
|
+
row_span = child_info.get('row_span', 1)
|
|
2216
|
+
col_span = child_info.get('col_span', 1)
|
|
2217
|
+
anchor = child_info.get('anchor')
|
|
2218
|
+
anchor_style = ''
|
|
2219
|
+
if anchor:
|
|
2220
|
+
if isinstance(anchor, str):
|
|
2221
|
+
anchor_map = {
|
|
2222
|
+
'top-left': 'justify-self: start; align-self: start;',
|
|
2223
|
+
'top': 'justify-self: center; align-self: start;',
|
|
2224
|
+
'top-right': 'justify-self: end; align-self: start;',
|
|
2225
|
+
'left': 'justify-self: start; align-self: center;',
|
|
2226
|
+
'center': 'justify-self: center; align-self: center;',
|
|
2227
|
+
'right': 'justify-self: end; align-self: center;',
|
|
2228
|
+
'bottom-left': 'justify-self: start; align-self: end;',
|
|
2229
|
+
'bottom': 'justify-self: center; align-self: end;',
|
|
2230
|
+
'bottom-right': 'justify-self: end; align-self: end;'
|
|
2231
|
+
}
|
|
2232
|
+
anchor_style = anchor_map.get(anchor, '')
|
|
2233
|
+
elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
|
|
2234
|
+
# AnchorPoint object
|
|
2235
|
+
if getattr(anchor, 'x', None):
|
|
2236
|
+
if anchor.x == 'left': anchor_style += 'justify-self: start;'
|
|
2237
|
+
elif anchor.x == 'center': anchor_style += 'justify-self: center;'
|
|
2238
|
+
elif anchor.x == 'right': anchor_style += 'justify-self: end;'
|
|
2239
|
+
elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
|
|
2240
|
+
if getattr(anchor, 'y', None):
|
|
2241
|
+
if anchor.y == 'top': anchor_style += 'align-self: start;'
|
|
2242
|
+
elif anchor.y == 'center': anchor_style += 'align-self: center;'
|
|
2243
|
+
elif anchor.y == 'bottom': anchor_style += 'align-self: end;'
|
|
2244
|
+
elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
|
|
2245
|
+
grid_item_style = f'grid-row: {row} / span {row_span}; grid-column: {col} / span {col_span}; {anchor_style}'
|
|
2246
|
+
children_html += f'<div style="{grid_item_style}">{self.render_component(child)}</div>'
|
|
2247
|
+
return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
|
|
2248
|
+
|
|
2249
|
+
def render_flex(self, flex):
|
|
2250
|
+
"""Renderiza un FlexLayout como un div con CSS flexbox."""
|
|
2251
|
+
component_id = self.get_component_id(flex, prefix="flex")
|
|
2252
|
+
class_attr = f'class="dars-flex {flex.class_name or ""}"'
|
|
2253
|
+
style = f'display: flex; flex-direction: {getattr(flex, "direction", "row")}; flex-wrap: {getattr(flex, "wrap", "wrap")}; justify-content: {getattr(flex, "justify", "flex-start")}; align-items: {getattr(flex, "align", "stretch")}; gap: {getattr(flex, "gap", "16px")};'
|
|
2254
|
+
children_html = ""
|
|
2255
|
+
for child in flex.children:
|
|
2256
|
+
anchor = getattr(child, 'anchor', None)
|
|
2257
|
+
anchor_style = ''
|
|
2258
|
+
if anchor:
|
|
2259
|
+
if isinstance(anchor, str):
|
|
2260
|
+
anchor_map = {
|
|
2261
|
+
'top-left': 'align-self: flex-start; justify-self: flex-start;',
|
|
2262
|
+
'top': 'align-self: flex-start; margin-left: auto; margin-right: auto;',
|
|
2263
|
+
'top-right': 'align-self: flex-start; margin-left: auto;',
|
|
2264
|
+
'left': 'align-self: center;',
|
|
2265
|
+
'center': 'align-self: center; margin-left: auto; margin-right: auto;',
|
|
2266
|
+
'right': 'align-self: center; margin-left: auto;',
|
|
2267
|
+
'bottom-left': 'align-self: flex-end;',
|
|
2268
|
+
'bottom': 'align-self: flex-end; margin-left: auto; margin-right: auto;',
|
|
2269
|
+
'bottom-right': 'align-self: flex-end; margin-left: auto;'
|
|
2270
|
+
}
|
|
2271
|
+
anchor_style = anchor_map.get(anchor, '')
|
|
2272
|
+
elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
|
|
2273
|
+
if getattr(anchor, 'x', None):
|
|
2274
|
+
if anchor.x == 'left': anchor_style += 'margin-right: auto;'
|
|
2275
|
+
elif anchor.x == 'center': anchor_style += 'margin-left: auto; margin-right: auto;'
|
|
2276
|
+
elif anchor.x == 'right': anchor_style += 'margin-left: auto;'
|
|
2277
|
+
elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
|
|
2278
|
+
if getattr(anchor, 'y', None):
|
|
2279
|
+
if anchor.y == 'top': anchor_style += 'align-self: flex-start;'
|
|
2280
|
+
elif anchor.y == 'center': anchor_style += 'align-self: center;'
|
|
2281
|
+
elif anchor.y == 'bottom': anchor_style += 'align-self: flex-end;'
|
|
2282
|
+
elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
|
|
2283
|
+
children_html += f'<div style="{anchor_style}">{self.render_component(child)}</div>'
|
|
2284
|
+
return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
|
|
2285
|
+
|
|
2286
|
+
def render_page(self, page):
|
|
2287
|
+
"""Renderiza un componente Page como root de una página multipage"""
|
|
2288
|
+
component_id = self.generate_unique_id(page)
|
|
2289
|
+
class_attr = f'class="dars-page {page.class_name or ""}"'
|
|
2290
|
+
style_attr = f'style="{self.render_styles(page.style)}"' if page.style else ""
|
|
2291
|
+
# Renderizar hijos
|
|
2292
|
+
children_html = ""
|
|
2293
|
+
children = getattr(page, 'children', [])
|
|
2294
|
+
if not isinstance(children, list):
|
|
2295
|
+
children = []
|
|
2296
|
+
for child in children:
|
|
2297
|
+
if hasattr(child, 'render'):
|
|
2298
|
+
children_html += self.render_component(child)
|
|
2299
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
def render_text(self, text: Text) -> str:
|
|
2304
|
+
"""Renderiza un componente Text"""
|
|
2305
|
+
component_id = self.get_component_id(text, prefix="text")
|
|
2306
|
+
class_attr = f'class="dars-text {text.class_name or ""}"'
|
|
2307
|
+
style_attr = f'style="{self.render_styles(text.style)}"' if text.style else ""
|
|
2308
|
+
|
|
2309
|
+
return f'<span id="{component_id}" {class_attr} {style_attr}>{text.text}</span>'
|
|
2310
|
+
|
|
2311
|
+
def render_button(self, button: Button) -> str:
|
|
2312
|
+
"""Renderiza un componente Button"""
|
|
2313
|
+
# Asegurarse de que el botón tenga un ID
|
|
2314
|
+
if not hasattr(button, 'id') or not button.id:
|
|
2315
|
+
import uuid
|
|
2316
|
+
button.id = f"btn_{str(uuid.uuid4())[:8]}"
|
|
2317
|
+
|
|
2318
|
+
component_id = self.get_component_id(button, prefix="btn")
|
|
2319
|
+
class_attr = f'class="dars-button {button.class_name or ""}"'
|
|
2320
|
+
style_attr = f'style="{self.render_styles(button.style)}"' if button.style else ""
|
|
2321
|
+
type_attr = f'type="{button.button_type}"'
|
|
2322
|
+
disabled_attr = "disabled" if button.disabled else ""
|
|
2323
|
+
|
|
2324
|
+
return f'<button id="{component_id}" {class_attr} {style_attr} {type_attr} {disabled_attr}>{button.text}</button>'
|
|
2325
|
+
|
|
2326
|
+
def render_input(self, input_comp: Input) -> str:
|
|
2327
|
+
"""Renderiza un componente Input"""
|
|
2328
|
+
component_id = self.get_component_id(input_comp, prefix="input")
|
|
2329
|
+
class_attr = f'class="dars-input {input_comp.class_name or ""}"'
|
|
2330
|
+
style_attr = f'style="{self.render_styles(input_comp.style)}"' if input_comp.style else ""
|
|
2331
|
+
type_attr = f'type="{input_comp.input_type}"'
|
|
2332
|
+
value_attr = f'value="{input_comp.value}"' if input_comp.value else ""
|
|
2333
|
+
placeholder_attr = f'placeholder="{input_comp.placeholder}"' if input_comp.placeholder else ""
|
|
2334
|
+
disabled_attr = "disabled" if input_comp.disabled else ""
|
|
2335
|
+
readonly_attr = "readonly" if input_comp.readonly else ""
|
|
2336
|
+
required_attr = "required" if input_comp.required else ""
|
|
2337
|
+
|
|
2338
|
+
attrs = [class_attr, style_attr, type_attr, value_attr, placeholder_attr,
|
|
2339
|
+
disabled_attr, readonly_attr, required_attr]
|
|
2340
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2341
|
+
|
|
2342
|
+
return f'<input id="{component_id}" {attrs_str} />'
|
|
2343
|
+
|
|
2344
|
+
def render_container(self, container: Container) -> str:
|
|
2345
|
+
"""Renderiza un componente Container"""
|
|
2346
|
+
component_id = self.get_component_id(container, prefix="container")
|
|
2347
|
+
class_attr = f'class="dars-container {container.class_name or ""}"'
|
|
2348
|
+
style_attr = f'style="{self.render_styles(container.style)}"' if container.style else ""
|
|
2349
|
+
|
|
2350
|
+
# Protección: asegurar que children es lista de Component
|
|
2351
|
+
children_html = ""
|
|
2352
|
+
children = container.children
|
|
2353
|
+
if not isinstance(children, list):
|
|
2354
|
+
children = []
|
|
2355
|
+
# Aplanar si hay listas anidadas
|
|
2356
|
+
flat_children = []
|
|
2357
|
+
for child in children:
|
|
2358
|
+
if isinstance(child, list):
|
|
2359
|
+
flat_children.extend([c for c in child if hasattr(c, 'render')])
|
|
2360
|
+
elif hasattr(child, 'render'):
|
|
2361
|
+
flat_children.append(child)
|
|
2362
|
+
for child in flat_children:
|
|
2363
|
+
children_html += self.render_component(child)
|
|
2364
|
+
|
|
2365
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
|
|
2366
|
+
|
|
2367
|
+
def render_image(self, image: Image) -> str:
|
|
2368
|
+
"""Renderiza un componente Image"""
|
|
2369
|
+
component_id = self.get_component_id(image, prefix="image")
|
|
2370
|
+
class_attr = f'class="dars-image {image.class_name or ""}"'
|
|
2371
|
+
style_attr = f'style="{self.render_styles(image.style)}"' if image.style else ""
|
|
2372
|
+
width_attr = f'width="{image.width}"' if image.width else ""
|
|
2373
|
+
height_attr = f'height="{image.height}"' if image.height else ""
|
|
2374
|
+
|
|
2375
|
+
return f'<img id="{component_id}" src="{image.src}" alt="{image.alt}" {width_attr} {height_attr} {class_attr} {style_attr} />'
|
|
2376
|
+
|
|
2377
|
+
def render_link(self, link: Link) -> str:
|
|
2378
|
+
"""Renderiza un componente Link"""
|
|
2379
|
+
component_id = self.get_component_id(link, prefix="link")
|
|
2380
|
+
class_attr = f'class="dars-link {link.class_name or ""}"'
|
|
2381
|
+
style_attr = f'style="{self.render_styles(link.style)}"' if link.style else ""
|
|
2382
|
+
target_attr = f'target="{link.target}"'
|
|
2383
|
+
|
|
2384
|
+
return f'<a id="{component_id}" href="{link.href}" {target_attr} {class_attr} {style_attr}>{link.text}</a>'
|
|
2385
|
+
|
|
2386
|
+
def render_textarea(self, textarea: Textarea) -> str:
|
|
2387
|
+
"""Renderiza un componente Textarea"""
|
|
2388
|
+
component_id = self.get_component_id(textarea, prefix="textarea")
|
|
2389
|
+
class_attr = f'class="dars-textarea {textarea.class_name or ""}"'
|
|
2390
|
+
style_attr = f'style="{self.render_styles(textarea.style)}"' if textarea.style else ""
|
|
2391
|
+
rows_attr = f'rows="{textarea.rows}"'
|
|
2392
|
+
cols_attr = f'cols="{textarea.cols}"'
|
|
2393
|
+
placeholder_attr = f'placeholder="{textarea.placeholder}"' if textarea.placeholder else ""
|
|
2394
|
+
disabled_attr = "disabled" if textarea.disabled else ""
|
|
2395
|
+
readonly_attr = "readonly" if textarea.readonly else ""
|
|
2396
|
+
required_attr = "required" if textarea.required else ""
|
|
2397
|
+
maxlength_attr = f'maxlength="{textarea.max_length}"' if textarea.max_length else ""
|
|
2398
|
+
|
|
2399
|
+
attrs = [class_attr, style_attr, rows_attr, cols_attr, placeholder_attr,
|
|
2400
|
+
disabled_attr, readonly_attr, required_attr, maxlength_attr]
|
|
2401
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2402
|
+
|
|
2403
|
+
return f'<textarea id="{component_id}" {attrs_str}>{textarea.value}</textarea>'
|
|
2404
|
+
|
|
2405
|
+
def render_card(self, card: Card) -> str:
|
|
2406
|
+
"""Renderiza un componente Card"""
|
|
2407
|
+
component_id = self.get_component_id(card, prefix="card")
|
|
2408
|
+
class_attr = f'class="dars-card {card.class_name or ""}"'
|
|
2409
|
+
style_attr = f'style="{self.render_styles(card.style)}"' if card.style else ""
|
|
2410
|
+
title_html = f'<h2>{card.title}</h2>' if card.title else ""
|
|
2411
|
+
children_html = ""
|
|
2412
|
+
for child in card.children:
|
|
2413
|
+
children_html += self.render_component(child)
|
|
2414
|
+
|
|
2415
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{title_html}{children_html}</div>'
|
|
2416
|
+
|
|
2417
|
+
def render_modal(self, modal: Modal) -> str:
|
|
2418
|
+
"""Renderiza un componente Modal"""
|
|
2419
|
+
component_id = self.get_component_id(modal, prefix="modal")
|
|
2420
|
+
class_list = "dars-modal"
|
|
2421
|
+
if not modal.is_open:
|
|
2422
|
+
class_list += " dars-modal-hidden"
|
|
2423
|
+
if modal.class_name:
|
|
2424
|
+
class_list += f" {modal.class_name}"
|
|
2425
|
+
hidden_attr = " hidden" if not modal.is_open else ""
|
|
2426
|
+
display_style = "display: flex;" if modal.is_open else "display: none;"
|
|
2427
|
+
modal_style = f'{display_style} position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); justify-content: center; align-items: center; z-index: 1000;'
|
|
2428
|
+
if modal.style:
|
|
2429
|
+
modal_style += f' {self.render_styles(modal.style)}'
|
|
2430
|
+
data_enabled = f'data-enabled="{str(getattr(modal, "is_enabled", True)).lower()}"'
|
|
2431
|
+
title_html = f'<h2>{modal.title}</h2>' if modal.title else ""
|
|
2432
|
+
children_html = ""
|
|
2433
|
+
for child in modal.children:
|
|
2434
|
+
children_html += self.render_component(child)
|
|
2435
|
+
return (
|
|
2436
|
+
f'<div id="{component_id}" class="{class_list}" {data_enabled}{hidden_attr} style="{modal_style}">\n'
|
|
2437
|
+
f' <div class="dars-modal-content" style="background: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%;">\n'
|
|
2438
|
+
f' {title_html}\n'
|
|
2439
|
+
f' {children_html}\n'
|
|
2440
|
+
f' </div>\n'
|
|
2441
|
+
f'</div>'
|
|
2442
|
+
)
|
|
2443
|
+
|
|
2444
|
+
def render_navbar(self, navbar: Navbar) -> str:
|
|
2445
|
+
"""Renderiza un componente Navbar"""
|
|
2446
|
+
component_id = self.get_component_id(navbar, prefix="navbar")
|
|
2447
|
+
class_attr = f'class="dars-navbar {navbar.class_name or ""}"'
|
|
2448
|
+
style_attr = f'style="{self.render_styles(navbar.style)}"' if navbar.style else ""
|
|
2449
|
+
brand_html = f'<div class="dars-navbar-brand">{navbar.brand}</div>' if navbar.brand else ""
|
|
2450
|
+
# Soporta hijos como lista o *args (igual que Container)
|
|
2451
|
+
children = getattr(navbar, 'children', [])
|
|
2452
|
+
if callable(children):
|
|
2453
|
+
children = children()
|
|
2454
|
+
if children is None:
|
|
2455
|
+
children = []
|
|
2456
|
+
if not isinstance(children, (list, tuple)):
|
|
2457
|
+
children = [children]
|
|
2458
|
+
children_html = ""
|
|
2459
|
+
for child in children:
|
|
2460
|
+
children_html += self.render_component(child)
|
|
2461
|
+
|
|
2462
|
+
return f'<nav id="{component_id}" {class_attr} {style_attr}>{brand_html}<div class="dars-navbar-nav">{children_html}</div></nav>'
|
|
2463
|
+
|
|
2464
|
+
def render_checkbox(self, checkbox: Checkbox) -> str:
|
|
2465
|
+
"""Renderiza un componente Checkbox"""
|
|
2466
|
+
component_id = self.get_component_id(checkbox, prefix="checkbox")
|
|
2467
|
+
class_attr = f'class="dars-checkbox {checkbox.class_name or ""}"'
|
|
2468
|
+
style_attr = f'style="{self.render_styles(checkbox.style)}"' if checkbox.style else ""
|
|
2469
|
+
checked_attr = "checked" if checkbox.checked else ""
|
|
2470
|
+
disabled_attr = "disabled" if checkbox.disabled else ""
|
|
2471
|
+
required_attr = "required" if checkbox.required else ""
|
|
2472
|
+
name_attr = f'name="{checkbox.name}"' if checkbox.name else ""
|
|
2473
|
+
value_attr = f'value="{checkbox.value}"' if checkbox.value else ""
|
|
2474
|
+
|
|
2475
|
+
attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
|
|
2476
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2477
|
+
|
|
2478
|
+
label_html = f'<label for="{component_id}">{checkbox.label}</label>' if checkbox.label else ""
|
|
2479
|
+
|
|
2480
|
+
return f'<div class="dars-checkbox-wrapper"><input type="checkbox" id="{component_id}" {attrs_str}>{label_html}</div>'
|
|
2481
|
+
|
|
2482
|
+
def render_radiobutton(self, radio: RadioButton) -> str:
|
|
2483
|
+
"""Renderiza un componente RadioButton"""
|
|
2484
|
+
component_id = self.get_component_id(radio, prefix="radiobutton")
|
|
2485
|
+
class_attr = f'class="dars-radio {radio.class_name or ""}"'
|
|
2486
|
+
style_attr = f'style="{self.render_styles(radio.style)}"' if radio.style else ""
|
|
2487
|
+
checked_attr = "checked" if radio.checked else ""
|
|
2488
|
+
disabled_attr = "disabled" if radio.disabled else ""
|
|
2489
|
+
required_attr = "required" if radio.required else ""
|
|
2490
|
+
name_attr = f'name="{radio.name}"'
|
|
2491
|
+
value_attr = f'value="{radio.value}"'
|
|
2492
|
+
|
|
2493
|
+
attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
|
|
2494
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2495
|
+
|
|
2496
|
+
label_html = f'<label for="{component_id}">{radio.label}</label>' if radio.label else ""
|
|
2497
|
+
|
|
2498
|
+
return f'<div class="dars-radio-wrapper"><input type="radio" id="{component_id}" {attrs_str}>{label_html}</div>'
|
|
2499
|
+
|
|
2500
|
+
def render_select(self, select: Select) -> str:
|
|
2501
|
+
"""Renderiza un componente Select"""
|
|
2502
|
+
component_id = self.get_component_id(select, prefix="select")
|
|
2503
|
+
class_attr = f'class="dars-select {select.class_name or ""}"'
|
|
2504
|
+
style_attr = f'style="{self.render_styles(select.style)}"' if select.style else ""
|
|
2505
|
+
disabled_attr = "disabled" if select.disabled else ""
|
|
2506
|
+
required_attr = "required" if select.required else ""
|
|
2507
|
+
multiple_attr = "multiple" if select.multiple else ""
|
|
2508
|
+
size_attr = f'size="{select.size}"' if select.size else ""
|
|
2509
|
+
|
|
2510
|
+
attrs = [class_attr, style_attr, disabled_attr, required_attr, multiple_attr, size_attr]
|
|
2511
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2512
|
+
|
|
2513
|
+
# Generar opciones
|
|
2514
|
+
options_html = ""
|
|
2515
|
+
if select.placeholder and not select.multiple:
|
|
2516
|
+
selected = "selected" if not select.value else ""
|
|
2517
|
+
options_html += f'<option value="" disabled {selected}>{select.placeholder}</option>'
|
|
2518
|
+
|
|
2519
|
+
for option in select.options:
|
|
2520
|
+
selected = "selected" if option.value == select.value else ""
|
|
2521
|
+
disabled = "disabled" if option.disabled else ""
|
|
2522
|
+
options_html += f'<option value="{option.value}" {selected} {disabled}>{option.label}</option>'
|
|
2523
|
+
|
|
2524
|
+
return f'<select id="{component_id}" {attrs_str}>{options_html}</select>'
|
|
2525
|
+
|
|
2526
|
+
def render_slider(self, slider: Slider) -> str:
|
|
2527
|
+
"""Renderiza un componente Slider"""
|
|
2528
|
+
component_id = self.get_component_id(slider, prefix="slider")
|
|
2529
|
+
class_attr = f'class="dars-slider {slider.class_name or ""}"'
|
|
2530
|
+
style_attr = f'style="{self.render_styles(slider.style)}"' if slider.style else ""
|
|
2531
|
+
disabled_attr = "disabled" if slider.disabled else ""
|
|
2532
|
+
min_attr = f'min="{slider.min_value}"'
|
|
2533
|
+
max_attr = f'max="{slider.max_value}"'
|
|
2534
|
+
value_attr = f'value="{slider.value}"'
|
|
2535
|
+
step_attr = f'step="{slider.step}"'
|
|
2536
|
+
|
|
2537
|
+
attrs = [class_attr, style_attr, disabled_attr, min_attr, max_attr, value_attr, step_attr]
|
|
2538
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2539
|
+
|
|
2540
|
+
label_html = f'<label for="{component_id}">{slider.label}</label>' if slider.label else ""
|
|
2541
|
+
value_display = f'<span class="dars-slider-value">{slider.value}</span>' if slider.show_value else ""
|
|
2542
|
+
|
|
2543
|
+
wrapper_class = "dars-slider-vertical" if slider.orientation == "vertical" else "dars-slider-horizontal"
|
|
2544
|
+
|
|
2545
|
+
return f'<div class="dars-slider-wrapper {wrapper_class}">{label_html}<input type="range" id="{component_id}" {attrs_str}>{value_display}</div>'
|
|
2546
|
+
|
|
2547
|
+
def render_datepicker(self, datepicker: DatePicker) -> str:
|
|
2548
|
+
"""Renderiza un componente DatePicker"""
|
|
2549
|
+
component_id = self.get_component_id(datepicker, prefix="datepicker")
|
|
2550
|
+
class_attr = f'class="dars-datepicker {datepicker.class_name or ""}"'
|
|
2551
|
+
style_attr = f'style="{self.render_styles(datepicker.style)}"' if datepicker.style else ""
|
|
2552
|
+
disabled_attr = "disabled" if datepicker.disabled else ""
|
|
2553
|
+
required_attr = "required" if datepicker.required else ""
|
|
2554
|
+
readonly_attr = "readonly" if datepicker.readonly else ""
|
|
2555
|
+
value_attr = f'value="{datepicker.value}"' if datepicker.value else ""
|
|
2556
|
+
placeholder_attr = f'placeholder="{datepicker.placeholder}"' if datepicker.placeholder else ""
|
|
2557
|
+
min_attr = f'min="{datepicker.min_date}"' if datepicker.min_date else ""
|
|
2558
|
+
max_attr = f'max="{datepicker.max_date}"' if datepicker.max_date else ""
|
|
2559
|
+
|
|
2560
|
+
# Determinar el tipo de input según si incluye tiempo
|
|
2561
|
+
input_type = "datetime-local" if datepicker.show_time else "date"
|
|
2562
|
+
|
|
2563
|
+
attrs = [class_attr, style_attr, disabled_attr, required_attr, readonly_attr,
|
|
2564
|
+
value_attr, placeholder_attr, min_attr, max_attr]
|
|
2565
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
2566
|
+
|
|
2567
|
+
# Si es inline, usar un div contenedor adicional
|
|
2568
|
+
if datepicker.inline:
|
|
2569
|
+
return f'<div class="dars-datepicker-inline"><input type="{input_type}" id="{component_id}" {attrs_str}></div>'
|
|
2570
|
+
else:
|
|
2571
|
+
return f'<input type="{input_type}" id="{component_id}" {attrs_str}>'
|
|
2572
|
+
|
|
2573
|
+
def render_table(self, table: Table) -> str:
|
|
2574
|
+
# Renderizado HTML para Table
|
|
2575
|
+
thead = '<thead><tr>' + ''.join(f'<th>{col["title"]}</th>' for col in table.columns) + '</tr></thead>'
|
|
2576
|
+
rows = table.data[:table.page_size] if table.page_size else table.data
|
|
2577
|
+
tbody = '<tbody>' + ''.join(
|
|
2578
|
+
'<tr>' + ''.join(f'<td>{row.get(col["field"], "")}</td>' for col in table.columns) + '</tr>'
|
|
2579
|
+
for row in rows) + '</tbody>'
|
|
2580
|
+
return f'<table class="dars-table">{thead}{tbody}</table>'
|
|
2581
|
+
|
|
2582
|
+
def render_tabs(self, tabs: Tabs) -> str:
|
|
2583
|
+
tab_headers = ''.join(
|
|
2584
|
+
f'<button class="dars-tab{ " dars-tab-active" if i == tabs.selected else "" }" data-tab="{i}">{title}</button>'
|
|
2585
|
+
for i, title in enumerate(tabs.tabs)
|
|
2586
|
+
)
|
|
2587
|
+
panels_html = ''.join(
|
|
2588
|
+
f'<div class="dars-tab-panel{ " dars-tab-panel-active" if i == tabs.selected else "" }">{self.render_component(panel) if hasattr(panel, "render") else panel}</div>'
|
|
2589
|
+
for i, panel in enumerate(tabs.panels)
|
|
2590
|
+
)
|
|
2591
|
+
return f'<div class="dars-tabs"><div class="dars-tabs-header">{tab_headers}</div><div class="dars-tabs-panels">{panels_html}</div></div>'
|
|
2592
|
+
|
|
2593
|
+
def render_accordion(self, accordion: Accordion) -> str:
|
|
2594
|
+
html = '<div class="dars-accordion">'
|
|
2595
|
+
for i, (title, content) in enumerate(accordion.sections):
|
|
2596
|
+
opened = ' dars-accordion-open' if i in accordion.open_indices else ''
|
|
2597
|
+
html += f'<div class="dars-accordion-section{opened}"><div class="dars-accordion-title">{title}</div><div class="dars-accordion-content">{self.render_component(content) if hasattr(content, "render") else content}</div></div>'
|
|
2598
|
+
html += '</div>'
|
|
2599
|
+
return html
|
|
2600
|
+
|
|
2601
|
+
def render_progressbar(self, bar: ProgressBar) -> str:
|
|
2602
|
+
percent = min(max(bar.value / bar.max_value * 100, 0), 100)
|
|
2603
|
+
return f'<div class="dars-progressbar"><div class="dars-progressbar-bar" style="width: {percent}%;"></div></div>'
|
|
2604
|
+
|
|
2605
|
+
def render_spinner(self, spinner: Spinner) -> str:
|
|
2606
|
+
return '<div class="dars-spinner"></div>'
|
|
2607
|
+
|
|
2608
|
+
def render_tooltip(self, tooltip: Tooltip) -> str:
|
|
2609
|
+
return f'<div class="dars-tooltip dars-tooltip-{tooltip.position}">{self.render_component(tooltip.child) if hasattr(tooltip.child, "render") else tooltip.child}<span class="dars-tooltip-text">{tooltip.text}</span></div>'
|
|
2610
|
+
|
|
2611
|
+
def render_markdown(self, markdown: 'Markdown') -> str:
|
|
2612
|
+
"""Render a Markdown component"""
|
|
2613
|
+
try:
|
|
2614
|
+
import markdown2
|
|
2615
|
+
# Convert markdown to HTML
|
|
2616
|
+
html_content = markdown2.markdown(
|
|
2617
|
+
markdown.content,
|
|
2618
|
+
extras=["fenced-code-blocks", "tables", "header-ids"]
|
|
2619
|
+
)
|
|
2620
|
+
except ImportError:
|
|
2621
|
+
# Fallback to basic conversion if markdown2 is not available
|
|
2622
|
+
html_content = self._basic_markdown_to_html(markdown.content)
|
|
2623
|
+
|
|
2624
|
+
component_id = self.get_component_id(markdown, prefix="markdown")
|
|
2625
|
+
|
|
2626
|
+
# Add dark theme class if enabled
|
|
2627
|
+
class_name = f"dars-markdown {markdown.class_name or ''}"
|
|
2628
|
+
if markdown.dark_theme:
|
|
2629
|
+
class_name += " dars-markdown-dark"
|
|
2630
|
+
|
|
2631
|
+
class_attr = f'class="{class_name.strip()}"'
|
|
2632
|
+
style_attr = f'style="{self.render_styles(markdown.style)}"' if markdown.style else ""
|
|
2633
|
+
|
|
2634
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{html_content}</div>'
|
|
2635
|
+
|
|
2636
|
+
def _basic_markdown_to_html(self, markdown_text: str) -> str:
|
|
2637
|
+
"""Basic markdown to HTML conversion as fallback"""
|
|
2638
|
+
if not markdown_text:
|
|
2639
|
+
return ""
|
|
2640
|
+
|
|
2641
|
+
html = markdown_text
|
|
2642
|
+
|
|
2643
|
+
# Basic replacements
|
|
2644
|
+
html = html.replace('**', '<strong>').replace('**', '</strong>')
|
|
2645
|
+
html = html.replace('*', '<em>').replace('*', '</em>')
|
|
2646
|
+
html = html.replace('__', '<strong>').replace('__', '</strong>')
|
|
2647
|
+
html = html.replace('_', '<em>').replace('_', '</em>')
|
|
2648
|
+
|
|
2649
|
+
# Headers
|
|
2650
|
+
html = html.replace('# ', '<h1>').replace('\n# ', '</h1>\n<h1>')
|
|
2651
|
+
html = html.replace('## ', '<h2>').replace('\n## ', '</h2>\n<h2>')
|
|
2652
|
+
html = html.replace('### ', '<h3>').replace('\n### ', '</h3>\n<h3>')
|
|
2653
|
+
|
|
2654
|
+
# Line breaks
|
|
2655
|
+
html = html.replace('\n\n', '<br><br>')
|
|
2656
|
+
|
|
2657
|
+
return html
|
|
2658
|
+
def render_generic_component(self, component: Component) -> str:
|
|
2659
|
+
"""Renderiza un componente genérico con estructura básica"""
|
|
2660
|
+
component_id = self.get_component_id(component, prefix="comp")
|
|
2661
|
+
class_attr = f'class="{component.class_name or ""}"'
|
|
2662
|
+
style_attr = f'style="{self.render_styles(component.style)}"' if component.style else ""
|
|
2663
|
+
|
|
2664
|
+
# Renderizar hijos usando el exporter
|
|
2665
|
+
children_html = ""
|
|
2666
|
+
for child in component.children:
|
|
2667
|
+
children_html += self.render_component(child)
|
|
2668
|
+
|
|
2669
|
+
# Agregar eventos como data attributes para referencia
|
|
2670
|
+
events_attr = ""
|
|
2671
|
+
if component.events:
|
|
2672
|
+
for event_name in component.events:
|
|
2673
|
+
events_attr += f' data-event-{event_name}="true"'
|
|
2674
|
+
|
|
2675
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}{events_attr}>{children_html}</div>'
|