dars-framework 1.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dars/__init__.py +0 -0
- dars/all.py +69 -0
- dars/cli/__init__.py +0 -0
- dars/cli/doctor/__init__.py +1 -0
- dars/cli/doctor/detect.py +154 -0
- dars/cli/doctor/doctor.py +176 -0
- dars/cli/doctor/installers.py +100 -0
- dars/cli/doctor/persist.py +62 -0
- dars/cli/doctor/preflight.py +33 -0
- dars/cli/doctor/ui.py +54 -0
- dars/cli/hot_reload.py +33 -0
- dars/cli/main.py +1107 -0
- dars/cli/preview.py +448 -0
- dars/cli/translations.py +531 -0
- dars/components/__init__.py +0 -0
- dars/components/advanced/__init__.py +8 -0
- dars/components/advanced/accordion.py +26 -0
- dars/components/advanced/card.py +33 -0
- dars/components/advanced/modal.py +45 -0
- dars/components/advanced/navbar.py +44 -0
- dars/components/advanced/table.py +25 -0
- dars/components/advanced/tabs.py +31 -0
- dars/components/basic/__init__.py +34 -0
- dars/components/basic/button.py +55 -0
- dars/components/basic/checkbox.py +35 -0
- dars/components/basic/container.py +29 -0
- dars/components/basic/datepicker.py +139 -0
- dars/components/basic/image.py +36 -0
- dars/components/basic/input.py +57 -0
- dars/components/basic/link.py +31 -0
- dars/components/basic/markdown.py +86 -0
- dars/components/basic/page.py +20 -0
- dars/components/basic/progressbar.py +18 -0
- dars/components/basic/radiobutton.py +35 -0
- dars/components/basic/select.py +82 -0
- dars/components/basic/slider.py +63 -0
- dars/components/basic/spinner.py +12 -0
- dars/components/basic/text.py +23 -0
- dars/components/basic/textarea.py +46 -0
- dars/components/basic/tooltip.py +19 -0
- dars/components/layout/__init__.py +0 -0
- dars/components/layout/anchor.py +13 -0
- dars/components/layout/flex.py +26 -0
- dars/components/layout/grid.py +45 -0
- dars/config.py +134 -0
- dars/core/__init__.py +0 -0
- dars/core/app.py +957 -0
- dars/core/component.py +284 -0
- dars/core/events.py +102 -0
- dars/core/js_bridge.py +99 -0
- dars/core/properties.py +127 -0
- dars/core/state.py +309 -0
- dars/dars_tests/apps_test/health_check.py +56 -0
- dars/dars_tests/run_tests.py +275 -0
- dars/dars_tests/tests/test_advanced_components.py +69 -0
- dars/dars_tests/tests/test_basic_components.py +88 -0
- dars/dars_tests/tests/test_core_and_cli.py +17 -0
- dars/dars_tests/tests/test_layout_components.py +58 -0
- dars/dars_tests/tests/test_version_check.py +21 -0
- dars/docs/__init__.py +0 -0
- dars/docs/app.md +290 -0
- dars/docs/cli.md +80 -0
- dars/docs/components.md +1679 -0
- dars/docs/custom_components.md +30 -0
- dars/docs/events.md +45 -0
- dars/docs/exporters.md +162 -0
- dars/docs/getting_started.md +79 -0
- dars/docs/index.md +18 -0
- dars/docs/scripts.md +593 -0
- dars/docs/state_management.md +57 -0
- dars/exporters/__init__.py +0 -0
- dars/exporters/base.py +96 -0
- dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
- dars/exporters/web/OLD/html_css_js_old.py +1406 -0
- dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
- dars/exporters/web/__init__.py +0 -0
- dars/exporters/web/html_css_js.py +2675 -0
- dars/exporters/web/vdom.py +251 -0
- dars/js_lib.py +206 -0
- dars/scripts/__init__.py +0 -0
- dars/scripts/dscript.py +26 -0
- dars/scripts/script.py +39 -0
- dars/security.py +195 -0
- dars/templates/__init__.py +0 -0
- dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- dars/templates/examples/README.md +4 -0
- dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
- dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
- dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
- dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
- dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
- dars/templates/examples/advanced/__init__.py +0 -0
- dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
- dars/templates/examples/basic/Forms/form_components.py +516 -0
- dars/templates/examples/basic/Forms/simple_form.py +379 -0
- dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
- dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
- dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
- dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
- dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
- dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
- dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
- dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
- dars/templates/examples/basic/__init__.py +0 -0
- dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
- dars/templates/examples/demo/complete_app.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
- dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
- dars/templates/examples/markdown/__init__.py +0 -0
- dars/templates/html/__init__.py +0 -0
- dars/version.py +2 -0
- dars_framework-1.2.3.dist-info/METADATA +15 -0
- dars_framework-1.2.3.dist-info/RECORD +118 -0
- dars_framework-1.2.3.dist-info/WHEEL +5 -0
- dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
- dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
- dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/cli/main.py
ADDED
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Dars Exporter - Command line tool for exporting Dars applications
|
|
4
|
+
"""
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import venv
|
|
8
|
+
from rich.prompt import Confirm
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import importlib.util
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Dict, Any
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
from rich.markdown import Markdown
|
|
24
|
+
from rich import print as rprint
|
|
25
|
+
from importlib import resources
|
|
26
|
+
|
|
27
|
+
# Importar exportadores
|
|
28
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
29
|
+
|
|
30
|
+
from dars.core.app import App
|
|
31
|
+
from dars.exporters.web.html_css_js import HTMLCSSJSExporter
|
|
32
|
+
from dars.cli.translations import translator
|
|
33
|
+
from dars.config import load_config, resolve_paths, write_default_config, update_config
|
|
34
|
+
from dars.cli.doctor.preflight import check_and_gate
|
|
35
|
+
from dars.cli.doctor.doctor import run_doctor, run_forcedev
|
|
36
|
+
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
class RichHelpFormatter(argparse.HelpFormatter):
|
|
40
|
+
"""Custom formatter for argparse help using Rich"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, prog, indent_increment=2, max_help_position=24, width=None):
|
|
43
|
+
super().__init__(prog, indent_increment, max_help_position, width)
|
|
44
|
+
|
|
45
|
+
def format_help(self):
|
|
46
|
+
# Call the original method to get the help text
|
|
47
|
+
help_text = super().format_help()
|
|
48
|
+
return help_text
|
|
49
|
+
|
|
50
|
+
def add_text(self, text):
|
|
51
|
+
# Override this method to prevent the epilog from being shown in the options section
|
|
52
|
+
if text and (text.startswith('\nEjemplos de uso:') or text.startswith('\nUsage examples:')):
|
|
53
|
+
return
|
|
54
|
+
return super().add_text(text)
|
|
55
|
+
|
|
56
|
+
def _format_action(self, action):
|
|
57
|
+
# Check if this is the help action and replace its help message with the translated one
|
|
58
|
+
if action.option_strings and ('-h' in action.option_strings or '--help' in action.option_strings):
|
|
59
|
+
action.help = translator.get('help_arg_message')
|
|
60
|
+
return super()._format_action(action)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def rich_print_help(cls, parser, console=console):
|
|
64
|
+
# Get the standard help text
|
|
65
|
+
help_text = parser.format_help()
|
|
66
|
+
|
|
67
|
+
# Extract the main sections
|
|
68
|
+
sections = {}
|
|
69
|
+
current_section = None
|
|
70
|
+
lines = help_text.split('\n')
|
|
71
|
+
section_content = []
|
|
72
|
+
|
|
73
|
+
for line in lines:
|
|
74
|
+
if line and not line.startswith(' ') and line.endswith(':'):
|
|
75
|
+
# It's a section header
|
|
76
|
+
if current_section:
|
|
77
|
+
sections[current_section] = '\n'.join(section_content)
|
|
78
|
+
current_section = line[:-1] # Remove the colon
|
|
79
|
+
section_content = []
|
|
80
|
+
elif current_section:
|
|
81
|
+
section_content.append(line)
|
|
82
|
+
|
|
83
|
+
# Add the last section
|
|
84
|
+
if current_section and section_content:
|
|
85
|
+
sections[current_section] = '\n'.join(section_content)
|
|
86
|
+
|
|
87
|
+
# Show the program title
|
|
88
|
+
prog_name = parser.prog
|
|
89
|
+
description = parser.description
|
|
90
|
+
|
|
91
|
+
# Main panel
|
|
92
|
+
console.print(Panel(
|
|
93
|
+
Text(prog_name, style="bold cyan", justify="center"),
|
|
94
|
+
subtitle=translator.get('cli_subtitle'),
|
|
95
|
+
border_style="cyan"
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
# Check if there are examples in the epilog
|
|
99
|
+
epilog_content = ""
|
|
100
|
+
if parser.epilog:
|
|
101
|
+
epilog_content = parser.epilog.strip()
|
|
102
|
+
|
|
103
|
+
# Show each section with style
|
|
104
|
+
for section, content in sections.items():
|
|
105
|
+
if section == 'usage':
|
|
106
|
+
# Usage section
|
|
107
|
+
usage = content.strip()
|
|
108
|
+
console.print(f"\n[bold cyan]{translator.get('usage')}:[/bold cyan]")
|
|
109
|
+
console.print(Syntax(usage, "bash", theme="monokai", word_wrap=True))
|
|
110
|
+
elif 'positional arguments' in section.lower():
|
|
111
|
+
# Positional arguments
|
|
112
|
+
console.print(f"\n[bold cyan]{translator.get('positional_arguments')}:[/bold cyan]")
|
|
113
|
+
_print_arguments_table(content)
|
|
114
|
+
elif 'optional arguments' in section.lower() or 'options' in section.lower():
|
|
115
|
+
# Optional arguments
|
|
116
|
+
console.print(f"\n[bold cyan]{translator.get('options')}:[/bold cyan]")
|
|
117
|
+
_print_arguments_table(content)
|
|
118
|
+
elif section.lower() == 'commands' or 'subcommands' in section.lower():
|
|
119
|
+
# Subcommands
|
|
120
|
+
console.print(f"\n[bold cyan]{translator.get('commands')}:[/bold cyan]")
|
|
121
|
+
_print_arguments_table(content)
|
|
122
|
+
elif 'examples' in section.lower() or section.lower() == 'epilog':
|
|
123
|
+
# We don't process examples here to avoid duplication
|
|
124
|
+
pass
|
|
125
|
+
elif section.lower() != 'usage examples':
|
|
126
|
+
# Other sections (skip 'usage examples' to avoid duplication)
|
|
127
|
+
console.print(f"\n[bold cyan]{section.upper()}:[/bold cyan]")
|
|
128
|
+
console.print(content.strip())
|
|
129
|
+
|
|
130
|
+
# Always show examples at the end
|
|
131
|
+
console.print(f"\n[bold cyan]{translator.get('examples')}:[/bold cyan]")
|
|
132
|
+
# Get the actual examples from translations
|
|
133
|
+
examples_text = translator.get('examples_text')
|
|
134
|
+
examples = [line.strip() for line in examples_text.strip().split('\n') if line.strip()]
|
|
135
|
+
|
|
136
|
+
examples_table = Table(box=None, expand=True, show_header=False, padding=(0, 1, 0, 1))
|
|
137
|
+
examples_table.add_column("Example", overflow="fold")
|
|
138
|
+
|
|
139
|
+
for example in examples:
|
|
140
|
+
if example.strip():
|
|
141
|
+
examples_table.add_row(Syntax(example.strip(), "bash", theme="monokai"))
|
|
142
|
+
|
|
143
|
+
console.print(Panel(examples_table, border_style="cyan", padding=(1, 2)))
|
|
144
|
+
|
|
145
|
+
def pretty_print_help(parser: argparse.ArgumentParser) -> None:
|
|
146
|
+
# Just print argparse help (no custom header)
|
|
147
|
+
parser.print_help()
|
|
148
|
+
|
|
149
|
+
def _print_arguments_table(content):
|
|
150
|
+
"""Prints a table of arguments from the text content"""
|
|
151
|
+
table = Table(show_header=False, box=None, padding=(0, 2, 0, 0), expand=True)
|
|
152
|
+
table.add_column(translator.get('argument_column'), style="bold green", width=30, no_wrap=True)
|
|
153
|
+
table.add_column(translator.get('description_column'), style="dim white", overflow="fold")
|
|
154
|
+
|
|
155
|
+
lines = content.strip().split('\n')
|
|
156
|
+
current_arg = None
|
|
157
|
+
current_desc = []
|
|
158
|
+
|
|
159
|
+
for line in lines:
|
|
160
|
+
if line.strip():
|
|
161
|
+
if not line.startswith(' '):
|
|
162
|
+
# Es un nuevo argumento
|
|
163
|
+
if current_arg:
|
|
164
|
+
# Estilizar el argumento
|
|
165
|
+
styled_arg = current_arg
|
|
166
|
+
if '-' in styled_arg:
|
|
167
|
+
# Resaltar las opciones cortas y largas
|
|
168
|
+
parts = styled_arg.split(', ')
|
|
169
|
+
styled_parts = []
|
|
170
|
+
for part in parts:
|
|
171
|
+
if part.startswith('--'):
|
|
172
|
+
styled_parts.append(f"[cyan]{part}[/cyan]")
|
|
173
|
+
elif part.startswith('-'):
|
|
174
|
+
styled_parts.append(f"[green]{part}[/green]")
|
|
175
|
+
else:
|
|
176
|
+
styled_parts.append(part)
|
|
177
|
+
styled_arg = ", ".join(styled_parts)
|
|
178
|
+
|
|
179
|
+
table.add_row(styled_arg, '\n'.join(current_desc))
|
|
180
|
+
|
|
181
|
+
parts = line.strip().split(' ', 1)
|
|
182
|
+
current_arg = parts[0].strip()
|
|
183
|
+
current_desc = [parts[1].strip()] if len(parts) > 1 else []
|
|
184
|
+
else:
|
|
185
|
+
# Es continuación de la descripción
|
|
186
|
+
current_desc.append(line.strip())
|
|
187
|
+
|
|
188
|
+
# Añadir el último argumento
|
|
189
|
+
if current_arg:
|
|
190
|
+
# Estilizar el último argumento
|
|
191
|
+
styled_arg = current_arg
|
|
192
|
+
if '-' in styled_arg:
|
|
193
|
+
# Resaltar las opciones cortas y largas
|
|
194
|
+
parts = styled_arg.split(', ')
|
|
195
|
+
styled_parts = []
|
|
196
|
+
for part in parts:
|
|
197
|
+
if part.startswith('--'):
|
|
198
|
+
styled_parts.append(f"[cyan]{part}[/cyan]")
|
|
199
|
+
elif part.startswith('-'):
|
|
200
|
+
styled_parts.append(f"[green]{part}[/green]")
|
|
201
|
+
else:
|
|
202
|
+
styled_parts.append(part)
|
|
203
|
+
styled_arg = ", ".join(styled_parts)
|
|
204
|
+
|
|
205
|
+
table.add_row(styled_arg, '\n'.join(current_desc))
|
|
206
|
+
|
|
207
|
+
console.print(table)
|
|
208
|
+
|
|
209
|
+
class DarsExporter:
|
|
210
|
+
"""Exportador principal de Dars"""
|
|
211
|
+
|
|
212
|
+
def __init__(self):
|
|
213
|
+
self.exporters = {
|
|
214
|
+
'html': HTMLCSSJSExporter()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def load_app_from_file(self, file_path: str) -> Optional[App]:
|
|
218
|
+
"""Loads a Dars application from a Python file"""
|
|
219
|
+
try:
|
|
220
|
+
# Verify that the file exists
|
|
221
|
+
if not os.path.exists(file_path):
|
|
222
|
+
console.print(f"[red]{translator.get('error_file_not_exists')} {file_path}[/red]")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
# Add the application's root directory to sys.path
|
|
226
|
+
file_dir = os.path.dirname(os.path.abspath(file_path))
|
|
227
|
+
if file_dir not in sys.path:
|
|
228
|
+
sys.path.insert(0, file_dir)
|
|
229
|
+
|
|
230
|
+
# Load the module dynamically
|
|
231
|
+
spec = importlib.util.spec_from_file_location("user_app", file_path)
|
|
232
|
+
if spec is None or spec.loader is None:
|
|
233
|
+
console.print(f"[red]{translator.get('error_file_load')} {file_path}[/red]")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
module = importlib.util.module_from_spec(spec)
|
|
237
|
+
spec.loader.exec_module(module)
|
|
238
|
+
|
|
239
|
+
# Look for the 'app' variable in the module
|
|
240
|
+
if hasattr(module, 'app') and isinstance(module.app, App):
|
|
241
|
+
return module.app
|
|
242
|
+
else:
|
|
243
|
+
console.print(f"[red]{translator.get('error_no_app_var')} {file_path}[/red]")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
console.print(f"[red]{translator.get('error_loading_file')}: {e}[/red]")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def validate_app(self, app: App) -> bool:
|
|
251
|
+
"""Validates a Dars application"""
|
|
252
|
+
errors = app.validate()
|
|
253
|
+
|
|
254
|
+
if errors:
|
|
255
|
+
console.print(f"[red]{translator.get('validation_errors')}[/red]")
|
|
256
|
+
for error in errors:
|
|
257
|
+
console.print(f" • {error}")
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
def export_app(self, app: App, format_name: str, output_path: str, show_preview: bool = False) -> bool:
|
|
263
|
+
"""Exports an application to the specified format"""
|
|
264
|
+
|
|
265
|
+
if format_name not in self.exporters:
|
|
266
|
+
console.print(f"[red]{translator.get('error_format_not_supported')} '{format_name}'[/red]")
|
|
267
|
+
self.show_supported_formats()
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
exporter = self.exporters[format_name]
|
|
271
|
+
|
|
272
|
+
with Progress(
|
|
273
|
+
SpinnerColumn(),
|
|
274
|
+
TextColumn("[progress.description]{task.description}"),
|
|
275
|
+
BarColumn(),
|
|
276
|
+
TaskProgressColumn(),
|
|
277
|
+
console=console
|
|
278
|
+
) as progress:
|
|
279
|
+
|
|
280
|
+
# Validation task
|
|
281
|
+
task1 = progress.add_task(translator.get('validating_app'), total=100)
|
|
282
|
+
progress.update(task1, advance=30)
|
|
283
|
+
|
|
284
|
+
if not self.validate_app(app):
|
|
285
|
+
progress.update(task1, completed=100)
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
progress.update(task1, advance=70)
|
|
289
|
+
|
|
290
|
+
# Export task
|
|
291
|
+
task2 = progress.add_task(f"{translator.get('exporting_to')} {format_name}...", total=100)
|
|
292
|
+
progress.update(task2, advance=20)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# En CLI 'dars export', generamos un bundle final (sin hot-reload dev)
|
|
296
|
+
success = exporter.export(app, output_path, bundle=True)
|
|
297
|
+
progress.update(task2, advance=80)
|
|
298
|
+
|
|
299
|
+
if success:
|
|
300
|
+
# Minification step for bundle
|
|
301
|
+
try:
|
|
302
|
+
from dars.security import minify_output_dir
|
|
303
|
+
# Use actual file count progress
|
|
304
|
+
task3 = progress.add_task("Applying minification (bundle)", total=1)
|
|
305
|
+
totals = {"total": 1, "inited": False}
|
|
306
|
+
def _cb(done, total):
|
|
307
|
+
# Initialize task total once when known
|
|
308
|
+
if not totals["inited"] and total > 0:
|
|
309
|
+
progress.update(task3, total=total)
|
|
310
|
+
totals["total"] = total
|
|
311
|
+
totals["inited"] = True
|
|
312
|
+
progress.update(task3, completed=done)
|
|
313
|
+
_ = minify_output_dir(output_path, progress_cb=_cb)
|
|
314
|
+
# Ensure completed
|
|
315
|
+
progress.update(task3, completed=totals.get("total", 1))
|
|
316
|
+
except Exception:
|
|
317
|
+
# Do not fail export on minification errors
|
|
318
|
+
pass
|
|
319
|
+
progress.update(task1, completed=100)
|
|
320
|
+
progress.update(task2, completed=100)
|
|
321
|
+
|
|
322
|
+
# Show success information
|
|
323
|
+
self.show_export_success(app, format_name, output_path)
|
|
324
|
+
|
|
325
|
+
if show_preview and format_name == 'html':
|
|
326
|
+
self.show_preview_info(output_path)
|
|
327
|
+
|
|
328
|
+
return True
|
|
329
|
+
else:
|
|
330
|
+
console.print(f"[red]{translator.get('error_during_export')} {format_name}[/red]")
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
console.print(f"[red]{translator.get('error_during_export_exception')}: {e}[/red]")
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def show_supported_formats(self):
|
|
338
|
+
"""Shows supported formats"""
|
|
339
|
+
table = Table(title=translator.get('supported_export_formats'))
|
|
340
|
+
table.add_column(translator.get('format_name'), style="cyan")
|
|
341
|
+
table.add_column(translator.get('format_description'), style="white")
|
|
342
|
+
table.add_column(translator.get('html_description'), style="green")
|
|
343
|
+
|
|
344
|
+
formats_info = {
|
|
345
|
+
'html': ('HTML/CSS/JavaScript', 'Web'),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for format_name, (description, platform) in formats_info.items():
|
|
349
|
+
table.add_row(format_name, description, platform)
|
|
350
|
+
|
|
351
|
+
console.print(table)
|
|
352
|
+
|
|
353
|
+
def show_export_success(self, app: App, format_name: str, output_path: str):
|
|
354
|
+
"""Shows export success information"""
|
|
355
|
+
stats = app.get_stats()
|
|
356
|
+
|
|
357
|
+
panel_content = f"""
|
|
358
|
+
[green]✓[/green] {translator.get('export_completed_successfully')}
|
|
359
|
+
|
|
360
|
+
[bold]{translator.get('application')}:[/bold] {app.title}
|
|
361
|
+
[bold]{translator.get('format')}:[/bold] {format_name}
|
|
362
|
+
[bold]{translator.get('output_directory')}:[/bold] {output_path}
|
|
363
|
+
|
|
364
|
+
[bold]{translator.get('statistics')}:[/bold]
|
|
365
|
+
• {translator.get('total_components')}: {stats['total_components']}
|
|
366
|
+
• {translator.get('total_pages')}: {stats.get('total_pages', 1)}
|
|
367
|
+
• {translator.get('max_depth')}: {stats['max_depth']}
|
|
368
|
+
• {translator.get('scripts')}: {stats['scripts_count']}
|
|
369
|
+
• {translator.get('global_styles')}: {stats['global_styles_count']}
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
console.print(Panel(panel_content, title=translator.get('export_successful'), border_style="green"))
|
|
373
|
+
|
|
374
|
+
def show_preview_info(self, output_path: str):
|
|
375
|
+
"""Shows information about how to preview the application"""
|
|
376
|
+
index_path = os.path.join(output_path, "index.html")
|
|
377
|
+
|
|
378
|
+
if os.path.exists(index_path):
|
|
379
|
+
console.print(f"\n[bold cyan]{translator.get('to_preview_app')}:[/bold cyan]")
|
|
380
|
+
console.print(f" {translator.get('open_in_browser')}: file://{os.path.abspath(index_path)}")
|
|
381
|
+
console.print(f" {translator.get('or_use')}: dars preview {output_path}")
|
|
382
|
+
|
|
383
|
+
def show_app_info(self, app: App):
|
|
384
|
+
"""Shows detailed information about the application"""
|
|
385
|
+
stats = app.get_stats()
|
|
386
|
+
|
|
387
|
+
# Basic information
|
|
388
|
+
info_table = Table(title=f"{translator.get('app_information')}: {app.title}")
|
|
389
|
+
info_table.add_column(translator.get('property_column'), style="cyan")
|
|
390
|
+
info_table.add_column(translator.get('value_column'), style="white")
|
|
391
|
+
|
|
392
|
+
info_table.add_row(translator.get('title'), app.title)
|
|
393
|
+
info_table.add_row(translator.get('total_components'), str(stats['total_components']))
|
|
394
|
+
info_table.add_row(translator.get('max_depth'), str(stats['max_depth']))
|
|
395
|
+
info_table.add_row(translator.get('scripts'), str(stats['scripts_count']))
|
|
396
|
+
info_table.add_row(translator.get('global_styles'), str(stats['global_styles_count']))
|
|
397
|
+
info_table.add_row(translator.get('theme'), app.config.get('theme', 'light'))
|
|
398
|
+
|
|
399
|
+
console.print(info_table)
|
|
400
|
+
|
|
401
|
+
# Component tree
|
|
402
|
+
if app.root:
|
|
403
|
+
console.print(f"\n[bold]{translator.get('component_structure')}:[/bold]")
|
|
404
|
+
self.print_component_tree(app.root)
|
|
405
|
+
|
|
406
|
+
def print_component_tree(self, component, level: int = 0):
|
|
407
|
+
"""Prints the component tree"""
|
|
408
|
+
indent = " " * level
|
|
409
|
+
component_name = component.__class__.__name__
|
|
410
|
+
component_id = f" (id: {component.id})" if component.id else ""
|
|
411
|
+
|
|
412
|
+
console.print(f"{indent}├─ {component_name}{component_id}")
|
|
413
|
+
|
|
414
|
+
for child in component.children:
|
|
415
|
+
self.print_component_tree(child, level + 1)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def init_project(self, name: str, template: Optional[str] = None):
|
|
419
|
+
"""Initializes a base Dars project, optionally using a template"""
|
|
420
|
+
if os.path.exists(name):
|
|
421
|
+
console.print(f"[red]❌ {translator.get('directory_exists').format(name=name)}[/red]")
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Create project directory
|
|
425
|
+
os.makedirs(name)
|
|
426
|
+
console.print(f"[green]✔ {translator.get('directory_created').format(name=name)}[/green]")
|
|
427
|
+
|
|
428
|
+
if template:
|
|
429
|
+
# Get template information
|
|
430
|
+
templates = list_templates()
|
|
431
|
+
if template not in templates:
|
|
432
|
+
console.print(f"[red]❌ {translator.get('template_not_found').format(template=template)}[/red]")
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
template_info = templates[template]
|
|
436
|
+
template_dir = template_info['template_dir']
|
|
437
|
+
extra_files = template_info['extra_files']
|
|
438
|
+
|
|
439
|
+
if not extra_files:
|
|
440
|
+
console.print(f"[yellow]⚠ {translator.get('template_empty').format(template=template)}[/yellow]")
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
# Copy ALL files (no main_file anymore)
|
|
444
|
+
for extra_file in extra_files:
|
|
445
|
+
src_file = template_dir / extra_file
|
|
446
|
+
dest_file = os.path.join(name, extra_file)
|
|
447
|
+
|
|
448
|
+
# Create directories if needed
|
|
449
|
+
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
|
|
450
|
+
|
|
451
|
+
if src_file.exists():
|
|
452
|
+
shutil.copy2(src_file, dest_file)
|
|
453
|
+
console.print(f"[green]✔ {translator.get('extra_file_copied').format(file=extra_file)}[/green]")
|
|
454
|
+
|
|
455
|
+
console.print(f"[green]✔ {translator.get('template_copied').format(template=template)}[/green]")
|
|
456
|
+
else:
|
|
457
|
+
# Default hello world code (sin template)
|
|
458
|
+
HELLO_WORLD_CODE = """
|
|
459
|
+
from dars.all import *
|
|
460
|
+
|
|
461
|
+
app = App(title="Hello World", theme="dark")
|
|
462
|
+
# Crear componentes
|
|
463
|
+
index = Page(
|
|
464
|
+
Text(
|
|
465
|
+
text="Hello World",
|
|
466
|
+
style={
|
|
467
|
+
'font-size': '48px',
|
|
468
|
+
'color': '#2c3e50',
|
|
469
|
+
'margin-bottom': '20px',
|
|
470
|
+
'font-weight': 'bold',
|
|
471
|
+
'text-align': 'center'
|
|
472
|
+
}
|
|
473
|
+
),
|
|
474
|
+
Text(
|
|
475
|
+
text="Hello World",
|
|
476
|
+
style={
|
|
477
|
+
'font-size': '20px',
|
|
478
|
+
'color': '#7f8c8d',
|
|
479
|
+
'margin-bottom': '40px',
|
|
480
|
+
'text-align': 'center'
|
|
481
|
+
}
|
|
482
|
+
),
|
|
483
|
+
|
|
484
|
+
Button(
|
|
485
|
+
text="Click Me!",
|
|
486
|
+
on_click= dScript("alert('Hello World')"),
|
|
487
|
+
on_mouse_enter=dScript("this.style.backgroundColor = '#2980b9';"),
|
|
488
|
+
on_mouse_leave=dScript("this.style.backgroundColor = '#3498db';"),
|
|
489
|
+
style={
|
|
490
|
+
'background-color': '#3498db',
|
|
491
|
+
'color': 'white',
|
|
492
|
+
'padding': '15px 30px',
|
|
493
|
+
'border': 'none',
|
|
494
|
+
'border-radius': '8px',
|
|
495
|
+
'font-size': '18px',
|
|
496
|
+
'cursor': 'pointer',
|
|
497
|
+
'transition': 'background-color 0.3s'
|
|
498
|
+
}
|
|
499
|
+
),
|
|
500
|
+
style={
|
|
501
|
+
'display': 'flex',
|
|
502
|
+
'flex-direction': 'column',
|
|
503
|
+
'align-items': 'center',
|
|
504
|
+
'justify-content': 'center',
|
|
505
|
+
'min-height': '100vh',
|
|
506
|
+
'background-color': '#f0f2f5',
|
|
507
|
+
'font-family': 'Arial, sans-serif'
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
app.add_page("index", index, title="Hello World", index=True)
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
app.rTimeCompile()
|
|
515
|
+
"""
|
|
516
|
+
main_py = Path(name) / "main.py"
|
|
517
|
+
main_py.write_text(HELLO_WORLD_CODE.strip(), encoding="utf-8")
|
|
518
|
+
console.print(f"[green]✔ {translator.get('main_py_created')}[/green]")
|
|
519
|
+
|
|
520
|
+
# Create default dars.config.json for the new project
|
|
521
|
+
try:
|
|
522
|
+
write_default_config(os.path.abspath(name), overwrite=False)
|
|
523
|
+
console.print("[green]✔ dars.config.json created[/green]")
|
|
524
|
+
except Exception:
|
|
525
|
+
# Non-fatal; keep init working even if config write fails
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
# Final instructions
|
|
529
|
+
console.print(f"\n[bold cyan]🎉 {translator.get('project_initialized')}[/bold cyan]")
|
|
530
|
+
console.print(Syntax(f"cd {name}", "bash"))
|
|
531
|
+
console.print(Syntax(f"\n{translator.get('export_command')}:", "bash"))
|
|
532
|
+
console.print(Syntax(f"dars export (python file) --format html --output build", "bash"))
|
|
533
|
+
console.print(Syntax(f"\n{translator.get('preview_command')}:", "bash"))
|
|
534
|
+
console.print(Syntax(f"python (python file)", "bash"))
|
|
535
|
+
|
|
536
|
+
def print_version_info():
|
|
537
|
+
import importlib.util
|
|
538
|
+
import os
|
|
539
|
+
from rich.panel import Panel
|
|
540
|
+
from rich.console import Console
|
|
541
|
+
console = Console()
|
|
542
|
+
version_path = os.path.join(os.path.dirname(__file__), '../version.py')
|
|
543
|
+
spec = importlib.util.spec_from_file_location("dars.version", version_path)
|
|
544
|
+
version_mod = importlib.util.module_from_spec(spec)
|
|
545
|
+
spec.loader.exec_module(version_mod)
|
|
546
|
+
version = getattr(version_mod, "__version__", "unknown")
|
|
547
|
+
release_url = getattr(version_mod, "__release_url__", "https://github.com/ZtaMDev/Dars-Framework/releases")
|
|
548
|
+
panel_content = f"[bold cyan]Dars Framework[/bold cyan]\n\n[green]Version:[/green] {version}\n[green]Release notes:[/green] [link={release_url}]{release_url}[/link]"
|
|
549
|
+
console.print(Panel(panel_content, title="Dars Version", border_style="cyan"))
|
|
550
|
+
|
|
551
|
+
def create_parser(include_hidden: bool = True) -> argparse.ArgumentParser:
|
|
552
|
+
"""Creates the command line argument parser"""
|
|
553
|
+
parser = argparse.ArgumentParser(
|
|
554
|
+
description=translator.get('main_description'),
|
|
555
|
+
formatter_class=argparse.HelpFormatter,
|
|
556
|
+
epilog=""
|
|
557
|
+
)
|
|
558
|
+
parser.add_argument('-v', '--version', action='store_true', help='Show Dars version and release link')
|
|
559
|
+
|
|
560
|
+
# English-only: no language flag
|
|
561
|
+
|
|
562
|
+
subparsers = parser.add_subparsers(
|
|
563
|
+
dest='command',
|
|
564
|
+
help=translator.get('available_commands'),
|
|
565
|
+
metavar='{export,info,formats,preview,init,build,config,dev,doctor}'
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Export command
|
|
569
|
+
export_parser = subparsers.add_parser('export', help=translator.get('export_help'))
|
|
570
|
+
export_parser.add_argument('file', help=translator.get('file_help'))
|
|
571
|
+
|
|
572
|
+
# --format opcional (default: html)
|
|
573
|
+
export_parser.add_argument(
|
|
574
|
+
'--format', '-f',
|
|
575
|
+
choices=["html"],
|
|
576
|
+
default="html",
|
|
577
|
+
help=translator.get('format_help') + " (default: html)"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# --output opcional (default: ./dist)
|
|
581
|
+
export_parser.add_argument(
|
|
582
|
+
'--output', '-o',
|
|
583
|
+
default="./dist",
|
|
584
|
+
help=translator.get('output_help') + " (default: ./dist)"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
export_parser.add_argument('--preview', '-p', action='store_true',
|
|
588
|
+
help=translator.get('preview_help'))
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# Info command
|
|
592
|
+
info_parser = subparsers.add_parser('info', help=translator.get('info_help'))
|
|
593
|
+
info_parser.add_argument('file', help=translator.get('file_help'))
|
|
594
|
+
|
|
595
|
+
# Formats command
|
|
596
|
+
formats_parser = subparsers.add_parser('formats', help=translator.get('formats_help'))
|
|
597
|
+
|
|
598
|
+
# Preview command
|
|
599
|
+
preview_parser = subparsers.add_parser('preview', help=translator.get('preview_cmd_help'))
|
|
600
|
+
preview_parser.add_argument('path', help=translator.get('path_help'))
|
|
601
|
+
|
|
602
|
+
init_parser = subparsers.add_parser('init', help=translator.get('init_help'))
|
|
603
|
+
init_parser.add_argument('name', nargs='?', help=translator.get('name_help'))
|
|
604
|
+
init_parser.add_argument(
|
|
605
|
+
'--list-templates', '-L', # Cambia -l por -L
|
|
606
|
+
action='store_true',
|
|
607
|
+
help=translator.get('list_templates_help')
|
|
608
|
+
)
|
|
609
|
+
init_parser.add_argument(
|
|
610
|
+
'--template', '-t',
|
|
611
|
+
help=translator.get('template_help')
|
|
612
|
+
)
|
|
613
|
+
init_parser.add_argument(
|
|
614
|
+
'--update', '-u',
|
|
615
|
+
action='store_true',
|
|
616
|
+
help='Create or update dars.config.json in the target (or current) directory'
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Build command (config-driven)
|
|
620
|
+
build_parser = subparsers.add_parser('build', help='Build using dars.config.json')
|
|
621
|
+
build_parser.add_argument(
|
|
622
|
+
'--project', '-p', default='.', help='Project root where dars.config.json resides (default: .)'
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Config command (validate)
|
|
626
|
+
config_parser = subparsers.add_parser('config', help='Manage and validate dars.config.json')
|
|
627
|
+
cfg_subparsers = config_parser.add_subparsers(dest='config_command')
|
|
628
|
+
cfg_validate = cfg_subparsers.add_parser('validate', help='Validate dars.config.json in a project')
|
|
629
|
+
cfg_validate.add_argument('--project', '-p', default='.', help='Project root (default: .)')
|
|
630
|
+
|
|
631
|
+
# Dev command (run entry in dev mode)
|
|
632
|
+
dev_parser = subparsers.add_parser('dev', help='Run the configured entry file in development mode')
|
|
633
|
+
dev_parser.add_argument('--project', '-p', default='.', help='Project root where dars.config.json resides (default: .)')
|
|
634
|
+
# English-only: no language option on subparsers
|
|
635
|
+
|
|
636
|
+
# Doctor command
|
|
637
|
+
doctor_parser = subparsers.add_parser('doctor', help='Check and install required external tools (Node LTS, Bun) and Python deps')
|
|
638
|
+
doctor_parser.add_argument('--check', action='store_true', help='Only verify environment and exit non-zero if missing')
|
|
639
|
+
doctor_parser.add_argument('--yes', '-y', action='store_true', help='Assume yes for all prompts')
|
|
640
|
+
doctor_parser.add_argument('--all', action='store_true', help='Install all missing items (with --yes for non-interactive)')
|
|
641
|
+
doctor_parser.add_argument('--force', action='store_true', help='Re-run checks even if environment was previously satisfied')
|
|
642
|
+
|
|
643
|
+
# Hidden forced installer (conditionally added to avoid appearing in help)
|
|
644
|
+
if include_hidden:
|
|
645
|
+
forcedev_parser = subparsers.add_parser('forcedev', help=argparse.SUPPRESS)
|
|
646
|
+
|
|
647
|
+
return parser
|
|
648
|
+
|
|
649
|
+
from pathlib import Path
|
|
650
|
+
from typing import Dict
|
|
651
|
+
|
|
652
|
+
from pathlib import Path
|
|
653
|
+
from typing import Dict
|
|
654
|
+
|
|
655
|
+
from pathlib import Path
|
|
656
|
+
from typing import Dict
|
|
657
|
+
|
|
658
|
+
from pathlib import Path
|
|
659
|
+
from typing import Dict
|
|
660
|
+
|
|
661
|
+
from pathlib import Path
|
|
662
|
+
from typing import Dict
|
|
663
|
+
|
|
664
|
+
from pathlib import Path
|
|
665
|
+
from typing import Dict
|
|
666
|
+
|
|
667
|
+
def list_templates(debug: bool = False) -> Dict[str, Dict]:
|
|
668
|
+
"""
|
|
669
|
+
Descubre templates:
|
|
670
|
+
- ignora dirs en IGNORED_DIRS (ej: __pycache__, .git, node_modules)
|
|
671
|
+
- ignora extensiones compiladas ('.pyc', '.pyo', '.pyd')
|
|
672
|
+
- ignora solo archivos ocultos que empiezan con '.' (ej: .env)
|
|
673
|
+
- incluye TODOS los demás archivos ('.py', '.md', '.png', '.json', etc.)
|
|
674
|
+
- salida determinista (ordenada)
|
|
675
|
+
"""
|
|
676
|
+
current_file = Path(__file__).resolve()
|
|
677
|
+
templates_base = current_file.parent.parent / "templates" / "examples"
|
|
678
|
+
|
|
679
|
+
if not templates_base.exists():
|
|
680
|
+
# usa console.print si tienes rich.console; aquí dejo print para compatibilidad
|
|
681
|
+
print(f"[red]Error: Template directory not found: {templates_base}[/red]")
|
|
682
|
+
return {}
|
|
683
|
+
|
|
684
|
+
IGNORED_DIRS = {'__pycache__', '.git', '.venv', 'node_modules', '.pytest_cache'}
|
|
685
|
+
IGNORE_EXTS = {'.pyc', '.pyo', '.pyd'}
|
|
686
|
+
|
|
687
|
+
templates: Dict[str, Dict] = {}
|
|
688
|
+
|
|
689
|
+
for category_dir in sorted(templates_base.iterdir()):
|
|
690
|
+
if not (category_dir.is_dir() and not category_dir.name.startswith('__')):
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
for template_dir in sorted(category_dir.iterdir()):
|
|
694
|
+
if not (template_dir.is_dir() and not template_dir.name.startswith('__')):
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
found_files = []
|
|
698
|
+
for file_path in sorted(template_dir.rglob('*')):
|
|
699
|
+
# 1) archivo
|
|
700
|
+
if not file_path.is_file():
|
|
701
|
+
if debug: print(f"SKIP (not file): {file_path}")
|
|
702
|
+
continue
|
|
703
|
+
|
|
704
|
+
# 2) si alguna parte del path es una carpeta ignorada
|
|
705
|
+
intersect = set(file_path.parts) & IGNORED_DIRS
|
|
706
|
+
if intersect:
|
|
707
|
+
if debug: print(f"SKIP (ignored dir {intersect}): {file_path}")
|
|
708
|
+
continue
|
|
709
|
+
|
|
710
|
+
# 3) extensiones compiladas
|
|
711
|
+
if file_path.suffix.lower() in IGNORE_EXTS:
|
|
712
|
+
if debug: print(f"SKIP (ignored ext): {file_path}")
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
# 4) solo ocultos que empiezan con '.' (por ejemplo .gitignore, .env)
|
|
716
|
+
if file_path.name.startswith('.'):
|
|
717
|
+
if debug: print(f"SKIP (hidden file): {file_path}")
|
|
718
|
+
continue
|
|
719
|
+
|
|
720
|
+
# si pasó todos los filtros, lo guardamos (ruta relativa al template)
|
|
721
|
+
rel = str(file_path.relative_to(template_dir))
|
|
722
|
+
if debug: print(f"INCLUDE: {rel}")
|
|
723
|
+
found_files.append(rel)
|
|
724
|
+
|
|
725
|
+
found_files = sorted(found_files)
|
|
726
|
+
|
|
727
|
+
template_key = f"{category_dir.name}/{template_dir.name}"
|
|
728
|
+
templates[template_key] = {
|
|
729
|
+
'main_file': None, # ya no usamos main_file
|
|
730
|
+
'extra_files': found_files,
|
|
731
|
+
'category': category_dir.name,
|
|
732
|
+
'template_dir': template_dir,
|
|
733
|
+
'all_files': found_files
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return templates
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def list_templates_detailed():
|
|
742
|
+
"""Muestra información detallada de los templates disponibles"""
|
|
743
|
+
templates = list_templates()
|
|
744
|
+
|
|
745
|
+
if not templates:
|
|
746
|
+
console.print("[yellow]No templates found[/yellow]")
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
table = Table(title="Available Templates")
|
|
750
|
+
table.add_column("Template", style="cyan")
|
|
751
|
+
table.add_column("Category", style="green")
|
|
752
|
+
table.add_column("Extra Files", style="white")
|
|
753
|
+
table.add_column("Description", style="dim")
|
|
754
|
+
|
|
755
|
+
for template_name, template_info in templates.items():
|
|
756
|
+
extra_files = ", ".join(template_info['extra_files']) if template_info['extra_files'] else "None"
|
|
757
|
+
table.add_row(
|
|
758
|
+
template_name,
|
|
759
|
+
template_info['category'],
|
|
760
|
+
extra_files,
|
|
761
|
+
f"Template with {len(template_info['extra_files'])} extra files"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
console.print(table)
|
|
765
|
+
def main():
|
|
766
|
+
"""Main CLI function"""
|
|
767
|
+
# English-only: no language parameter pre-scan
|
|
768
|
+
|
|
769
|
+
# Intercept only when no args provided; otherwise let argparse show the correct subcommand help
|
|
770
|
+
if len(sys.argv) == 1:
|
|
771
|
+
parser = create_parser(include_hidden=False)
|
|
772
|
+
pretty_print_help(parser)
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
# Continue with normal flow if not help
|
|
776
|
+
# If user asked for top-level help (no subcommand), build parser without hidden commands
|
|
777
|
+
known_cmds = ['export','info','formats','preview','init','build','config','dev','doctor']
|
|
778
|
+
top_level_help = ('-h' in sys.argv or '--help' in sys.argv) and not any(cmd in sys.argv for cmd in known_cmds)
|
|
779
|
+
parser = create_parser(include_hidden=not top_level_help)
|
|
780
|
+
if top_level_help:
|
|
781
|
+
pretty_print_help(create_parser(include_hidden=False))
|
|
782
|
+
return
|
|
783
|
+
args = parser.parse_args()
|
|
784
|
+
|
|
785
|
+
# Set language from args only if explicitly provided
|
|
786
|
+
# This is already handled in the pre-parsing step above, so we don't need to do it again
|
|
787
|
+
# The translator will already have the correct language set
|
|
788
|
+
|
|
789
|
+
# Show version and exit if -v/--version is passed
|
|
790
|
+
if getattr(args, 'version', False):
|
|
791
|
+
print_version_info()
|
|
792
|
+
sys.exit(0)
|
|
793
|
+
|
|
794
|
+
# No banner for normal commands; keep output minimal
|
|
795
|
+
|
|
796
|
+
exporter = DarsExporter()
|
|
797
|
+
|
|
798
|
+
# Run preflight gating for all commands except 'doctor'
|
|
799
|
+
if getattr(args, 'command', None) and args.command != 'doctor':
|
|
800
|
+
try:
|
|
801
|
+
check_and_gate(args.command)
|
|
802
|
+
except SystemExit as e:
|
|
803
|
+
# If doctor failed or user cancelled, abort the command
|
|
804
|
+
sys.exit(e.code if isinstance(e.code, int) else 1)
|
|
805
|
+
|
|
806
|
+
if args.command == 'export':
|
|
807
|
+
# If file points to config, resolve from dars.config.json
|
|
808
|
+
file_arg = args.file
|
|
809
|
+
if file_arg in ('.', 'config', 'cfg'):
|
|
810
|
+
project_root = os.getcwd()
|
|
811
|
+
cfg, _found = load_config(project_root)
|
|
812
|
+
resolved = resolve_paths(cfg, project_root)
|
|
813
|
+
file_arg = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
|
|
814
|
+
|
|
815
|
+
# Validate entry file exists
|
|
816
|
+
if not os.path.exists(file_arg):
|
|
817
|
+
console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {file_arg}[/red]")
|
|
818
|
+
console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
|
|
819
|
+
sys.exit(1)
|
|
820
|
+
|
|
821
|
+
# Load application
|
|
822
|
+
app = exporter.load_app_from_file(file_arg)
|
|
823
|
+
if app is None:
|
|
824
|
+
sys.exit(1)
|
|
825
|
+
|
|
826
|
+
# Export
|
|
827
|
+
# If config exists and user didn't override output explicitly, use cfg.outdir
|
|
828
|
+
project_root = os.path.dirname(os.path.abspath(file_arg))
|
|
829
|
+
cfg, cfg_found = load_config(project_root)
|
|
830
|
+
outdir = args.output
|
|
831
|
+
if cfg_found and (args.output == './dist' or args.output == 'dist'):
|
|
832
|
+
resolved = resolve_paths(cfg, project_root)
|
|
833
|
+
outdir = resolved.get('outdir_abs') or outdir
|
|
834
|
+
|
|
835
|
+
# Validate format (currently only html)
|
|
836
|
+
if args.format not in ['html']:
|
|
837
|
+
console.print(f"[red]{translator.get('error_format_only_html')}[/red]")
|
|
838
|
+
sys.exit(1)
|
|
839
|
+
|
|
840
|
+
# Ensure outdir can be created
|
|
841
|
+
try:
|
|
842
|
+
os.makedirs(outdir, exist_ok=True)
|
|
843
|
+
except Exception as e:
|
|
844
|
+
console.print(f"[red]{translator.get('error_output_create')}: {outdir} -> {e}[/red]")
|
|
845
|
+
sys.exit(1)
|
|
846
|
+
|
|
847
|
+
ensure_dars_lib(project_root)
|
|
848
|
+
success = exporter.export_app(app, args.format, outdir, args.preview)
|
|
849
|
+
sys.exit(0 if success else 1)
|
|
850
|
+
|
|
851
|
+
elif args.command == 'info':
|
|
852
|
+
# Show information
|
|
853
|
+
app = exporter.load_app_from_file(args.file)
|
|
854
|
+
if app is None:
|
|
855
|
+
sys.exit(1)
|
|
856
|
+
|
|
857
|
+
exporter.show_app_info(app)
|
|
858
|
+
|
|
859
|
+
elif args.command == 'formats':
|
|
860
|
+
# Show formats
|
|
861
|
+
exporter.show_supported_formats()
|
|
862
|
+
|
|
863
|
+
elif args.command == 'init':
|
|
864
|
+
if args.list_templates:
|
|
865
|
+
list_templates_detailed()
|
|
866
|
+
elif args.update:
|
|
867
|
+
# Update or create config in provided name or current directory
|
|
868
|
+
target_dir = args.name or '.'
|
|
869
|
+
project_root = os.path.abspath(target_dir)
|
|
870
|
+
os.makedirs(project_root, exist_ok=True)
|
|
871
|
+
write_default_config(project_root, overwrite=False)
|
|
872
|
+
ensure_dars_lib(project_root)
|
|
873
|
+
console.print("[green]✔ dars.config.json created/updated[/green]")
|
|
874
|
+
elif not args.name:
|
|
875
|
+
console.print("[red]Error: Project name is required[/red]")
|
|
876
|
+
parser.parse_args(['init', '--help'])
|
|
877
|
+
else:
|
|
878
|
+
exporter.init_project(args.name, template=args.template)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
elif args.command == 'build':
|
|
882
|
+
project_root = os.path.abspath(getattr(args, 'project', '.'))
|
|
883
|
+
cfg, found = load_config(project_root)
|
|
884
|
+
if not found:
|
|
885
|
+
console.print("[yellow][Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it.[/yellow]")
|
|
886
|
+
resolved = resolve_paths(cfg, project_root)
|
|
887
|
+
entry = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
|
|
888
|
+
format_name = cfg.get('format', 'html')
|
|
889
|
+
outdir = resolved.get('outdir_abs') or os.path.join(project_root, 'dist')
|
|
890
|
+
|
|
891
|
+
# Validate entry file exists
|
|
892
|
+
if not os.path.exists(entry):
|
|
893
|
+
console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {entry}[/red]")
|
|
894
|
+
console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
|
|
895
|
+
sys.exit(1)
|
|
896
|
+
|
|
897
|
+
# Validate format (currently only html)
|
|
898
|
+
if format_name not in ['html']:
|
|
899
|
+
console.print(f"[red]{translator.get('error_format_only_html')}[/red]")
|
|
900
|
+
sys.exit(1)
|
|
901
|
+
|
|
902
|
+
# Ensure outdir can be created
|
|
903
|
+
try:
|
|
904
|
+
os.makedirs(outdir, exist_ok=True)
|
|
905
|
+
except Exception as e:
|
|
906
|
+
console.print(f"[red]{translator.get('error_output_create')}: {outdir} -> {e}[/red]")
|
|
907
|
+
sys.exit(1)
|
|
908
|
+
|
|
909
|
+
ensure_dars_lib(project_root)
|
|
910
|
+
app = exporter.load_app_from_file(entry)
|
|
911
|
+
if app is None:
|
|
912
|
+
sys.exit(1)
|
|
913
|
+
success = exporter.export_app(app, format_name, outdir, show_preview=False)
|
|
914
|
+
sys.exit(0 if success else 1)
|
|
915
|
+
|
|
916
|
+
elif args.command == 'preview':
|
|
917
|
+
index_path = os.path.join(args.path, "index.html")
|
|
918
|
+
if os.path.exists(index_path):
|
|
919
|
+
console.print(f"[green]{translator.get('app_found')}: {args.path} [/green]")
|
|
920
|
+
console.print(f"{translator.get('open_in_browser')}: file://{os.path.abspath(index_path)}")
|
|
921
|
+
console.print(f"{translator.get('view_preview')} [green]y[/green] / [red]n[/red] [y/n] ")
|
|
922
|
+
if input().lower() == 'y':
|
|
923
|
+
# Pass the current language to preview.py
|
|
924
|
+
|
|
925
|
+
import subprocess
|
|
926
|
+
process = None
|
|
927
|
+
try:
|
|
928
|
+
process = subprocess.Popen([sys.executable, '-m', 'dars.cli.preview', args.path])
|
|
929
|
+
process.wait()
|
|
930
|
+
except KeyboardInterrupt:
|
|
931
|
+
if process:
|
|
932
|
+
process.terminate()
|
|
933
|
+
process.wait()
|
|
934
|
+
finally:
|
|
935
|
+
if process and process.poll() is None:
|
|
936
|
+
process.terminate()
|
|
937
|
+
process.wait()
|
|
938
|
+
else:
|
|
939
|
+
console.print(f"[red]{translator.get('index_not_found')} {args.path}[/red]")
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
elif args.command == 'config':
|
|
943
|
+
if getattr(args, 'config_command', None) == 'validate':
|
|
944
|
+
project_root = os.path.abspath(getattr(args, 'project', '.'))
|
|
945
|
+
cfg, found = load_config(project_root)
|
|
946
|
+
resolved = resolve_paths(cfg, project_root)
|
|
947
|
+
|
|
948
|
+
issues = []
|
|
949
|
+
def ok(msg):
|
|
950
|
+
return f"[green]✔ {msg}[/green]"
|
|
951
|
+
def warn(msg):
|
|
952
|
+
return f"[yellow]⚠ {msg}[/yellow]"
|
|
953
|
+
def err(msg):
|
|
954
|
+
return f"[red]✖ {msg}[/red]"
|
|
955
|
+
|
|
956
|
+
if not found:
|
|
957
|
+
issues.append(warn(translator.get('cfg_not_found_warn')))
|
|
958
|
+
|
|
959
|
+
# entry validation
|
|
960
|
+
entry = resolved.get('entry_abs')
|
|
961
|
+
if not entry or not os.path.isfile(entry):
|
|
962
|
+
issues.append(err(translator.get('cfg_entry_missing').format(path=cfg.get('entry'))))
|
|
963
|
+
else:
|
|
964
|
+
issues.append(ok(translator.get('cfg_entry_ok').format(path=cfg.get('entry'))))
|
|
965
|
+
|
|
966
|
+
# format validation
|
|
967
|
+
fmt = cfg.get('format')
|
|
968
|
+
if fmt != 'html':
|
|
969
|
+
issues.append(err(translator.get('cfg_format_only_html').format(fmt=fmt)))
|
|
970
|
+
else:
|
|
971
|
+
issues.append(ok(translator.get('cfg_format_ok').format(fmt=fmt)))
|
|
972
|
+
|
|
973
|
+
# outdir validation (creatable)
|
|
974
|
+
outdir_abs = resolved.get('outdir_abs')
|
|
975
|
+
try:
|
|
976
|
+
os.makedirs(outdir_abs, exist_ok=True)
|
|
977
|
+
issues.append(ok(translator.get('cfg_outdir_ok').format(path=cfg.get('outdir'))))
|
|
978
|
+
except Exception as e:
|
|
979
|
+
issues.append(err(translator.get('cfg_outdir_error').format(path=cfg.get('outdir'), error=str(e))))
|
|
980
|
+
|
|
981
|
+
# publicDir (if set) existence
|
|
982
|
+
pub = cfg.get('publicDir')
|
|
983
|
+
if pub:
|
|
984
|
+
pub_abs = resolved.get('public_abs')
|
|
985
|
+
if not pub_abs or not os.path.isdir(pub_abs):
|
|
986
|
+
issues.append(err(translator.get('cfg_public_missing').format(path=pub)))
|
|
987
|
+
else:
|
|
988
|
+
issues.append(ok(translator.get('cfg_public_ok').format(path=pub)))
|
|
989
|
+
else:
|
|
990
|
+
issues.append(warn(translator.get('cfg_public_autodetect')))
|
|
991
|
+
|
|
992
|
+
# include/exclude types
|
|
993
|
+
if not isinstance(cfg.get('include', []), list):
|
|
994
|
+
issues.append(err(translator.get('cfg_include_type')))
|
|
995
|
+
if not isinstance(cfg.get('exclude', []), list):
|
|
996
|
+
issues.append(err(translator.get('cfg_exclude_type')))
|
|
997
|
+
|
|
998
|
+
# bundle is bool
|
|
999
|
+
if not isinstance(cfg.get('bundle', False), bool):
|
|
1000
|
+
issues.append(err(translator.get('cfg_bundle_type')))
|
|
1001
|
+
|
|
1002
|
+
# Print report
|
|
1003
|
+
report = Table(title=translator.get('cfg_validation_title'))
|
|
1004
|
+
report.add_column(translator.get('cfg_item'), style="cyan")
|
|
1005
|
+
report.add_column(translator.get('cfg_result'), style="white")
|
|
1006
|
+
|
|
1007
|
+
report.add_row('config', translator.get('cfg_found') if found else translator.get('cfg_not_found'))
|
|
1008
|
+
for msg in issues:
|
|
1009
|
+
if 'entry' in msg:
|
|
1010
|
+
report.add_row('entry', msg)
|
|
1011
|
+
elif 'format' in msg:
|
|
1012
|
+
report.add_row('format', msg)
|
|
1013
|
+
elif 'outdir' in msg:
|
|
1014
|
+
report.add_row('outdir', msg)
|
|
1015
|
+
elif 'public' in msg or 'publicDir' in msg:
|
|
1016
|
+
report.add_row('publicDir', msg)
|
|
1017
|
+
elif 'include' in msg:
|
|
1018
|
+
report.add_row('include', msg)
|
|
1019
|
+
elif 'exclude' in msg:
|
|
1020
|
+
report.add_row('exclude', msg)
|
|
1021
|
+
elif 'bundle' in msg:
|
|
1022
|
+
report.add_row('bundle', msg)
|
|
1023
|
+
else:
|
|
1024
|
+
report.add_row('note', msg)
|
|
1025
|
+
|
|
1026
|
+
console.print(report)
|
|
1027
|
+
has_errors = any(msg.startswith('[red]') for msg in issues)
|
|
1028
|
+
sys.exit(1 if has_errors else 0)
|
|
1029
|
+
else:
|
|
1030
|
+
# Show help for config subcommands
|
|
1031
|
+
parser = create_parser(include_hidden=False)
|
|
1032
|
+
subparsers_actions = [action for action in parser._actions if isinstance(action, argparse._SubParsersAction)]
|
|
1033
|
+
for subparsers_action in subparsers_actions:
|
|
1034
|
+
if 'config' in subparsers_action.choices:
|
|
1035
|
+
pretty_print_help(subparsers_action.choices['config'])
|
|
1036
|
+
return
|
|
1037
|
+
|
|
1038
|
+
elif args.command == 'dev':
|
|
1039
|
+
# Resolve project and config
|
|
1040
|
+
project_root = os.path.abspath(getattr(args, 'project', '.'))
|
|
1041
|
+
cfg, found = load_config(project_root)
|
|
1042
|
+
if not found:
|
|
1043
|
+
console.print("[yellow][Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it.[/yellow]")
|
|
1044
|
+
resolved = resolve_paths(cfg, project_root)
|
|
1045
|
+
entry = resolved.get('entry_abs') or os.path.join(project_root, cfg.get('entry', 'main.py'))
|
|
1046
|
+
|
|
1047
|
+
if not os.path.exists(entry):
|
|
1048
|
+
console.print(f"[red]{translator.get('error_entry_not_found_in_config')}: {entry}[/red]")
|
|
1049
|
+
console.print(f"[yellow]{translator.get('edit_config_hint')}[/yellow]")
|
|
1050
|
+
sys.exit(1)
|
|
1051
|
+
|
|
1052
|
+
# Ensure dars.min.js exists in project
|
|
1053
|
+
ensure_dars_lib(project_root)
|
|
1054
|
+
# Run entry in development mode (the entry typically calls app.rTimeCompile())
|
|
1055
|
+
import subprocess
|
|
1056
|
+
process = None
|
|
1057
|
+
try:
|
|
1058
|
+
console.print(f"[cyan]Running dev: {entry}[/cyan]")
|
|
1059
|
+
process = subprocess.Popen([sys.executable, entry], cwd=os.path.dirname(entry))
|
|
1060
|
+
process.wait()
|
|
1061
|
+
sys.exit(process.returncode or 0)
|
|
1062
|
+
except KeyboardInterrupt:
|
|
1063
|
+
if process:
|
|
1064
|
+
process.terminate()
|
|
1065
|
+
process.wait()
|
|
1066
|
+
sys.exit(0)
|
|
1067
|
+
except Exception as e:
|
|
1068
|
+
console.print(f"[red]Failed to start dev process: {e}[/red]")
|
|
1069
|
+
sys.exit(1)
|
|
1070
|
+
|
|
1071
|
+
elif args.command == 'doctor':
|
|
1072
|
+
# Run doctor with provided flags
|
|
1073
|
+
code = run_doctor(
|
|
1074
|
+
check_only=getattr(args, 'check', False),
|
|
1075
|
+
auto_yes=getattr(args, 'yes', False),
|
|
1076
|
+
install_all=getattr(args, 'all', False),
|
|
1077
|
+
force=getattr(args, 'force', False)
|
|
1078
|
+
)
|
|
1079
|
+
sys.exit(code)
|
|
1080
|
+
|
|
1081
|
+
elif args.command == 'forcedev':
|
|
1082
|
+
# Hidden: force-install Node, Bun, and all Python deps without prompts
|
|
1083
|
+
code = run_forcedev()
|
|
1084
|
+
sys.exit(code)
|
|
1085
|
+
|
|
1086
|
+
else:
|
|
1087
|
+
# Fallback: pretty help with header
|
|
1088
|
+
pretty_print_help(parser)
|
|
1089
|
+
|
|
1090
|
+
# Utility: ensure lib/dars.min.js exists at project root (no overwrite)
|
|
1091
|
+
def ensure_dars_lib(project_root: str):
|
|
1092
|
+
try:
|
|
1093
|
+
os.makedirs(os.path.join(project_root, 'lib'), exist_ok=True)
|
|
1094
|
+
dest = os.path.join(project_root, 'lib', 'dars.min.js')
|
|
1095
|
+
if not os.path.exists(dest):
|
|
1096
|
+
try:
|
|
1097
|
+
from dars.js_lib import DARS_MIN_JS
|
|
1098
|
+
with open(dest, 'w', encoding='utf-8') as fdst:
|
|
1099
|
+
fdst.write(DARS_MIN_JS)
|
|
1100
|
+
except Exception:
|
|
1101
|
+
pass
|
|
1102
|
+
except Exception:
|
|
1103
|
+
pass
|
|
1104
|
+
|
|
1105
|
+
if __name__ == "__main__":
|
|
1106
|
+
main()
|
|
1107
|
+
|