quantum-framework 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI Engine - Cross-Target Validator
|
|
3
|
+
|
|
4
|
+
Analyzes UI AST to detect features that may not translate well between
|
|
5
|
+
different targets (HTML, Textual). Returns compatibility reports and warnings.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from quantum.runtime.ui_validator import UIValidator
|
|
9
|
+
|
|
10
|
+
validator = UIValidator()
|
|
11
|
+
report = validator.validate(app.ui_windows, app.ui_children)
|
|
12
|
+
|
|
13
|
+
print(f"Compatibility Score: {report.compatibility_score}%")
|
|
14
|
+
for warning in report.warnings:
|
|
15
|
+
print(f" - {warning}")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import List, Set, Dict, Optional
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
from quantum.core.ast_nodes import QuantumNode
|
|
23
|
+
from quantum.core.features.ui_engine.src.ast_nodes import (
|
|
24
|
+
UIWindowNode, UIHBoxNode, UIVBoxNode, UIPanelNode,
|
|
25
|
+
UITabPanelNode, UITabNode, UIGridNode, UIAccordionNode,
|
|
26
|
+
UISectionNode, UIDividedBoxNode, UIFormNode, UIFormItemNode,
|
|
27
|
+
UISpacerNode, UIScrollBoxNode,
|
|
28
|
+
UITextNode, UIButtonNode, UIInputNode, UICheckboxNode,
|
|
29
|
+
UIRadioNode, UISwitchNode, UISelectNode, UITableNode,
|
|
30
|
+
UIColumnNode, UIListNode, UIItemNode, UIImageNode,
|
|
31
|
+
UILinkNode, UIProgressNode, UITreeNode, UIMenuNode,
|
|
32
|
+
UIOptionNode, UILogNode, UIMarkdownNode, UIHeaderNode,
|
|
33
|
+
UIFooterNode, UIRuleNode, UILoadingNode, UIBadgeNode,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Severity(Enum):
|
|
38
|
+
"""Warning severity levels."""
|
|
39
|
+
INFO = "info" # FYI, no impact
|
|
40
|
+
WARNING = "warning" # Works differently
|
|
41
|
+
ERROR = "error" # Does not work
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CompatibilityIssue:
|
|
46
|
+
"""Single compatibility issue."""
|
|
47
|
+
feature: str
|
|
48
|
+
message: str
|
|
49
|
+
severity: Severity
|
|
50
|
+
target: str # Which target is affected
|
|
51
|
+
node_type: str
|
|
52
|
+
suggestion: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ValidationReport:
|
|
57
|
+
"""Complete validation report."""
|
|
58
|
+
issues: List[CompatibilityIssue] = field(default_factory=list)
|
|
59
|
+
features_used: Set[str] = field(default_factory=set)
|
|
60
|
+
widgets_count: int = 0
|
|
61
|
+
containers_count: int = 0
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def compatibility_score(self) -> int:
|
|
65
|
+
"""Calculate compatibility score (0-100) based on issues."""
|
|
66
|
+
if not self.issues:
|
|
67
|
+
return 100
|
|
68
|
+
|
|
69
|
+
# Weight by severity
|
|
70
|
+
error_count = sum(1 for i in self.issues if i.severity == Severity.ERROR)
|
|
71
|
+
warning_count = sum(1 for i in self.issues if i.severity == Severity.WARNING)
|
|
72
|
+
info_count = sum(1 for i in self.issues if i.severity == Severity.INFO)
|
|
73
|
+
|
|
74
|
+
# Calculate penalty
|
|
75
|
+
total_elements = max(1, self.widgets_count + self.containers_count)
|
|
76
|
+
penalty = (error_count * 20 + warning_count * 5 + info_count * 1)
|
|
77
|
+
score = max(0, 100 - penalty)
|
|
78
|
+
|
|
79
|
+
return score
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def html_compatible(self) -> bool:
|
|
83
|
+
"""Check if fully HTML compatible."""
|
|
84
|
+
return not any(i.target == 'html' and i.severity == Severity.ERROR
|
|
85
|
+
for i in self.issues)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def textual_compatible(self) -> bool:
|
|
89
|
+
"""Check if fully Textual compatible."""
|
|
90
|
+
return not any(i.target == 'textual' and i.severity == Severity.ERROR
|
|
91
|
+
for i in self.issues)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def warnings(self) -> List[str]:
|
|
95
|
+
"""Get formatted warning messages."""
|
|
96
|
+
return [f"[{i.severity.value}] {i.feature}: {i.message}"
|
|
97
|
+
for i in self.issues]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Feature compatibility matrix
|
|
101
|
+
FEATURE_COMPAT = {
|
|
102
|
+
# Feature: (html_support, textual_support, message, suggestion)
|
|
103
|
+
'image': (True, 'degraded',
|
|
104
|
+
"Images display as placeholder text in terminal",
|
|
105
|
+
"Consider using ASCII art or omit images for TUI target"),
|
|
106
|
+
|
|
107
|
+
'link_external': (True, 'degraded',
|
|
108
|
+
"External links are not clickable in terminal",
|
|
109
|
+
"Link URL is shown as text instead"),
|
|
110
|
+
|
|
111
|
+
'gap': (True, False,
|
|
112
|
+
"CSS gap property is not supported in Textual",
|
|
113
|
+
"Layout may differ; children won't have automatic spacing"),
|
|
114
|
+
|
|
115
|
+
'justify_between': (True, False,
|
|
116
|
+
"justify-content: space-between not available in Textual",
|
|
117
|
+
"Items will be centered instead"),
|
|
118
|
+
|
|
119
|
+
'justify_around': (True, False,
|
|
120
|
+
"justify-content: space-around not available in Textual",
|
|
121
|
+
"Items will be centered instead"),
|
|
122
|
+
|
|
123
|
+
'font_size': (True, False,
|
|
124
|
+
"Font sizes cannot be changed in terminal",
|
|
125
|
+
"Use text weight (bold) for emphasis instead"),
|
|
126
|
+
|
|
127
|
+
'pixel_units': (True, 'converted',
|
|
128
|
+
"Pixel values are converted to character cells (÷8)",
|
|
129
|
+
"Results may vary; prefer tokens like 'sm', 'md', 'lg'"),
|
|
130
|
+
|
|
131
|
+
'animation': (True, False,
|
|
132
|
+
"CSS animations are not supported in terminal",
|
|
133
|
+
None),
|
|
134
|
+
|
|
135
|
+
'gradient': (True, False,
|
|
136
|
+
"CSS gradients are not supported in terminal",
|
|
137
|
+
"Use solid colors instead"),
|
|
138
|
+
|
|
139
|
+
'shadow': (True, False,
|
|
140
|
+
"Box shadows are not supported in terminal",
|
|
141
|
+
None),
|
|
142
|
+
|
|
143
|
+
'border_radius': (True, False,
|
|
144
|
+
"Border radius is not supported in terminal",
|
|
145
|
+
"Borders will appear rectangular"),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class UIValidator:
|
|
150
|
+
"""Validates UI AST for cross-target compatibility."""
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
self._report: ValidationReport = None
|
|
154
|
+
self._target: str = 'all' # 'html', 'textual', or 'all'
|
|
155
|
+
|
|
156
|
+
def validate(self, windows: List[QuantumNode],
|
|
157
|
+
ui_children: List[QuantumNode],
|
|
158
|
+
target: str = 'all') -> ValidationReport:
|
|
159
|
+
"""
|
|
160
|
+
Validate UI AST for target compatibility.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
windows: List of UIWindowNode
|
|
164
|
+
ui_children: List of top-level UI nodes
|
|
165
|
+
target: 'html', 'textual', or 'all' (check both)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ValidationReport with issues and compatibility score
|
|
169
|
+
"""
|
|
170
|
+
self._report = ValidationReport()
|
|
171
|
+
self._target = target
|
|
172
|
+
|
|
173
|
+
# Walk all windows
|
|
174
|
+
for window in windows:
|
|
175
|
+
self._validate_node(window)
|
|
176
|
+
|
|
177
|
+
# Walk top-level children
|
|
178
|
+
for child in ui_children:
|
|
179
|
+
self._validate_node(child)
|
|
180
|
+
|
|
181
|
+
return self._report
|
|
182
|
+
|
|
183
|
+
def _validate_node(self, node: QuantumNode):
|
|
184
|
+
"""Recursively validate a node and its children."""
|
|
185
|
+
if node is None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Classify node
|
|
189
|
+
if self._is_container(node):
|
|
190
|
+
self._report.containers_count += 1
|
|
191
|
+
self._validate_container(node)
|
|
192
|
+
elif self._is_widget(node):
|
|
193
|
+
self._report.widgets_count += 1
|
|
194
|
+
self._validate_widget(node)
|
|
195
|
+
|
|
196
|
+
# Validate layout attributes common to all nodes
|
|
197
|
+
self._validate_layout_attrs(node)
|
|
198
|
+
|
|
199
|
+
# Recurse into children
|
|
200
|
+
if hasattr(node, 'children') and node.children:
|
|
201
|
+
for child in node.children:
|
|
202
|
+
self._validate_node(child)
|
|
203
|
+
|
|
204
|
+
def _is_container(self, node) -> bool:
|
|
205
|
+
"""Check if node is a container type."""
|
|
206
|
+
return isinstance(node, (
|
|
207
|
+
UIWindowNode, UIHBoxNode, UIVBoxNode, UIPanelNode,
|
|
208
|
+
UITabPanelNode, UITabNode, UIGridNode, UIAccordionNode,
|
|
209
|
+
UISectionNode, UIDividedBoxNode, UIFormNode, UIFormItemNode,
|
|
210
|
+
UIScrollBoxNode, UIListNode, UIItemNode, UIMenuNode,
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
def _is_widget(self, node) -> bool:
|
|
214
|
+
"""Check if node is a widget type."""
|
|
215
|
+
return isinstance(node, (
|
|
216
|
+
UITextNode, UIButtonNode, UIInputNode, UICheckboxNode,
|
|
217
|
+
UIRadioNode, UISwitchNode, UISelectNode, UITableNode,
|
|
218
|
+
UIProgressNode, UITreeNode, UILogNode, UIMarkdownNode,
|
|
219
|
+
UIHeaderNode, UIFooterNode, UIRuleNode, UILoadingNode,
|
|
220
|
+
UIImageNode, UILinkNode, UIBadgeNode, UISpacerNode,
|
|
221
|
+
UIColumnNode, UIOptionNode,
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
def _validate_container(self, node):
|
|
225
|
+
"""Validate container-specific features."""
|
|
226
|
+
pass # Containers generally work well across targets
|
|
227
|
+
|
|
228
|
+
def _validate_widget(self, node):
|
|
229
|
+
"""Validate widget-specific features."""
|
|
230
|
+
# Image - limited in terminal
|
|
231
|
+
if isinstance(node, UIImageNode):
|
|
232
|
+
self._add_feature_issue('image', type(node).__name__)
|
|
233
|
+
|
|
234
|
+
# External link - not clickable in terminal
|
|
235
|
+
if isinstance(node, UILinkNode):
|
|
236
|
+
if node.external:
|
|
237
|
+
self._add_feature_issue('link_external', type(node).__name__)
|
|
238
|
+
|
|
239
|
+
# Markdown - supported in both but rendering differs
|
|
240
|
+
if isinstance(node, UIMarkdownNode):
|
|
241
|
+
self._report.features_used.add('markdown')
|
|
242
|
+
|
|
243
|
+
def _validate_layout_attrs(self, node):
|
|
244
|
+
"""Validate layout attributes for compatibility."""
|
|
245
|
+
node_type = type(node).__name__
|
|
246
|
+
|
|
247
|
+
# Gap
|
|
248
|
+
if hasattr(node, 'gap') and node.gap:
|
|
249
|
+
self._add_feature_issue('gap', node_type)
|
|
250
|
+
|
|
251
|
+
# Justify
|
|
252
|
+
if hasattr(node, 'justify') and node.justify:
|
|
253
|
+
if node.justify == 'between':
|
|
254
|
+
self._add_feature_issue('justify_between', node_type)
|
|
255
|
+
elif node.justify == 'around':
|
|
256
|
+
self._add_feature_issue('justify_around', node_type)
|
|
257
|
+
|
|
258
|
+
# Pixel values
|
|
259
|
+
if hasattr(node, 'width') and node.width:
|
|
260
|
+
if self._is_pixel_value(node.width):
|
|
261
|
+
self._add_feature_issue('pixel_units', node_type)
|
|
262
|
+
|
|
263
|
+
if hasattr(node, 'height') and node.height:
|
|
264
|
+
if self._is_pixel_value(node.height):
|
|
265
|
+
self._add_feature_issue('pixel_units', node_type)
|
|
266
|
+
|
|
267
|
+
if hasattr(node, 'padding') and node.padding:
|
|
268
|
+
if self._is_pixel_value(node.padding):
|
|
269
|
+
self._add_feature_issue('pixel_units', node_type)
|
|
270
|
+
|
|
271
|
+
if hasattr(node, 'margin') and node.margin:
|
|
272
|
+
if self._is_pixel_value(node.margin):
|
|
273
|
+
self._add_feature_issue('pixel_units', node_type)
|
|
274
|
+
|
|
275
|
+
# Font size
|
|
276
|
+
if hasattr(node, 'size') and node.size:
|
|
277
|
+
# For text nodes
|
|
278
|
+
self._add_feature_issue('font_size', node_type)
|
|
279
|
+
|
|
280
|
+
def _is_pixel_value(self, value: str) -> bool:
|
|
281
|
+
"""Check if value is a raw pixel value (not a token)."""
|
|
282
|
+
if not value:
|
|
283
|
+
return False
|
|
284
|
+
value = str(value).strip().lower()
|
|
285
|
+
# Check if it's a number or ends with 'px'
|
|
286
|
+
if value.endswith('px'):
|
|
287
|
+
return True
|
|
288
|
+
try:
|
|
289
|
+
int(value)
|
|
290
|
+
return True
|
|
291
|
+
except ValueError:
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def _add_feature_issue(self, feature: str, node_type: str):
|
|
295
|
+
"""Add a feature compatibility issue to the report."""
|
|
296
|
+
if feature in self._report.features_used:
|
|
297
|
+
return # Already reported
|
|
298
|
+
|
|
299
|
+
self._report.features_used.add(feature)
|
|
300
|
+
|
|
301
|
+
if feature not in FEATURE_COMPAT:
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
html_support, textual_support, message, suggestion = FEATURE_COMPAT[feature]
|
|
305
|
+
|
|
306
|
+
# Determine severity for Textual target
|
|
307
|
+
if textual_support is False:
|
|
308
|
+
severity = Severity.WARNING
|
|
309
|
+
elif textual_support == 'degraded':
|
|
310
|
+
severity = Severity.INFO
|
|
311
|
+
elif textual_support == 'converted':
|
|
312
|
+
severity = Severity.INFO
|
|
313
|
+
else:
|
|
314
|
+
return # Fully supported
|
|
315
|
+
|
|
316
|
+
# Only add if checking this target
|
|
317
|
+
if self._target in ('all', 'textual'):
|
|
318
|
+
self._report.issues.append(CompatibilityIssue(
|
|
319
|
+
feature=feature,
|
|
320
|
+
message=message,
|
|
321
|
+
severity=severity,
|
|
322
|
+
target='textual',
|
|
323
|
+
node_type=node_type,
|
|
324
|
+
suggestion=suggestion,
|
|
325
|
+
))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def validate_cross_target(windows: List[QuantumNode],
|
|
329
|
+
ui_children: List[QuantumNode]) -> ValidationReport:
|
|
330
|
+
"""
|
|
331
|
+
Convenience function to validate UI for cross-target compatibility.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
windows: List of UIWindowNode
|
|
335
|
+
ui_children: List of top-level UI nodes
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
ValidationReport with compatibility score and issues
|
|
339
|
+
"""
|
|
340
|
+
validator = UIValidator()
|
|
341
|
+
return validator.validate(windows, ui_children)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def print_validation_report(report: ValidationReport):
|
|
345
|
+
"""Print a formatted validation report."""
|
|
346
|
+
print(f"\n{'='*60}")
|
|
347
|
+
print(f"UI Cross-Target Compatibility Report")
|
|
348
|
+
print(f"{'='*60}")
|
|
349
|
+
print(f"Containers: {report.containers_count}")
|
|
350
|
+
print(f"Widgets: {report.widgets_count}")
|
|
351
|
+
print(f"Compatibility Score: {report.compatibility_score}%")
|
|
352
|
+
print(f"HTML Compatible: {'Yes' if report.html_compatible else 'No'}")
|
|
353
|
+
print(f"Textual Compatible: {'Yes' if report.textual_compatible else 'No'}")
|
|
354
|
+
|
|
355
|
+
if report.issues:
|
|
356
|
+
print(f"\nIssues ({len(report.issues)}):")
|
|
357
|
+
for issue in report.issues:
|
|
358
|
+
icon = {'info': 'ℹ', 'warning': '⚠', 'error': '✖'}.get(issue.severity.value, '•')
|
|
359
|
+
print(f" {icon} [{issue.target}] {issue.feature}: {issue.message}")
|
|
360
|
+
if issue.suggestion:
|
|
361
|
+
print(f" └─ Suggestion: {issue.suggestion}")
|
|
362
|
+
else:
|
|
363
|
+
print("\nNo compatibility issues found!")
|
|
364
|
+
|
|
365
|
+
print(f"{'='*60}\n")
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""The view of a page as data, for renderers that are not a browser (UI-3).
|
|
2
|
+
|
|
3
|
+
The web server executes a page as always, and — when asked with
|
|
4
|
+
`Accept: application/vnd.quantum.view+json` — answers with this tree instead
|
|
5
|
+
of HTML. The console renderer (quantum/runtime/ui_console.py) draws it and
|
|
6
|
+
sends the same q:action posts a browser would. One runtime, two renderers:
|
|
7
|
+
nothing about the page's logic is translated.
|
|
8
|
+
|
|
9
|
+
A view node is a dict:
|
|
10
|
+
|
|
11
|
+
{"type": "hbox", "props": {...}, "children": [...]}
|
|
12
|
+
{"type": "button", "props": {"text": "Salvar"},
|
|
13
|
+
"event": {"action": "salvar", "fields": {"id": "3"}}}
|
|
14
|
+
{"type": "text", "props": {"text": "..."}} # also plain text/HTML
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
from typing import Any, Dict, List
|
|
19
|
+
|
|
20
|
+
from quantum.core.ast_nodes import CommentNode, HTMLNode, TextNode
|
|
21
|
+
from quantum.core.features.conditionals.src.ast_node import IfNode
|
|
22
|
+
from quantum.core.features.loops.src.ast_node import LoopNode
|
|
23
|
+
from quantum.core.features.ui_engine.src.ast_nodes import (
|
|
24
|
+
UIColumnNode, UIFormNode, UIListNode, UIPagerNode, UITableNode, UIWindowNode, is_ui_node)
|
|
25
|
+
from quantum.runtime.ui_pager import pager_items
|
|
26
|
+
from quantum.runtime.ui_table_edit import cell_field, edit_plan, sort_link
|
|
27
|
+
from quantum.runtime.ui_form_rules import FIELDS, form_context, generated_fields, prepare_field
|
|
28
|
+
from quantum.runtime.ui_page_renderer import cell_value, source_rows
|
|
29
|
+
|
|
30
|
+
# Fields that are structure or events, not props.
|
|
31
|
+
_NAO_PROPS = {'children', 'on_click', 'on_submit', 'on_change', 'on_select', 'send', 'source', 'as_var', 'values',
|
|
32
|
+
'edit', 'edit_datasource', 'sort',
|
|
33
|
+
# DEV-2: where the element is in the .q — never sent to a client
|
|
34
|
+
'source_line', 'source_file'}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tipo(node) -> str:
|
|
38
|
+
nome = type(node).__name__
|
|
39
|
+
return nome[2:-4].lower() if nome.startswith('UI') and nome.endswith('Node') else nome.lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def page_title(ast, renderer) -> str:
|
|
43
|
+
"""UI-4: a page's title, the same for every renderer — the first ui:window's
|
|
44
|
+
`title` (bindings resolved), else the component's name."""
|
|
45
|
+
for node in getattr(ast, 'statements', []):
|
|
46
|
+
if isinstance(node, UIWindowNode) and node.title:
|
|
47
|
+
titulo = renderer._apply_databinding(node.title)
|
|
48
|
+
if titulo not in (None, ''):
|
|
49
|
+
return str(titulo)
|
|
50
|
+
return getattr(ast, 'name', '') or ''
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UIViewBuilder:
|
|
54
|
+
"""Walks a page's nodes with the page renderer's evaluation (loops, ifs, bindings)."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, renderer):
|
|
57
|
+
self.r = renderer
|
|
58
|
+
|
|
59
|
+
def build(self, nodes) -> List[Dict[str, Any]]:
|
|
60
|
+
vista: List[Dict[str, Any]] = []
|
|
61
|
+
for node in nodes:
|
|
62
|
+
vista.extend(self._node(node))
|
|
63
|
+
return vista
|
|
64
|
+
|
|
65
|
+
def _texto(self, valor: str) -> str:
|
|
66
|
+
resolvido = self.r._apply_databinding(valor)
|
|
67
|
+
return '' if resolvido is None else str(resolvido)
|
|
68
|
+
|
|
69
|
+
def _campos(self, envio) -> Dict[str, str]:
|
|
70
|
+
campos = {}
|
|
71
|
+
if envio:
|
|
72
|
+
for par in self._texto(envio).split(','):
|
|
73
|
+
if '=' in par:
|
|
74
|
+
campo, valor = (p.strip() for p in par.split('=', 1))
|
|
75
|
+
campos[campo] = valor
|
|
76
|
+
return campos
|
|
77
|
+
|
|
78
|
+
def _node(self, node) -> List[Dict[str, Any]]:
|
|
79
|
+
if is_ui_node(node):
|
|
80
|
+
return [self._ui(node)]
|
|
81
|
+
if isinstance(node, LoopNode):
|
|
82
|
+
return self._loop(node)
|
|
83
|
+
if isinstance(node, IfNode):
|
|
84
|
+
return self.build(self._ramo(node))
|
|
85
|
+
if isinstance(node, TextNode):
|
|
86
|
+
texto = self._texto(node.content).strip()
|
|
87
|
+
return [{'type': 'text', 'props': {'text': texto}}] if texto else []
|
|
88
|
+
if isinstance(node, HTMLNode):
|
|
89
|
+
# HTML inside a UI page: its text, in order (a terminal draws no tags).
|
|
90
|
+
filhos = self.build(node.children or [])
|
|
91
|
+
return filhos
|
|
92
|
+
if isinstance(node, CommentNode):
|
|
93
|
+
return []
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
def _ui(self, node) -> Dict[str, Any]:
|
|
97
|
+
if isinstance(node, UIPagerNode):
|
|
98
|
+
# UI-11: the same items the browser gets, as links the console follows.
|
|
99
|
+
filhos = []
|
|
100
|
+
for item in pager_items(self.r, node):
|
|
101
|
+
if item['href']:
|
|
102
|
+
filhos.append({'type': 'link', 'props': {'to': item['href'], 'text': item['label']},
|
|
103
|
+
'children': []})
|
|
104
|
+
else:
|
|
105
|
+
texto = f"[{item['label']}]" if item['current'] else item['label']
|
|
106
|
+
filhos.append({'type': 'text', 'props': {'text': texto}, 'children': []})
|
|
107
|
+
return {'type': 'hbox', 'props': {'ui_id': getattr(node, 'ui_id', None)}, 'children': filhos}
|
|
108
|
+
erro = None
|
|
109
|
+
if getattr(node, 'search', None) and getattr(node, 'value', None) in (None, ''):
|
|
110
|
+
# UI-12: the search field shows the search in the URL, as in the browser.
|
|
111
|
+
try:
|
|
112
|
+
atuais = self.r.context.get_variable('query')
|
|
113
|
+
except Exception:
|
|
114
|
+
atuais = None
|
|
115
|
+
if isinstance(atuais, dict) and atuais.get(node.bind) not in (None, ''):
|
|
116
|
+
node = copy.copy(node)
|
|
117
|
+
node.value = str(atuais[node.bind])
|
|
118
|
+
if isinstance(node, FIELDS):
|
|
119
|
+
# UI-9: the same rules, values and errors the browser's form gets.
|
|
120
|
+
node, erro = prepare_field(node, getattr(self, '_form_ctx', None), escape=False)
|
|
121
|
+
if isinstance(node, UIFormNode):
|
|
122
|
+
anterior = getattr(self, '_form_ctx', None)
|
|
123
|
+
acao = (node.on_submit or '').strip().rstrip('()') or None
|
|
124
|
+
self._form_ctx = form_context(self.r, node, acao)
|
|
125
|
+
try:
|
|
126
|
+
com_campos = copy.copy(node)
|
|
127
|
+
com_campos.children = generated_fields(node, self._form_ctx) # UI-10
|
|
128
|
+
return self._ui_sem_contexto(com_campos, None)
|
|
129
|
+
finally:
|
|
130
|
+
self._form_ctx = anterior
|
|
131
|
+
return self._ui_sem_contexto(node, erro)
|
|
132
|
+
|
|
133
|
+
def _ui_sem_contexto(self, node, erro) -> Dict[str, Any]:
|
|
134
|
+
props = {}
|
|
135
|
+
for nome, valor in vars(node).items():
|
|
136
|
+
if nome in _NAO_PROPS or nome.startswith('_') or valor is None:
|
|
137
|
+
continue
|
|
138
|
+
if isinstance(valor, str):
|
|
139
|
+
props[nome] = self._texto(valor) if '{' in valor else valor
|
|
140
|
+
elif isinstance(valor, (bool, int, float)):
|
|
141
|
+
props[nome] = valor
|
|
142
|
+
if 'content' in props:
|
|
143
|
+
props['text'] = props.pop('content')
|
|
144
|
+
if isinstance(node, UITableNode) and node.source:
|
|
145
|
+
return self._tabela(node, props)
|
|
146
|
+
if isinstance(node, UIListNode) and node.source:
|
|
147
|
+
filhos = []
|
|
148
|
+
for linha in source_rows(self.r, node, 'ui:list', 'item'):
|
|
149
|
+
filhos.extend(self.build(node.children) if node.children
|
|
150
|
+
else [{'type': 'item', 'props': {}, 'children': [
|
|
151
|
+
{'type': 'text', 'props': {'text': '' if linha is None else str(linha)}}]}])
|
|
152
|
+
return {'type': 'list', 'props': props, 'children': filhos}
|
|
153
|
+
if erro:
|
|
154
|
+
props['error'] = erro
|
|
155
|
+
vista = {'type': _tipo(node), 'props': props,
|
|
156
|
+
'children': self.build(getattr(node, 'children', None) or [])}
|
|
157
|
+
acao = getattr(node, 'on_click', None) or getattr(node, 'on_submit', None)
|
|
158
|
+
if acao:
|
|
159
|
+
vista['event'] = {'action': acao.strip().rstrip('()'), 'fields': self._campos(getattr(node, 'send', None))}
|
|
160
|
+
return vista
|
|
161
|
+
|
|
162
|
+
def _tabela(self, node: UITableNode, props: Dict[str, Any]) -> Dict[str, Any]:
|
|
163
|
+
"""UI-5: a table with its rows; each cell is a list of view nodes.
|
|
164
|
+
UI-13: sortable headers carry the link; an editable cell is a form."""
|
|
165
|
+
colunas = [c for c in node.children if isinstance(c, UIColumnNode)]
|
|
166
|
+
plano = edit_plan(self.r, node)
|
|
167
|
+
estado = getattr(self.r, 'form_state', None)
|
|
168
|
+
linhas = []
|
|
169
|
+
for linha in source_rows(self.r, node, 'ui:table', 'row'):
|
|
170
|
+
celulas = []
|
|
171
|
+
for c in colunas:
|
|
172
|
+
if c.children:
|
|
173
|
+
celulas.append(self.build(c.children))
|
|
174
|
+
elif plano and c.key in plano['columns']:
|
|
175
|
+
campo, ocultos, erro = cell_field(plano, plano['columns'][c.key], linha, estado, escape=False)
|
|
176
|
+
vista_campo = self._ui(campo)
|
|
177
|
+
if erro:
|
|
178
|
+
vista_campo['props']['error'] = erro
|
|
179
|
+
celulas.append([{'type': 'form', 'props': {},
|
|
180
|
+
'event': {'action': ocultos.pop('action'), 'fields': ocultos},
|
|
181
|
+
'children': [vista_campo]}])
|
|
182
|
+
else:
|
|
183
|
+
celulas.append([{'type': 'text', 'props': {'text': cell_value(linha, c)}}])
|
|
184
|
+
linhas.append(celulas)
|
|
185
|
+
cabecalho = []
|
|
186
|
+
for c in colunas:
|
|
187
|
+
coluna = {'label': c.label or c.key or '', 'align': c.align, 'width': c.column_width}
|
|
188
|
+
if getattr(node, 'sort', False) and c.key and not c.children:
|
|
189
|
+
href, marca = sort_link(self.r, c.key)
|
|
190
|
+
coluna['to'] = href
|
|
191
|
+
coluna['label'] += f' {marca}' if marca else ''
|
|
192
|
+
cabecalho.append(coluna)
|
|
193
|
+
return {'type': 'table', 'props': props, 'columns': cabecalho, 'rows': linhas, 'children': []}
|
|
194
|
+
|
|
195
|
+
def _ramo(self, node: IfNode) -> list:
|
|
196
|
+
if self.r._evaluate_condition(node.condition):
|
|
197
|
+
return list(node.if_body)
|
|
198
|
+
for bloco in node.elseif_blocks or []:
|
|
199
|
+
cond = bloco.get('condition') if isinstance(bloco, dict) else getattr(bloco, 'condition', None)
|
|
200
|
+
corpo = bloco.get('body') if isinstance(bloco, dict) else getattr(bloco, 'body', None)
|
|
201
|
+
if cond is not None and self.r._evaluate_condition(cond):
|
|
202
|
+
return list(corpo or [])
|
|
203
|
+
return list(node.else_body or [])
|
|
204
|
+
|
|
205
|
+
def _loop(self, node: LoopNode) -> List[Dict[str, Any]]:
|
|
206
|
+
# The same iteration as HTMLRenderer._render_loop.
|
|
207
|
+
vista: List[Dict[str, Any]] = []
|
|
208
|
+
ctx = self.r.context
|
|
209
|
+
originais = ctx.local_vars.copy()
|
|
210
|
+
try:
|
|
211
|
+
for indice, item in enumerate(self.r._get_loop_items(node) or []):
|
|
212
|
+
ctx.set_variable(node.var_name, item, scope='local')
|
|
213
|
+
if node.loop_type == 'query' and isinstance(item, dict):
|
|
214
|
+
for campo, valor in item.items():
|
|
215
|
+
ctx.set_variable(f'{node.var_name}.{campo}', valor, scope='local')
|
|
216
|
+
if node.index_name:
|
|
217
|
+
ctx.set_variable(node.index_name, indice, scope='local')
|
|
218
|
+
vista.extend(self.build(node.body))
|
|
219
|
+
finally:
|
|
220
|
+
ctx.local_vars = originais
|
|
221
|
+
return vista
|