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,1192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI Engine Parser - Parse ui: namespace elements into UI AST nodes.
|
|
3
|
+
|
|
4
|
+
This module provides parse functions for all UI-specific tags.
|
|
5
|
+
It is called from the main QuantumParser when a ui: prefixed tag is encountered.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
|
|
11
|
+
from .ast_nodes import (
|
|
12
|
+
UIWindowNode, UIHBoxNode, UIVBoxNode, UIPanelNode,
|
|
13
|
+
UITabPanelNode, UITabNode, UIGridNode, UIAccordionNode,
|
|
14
|
+
UISectionNode, UIDividedBoxNode, UIFormNode, UIFormItemNode,
|
|
15
|
+
UISpacerNode, UIScrollBoxNode,
|
|
16
|
+
UITextNode, UIButtonNode, UIInputNode, UICheckboxNode,
|
|
17
|
+
UIRadioNode, UISwitchNode, UISelectNode, UITableNode,
|
|
18
|
+
UIColumnNode, UIListNode, UIItemNode, UIImageNode,
|
|
19
|
+
UILinkNode, UIProgressNode, UITreeNode, UIMenuNode,
|
|
20
|
+
UIOptionNode, UILogNode, UIMarkdownNode, UIHeaderNode,
|
|
21
|
+
UIFooterNode, UIRuleNode, UILoadingNode, UIBadgeNode,
|
|
22
|
+
UIValidatorNode, UIAnimateNode, UIAnimationMixin,
|
|
23
|
+
# Component Library nodes
|
|
24
|
+
UICardNode, UICardHeaderNode, UICardBodyNode, UICardFooterNode,
|
|
25
|
+
UIModalNode, UIChartNode, UIAvatarNode, UITooltipNode,
|
|
26
|
+
UIDropdownNode, UIAlertNode, UIBreadcrumbNode, UIBreadcrumbItemNode,
|
|
27
|
+
UIPaginationNode, UISkeletonNode,
|
|
28
|
+
UIToastNode, UIToastContainerNode,
|
|
29
|
+
UICarouselNode, UISlideNode,
|
|
30
|
+
UIStepperNode, UIStepNode,
|
|
31
|
+
UICalendarNode, UIDatePickerNode, UIPagerNode,
|
|
32
|
+
)
|
|
33
|
+
from quantum.core.features.theming.src import UIThemeNode, UIColorNode
|
|
34
|
+
from quantum.core.ast_nodes import TextNode
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UIParseError(Exception):
|
|
38
|
+
"""UI-specific parse error."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UIParser:
|
|
43
|
+
"""Parser for ui: namespace UI elements."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, parent_parser, page_mode: bool = False):
|
|
46
|
+
"""
|
|
47
|
+
Args:
|
|
48
|
+
parent_parser: The main QuantumParser instance, used to parse
|
|
49
|
+
q: namespace children (q:set, q:function, q:if, etc.)
|
|
50
|
+
"""
|
|
51
|
+
self.parent = parent_parser
|
|
52
|
+
# UI-1: inside a served page, HTML children are content too. In a
|
|
53
|
+
# <q:application type="ui"> build they were (and stay) ignored.
|
|
54
|
+
self.page_mode = page_mode
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Dispatch
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
UI_TAG_MAP = {
|
|
61
|
+
'window': '_parse_ui_window',
|
|
62
|
+
'hbox': '_parse_ui_hbox',
|
|
63
|
+
'vbox': '_parse_ui_vbox',
|
|
64
|
+
'panel': '_parse_ui_panel',
|
|
65
|
+
'tabpanel': '_parse_ui_tabpanel',
|
|
66
|
+
'tab': '_parse_ui_tab',
|
|
67
|
+
'grid': '_parse_ui_grid',
|
|
68
|
+
'accordion': '_parse_ui_accordion',
|
|
69
|
+
'section': '_parse_ui_section',
|
|
70
|
+
'dividedbox': '_parse_ui_dividedbox',
|
|
71
|
+
'form': '_parse_ui_form',
|
|
72
|
+
'formitem': '_parse_ui_formitem',
|
|
73
|
+
'spacer': '_parse_ui_spacer',
|
|
74
|
+
'scrollbox': '_parse_ui_scrollbox',
|
|
75
|
+
'text': '_parse_ui_text',
|
|
76
|
+
'button': '_parse_ui_button',
|
|
77
|
+
'input': '_parse_ui_input',
|
|
78
|
+
'checkbox': '_parse_ui_checkbox',
|
|
79
|
+
'radio': '_parse_ui_radio',
|
|
80
|
+
'switch': '_parse_ui_switch',
|
|
81
|
+
'select': '_parse_ui_select',
|
|
82
|
+
'table': '_parse_ui_table',
|
|
83
|
+
'column': '_parse_ui_column',
|
|
84
|
+
'pager': '_parse_ui_pager',
|
|
85
|
+
'list': '_parse_ui_list',
|
|
86
|
+
'item': '_parse_ui_item',
|
|
87
|
+
'image': '_parse_ui_image',
|
|
88
|
+
'link': '_parse_ui_link',
|
|
89
|
+
'progress': '_parse_ui_progress',
|
|
90
|
+
'tree': '_parse_ui_tree',
|
|
91
|
+
'menu': '_parse_ui_menu',
|
|
92
|
+
'option': '_parse_ui_option',
|
|
93
|
+
'log': '_parse_ui_log',
|
|
94
|
+
'markdown': '_parse_ui_markdown',
|
|
95
|
+
'header': '_parse_ui_header',
|
|
96
|
+
'footer': '_parse_ui_footer',
|
|
97
|
+
'rule': '_parse_ui_rule',
|
|
98
|
+
'loading': '_parse_ui_loading',
|
|
99
|
+
'badge': '_parse_ui_badge',
|
|
100
|
+
'validator': '_parse_ui_validator',
|
|
101
|
+
'animate': '_parse_ui_animate',
|
|
102
|
+
# Component Library
|
|
103
|
+
'card': '_parse_ui_card',
|
|
104
|
+
'card-header': '_parse_ui_card_header',
|
|
105
|
+
'card-body': '_parse_ui_card_body',
|
|
106
|
+
'card-footer': '_parse_ui_card_footer',
|
|
107
|
+
'modal': '_parse_ui_modal',
|
|
108
|
+
'chart': '_parse_ui_chart',
|
|
109
|
+
'avatar': '_parse_ui_avatar',
|
|
110
|
+
'tooltip': '_parse_ui_tooltip',
|
|
111
|
+
'dropdown': '_parse_ui_dropdown',
|
|
112
|
+
'alert': '_parse_ui_alert',
|
|
113
|
+
'breadcrumb': '_parse_ui_breadcrumb',
|
|
114
|
+
'breadcrumb-item': '_parse_ui_breadcrumb_item',
|
|
115
|
+
'pagination': '_parse_ui_pagination',
|
|
116
|
+
'skeleton': '_parse_ui_skeleton',
|
|
117
|
+
# Toast notifications
|
|
118
|
+
'toast': '_parse_ui_toast',
|
|
119
|
+
'toast-container': '_parse_ui_toast_container',
|
|
120
|
+
# Carousel / slider
|
|
121
|
+
'carousel': '_parse_ui_carousel',
|
|
122
|
+
'slide': '_parse_ui_slide',
|
|
123
|
+
# Stepper / wizard
|
|
124
|
+
'stepper': '_parse_ui_stepper',
|
|
125
|
+
'step': '_parse_ui_step',
|
|
126
|
+
# Calendar / date picker
|
|
127
|
+
'calendar': '_parse_ui_calendar',
|
|
128
|
+
'date-picker': '_parse_ui_date_picker',
|
|
129
|
+
# Theming
|
|
130
|
+
'theme': '_parse_ui_theme',
|
|
131
|
+
'color': '_parse_ui_color',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def parse_ui_element(self, local_name: str, element: ET.Element):
|
|
135
|
+
"""Dispatch a ui: element to the correct parse method."""
|
|
136
|
+
method_name = self.UI_TAG_MAP.get(local_name)
|
|
137
|
+
if method_name is None:
|
|
138
|
+
import difflib
|
|
139
|
+
parecidas = difflib.get_close_matches(local_name, self.UI_TAG_MAP, n=1)
|
|
140
|
+
dica = f"; did you mean <ui:{parecidas[0]}>?" if parecidas else ""
|
|
141
|
+
erro = UIParseError(f"Unknown UI tag: <ui:{local_name}>{dica}")
|
|
142
|
+
erro.line = getattr(element, 'sourceline', None)
|
|
143
|
+
raise erro
|
|
144
|
+
method = getattr(self, method_name)
|
|
145
|
+
try:
|
|
146
|
+
node = method(element)
|
|
147
|
+
except UIParseError as exc:
|
|
148
|
+
if getattr(exc, 'line', None) is None:
|
|
149
|
+
exc.line = getattr(element, 'sourceline', None)
|
|
150
|
+
raise
|
|
151
|
+
# DEV-2: a ui:* element knows its line and file, like a q: statement.
|
|
152
|
+
if getattr(element, 'sourceline', None) and node is not None:
|
|
153
|
+
try:
|
|
154
|
+
node.source_line = element.sourceline
|
|
155
|
+
node.source_file = getattr(self.parent, '_source_path', None)
|
|
156
|
+
except AttributeError:
|
|
157
|
+
pass
|
|
158
|
+
return node
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Helpers
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _get_local_name(self, element: ET.Element) -> str:
|
|
165
|
+
tag = element.tag
|
|
166
|
+
if '}' in tag:
|
|
167
|
+
return tag.split('}')[-1]
|
|
168
|
+
if ':' in tag:
|
|
169
|
+
return tag.split(':')[-1]
|
|
170
|
+
return tag
|
|
171
|
+
|
|
172
|
+
def _get_namespace(self, element: ET.Element) -> Optional[str]:
|
|
173
|
+
tag = element.tag
|
|
174
|
+
if '{https://quantum.lang/ui}' in tag:
|
|
175
|
+
return 'ui'
|
|
176
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
177
|
+
return 'quantum'
|
|
178
|
+
if tag.startswith('ui:'):
|
|
179
|
+
return 'ui'
|
|
180
|
+
if tag.startswith('q:'):
|
|
181
|
+
return 'quantum'
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def _parse_bool(self, value: Optional[str], default: bool = False) -> bool:
|
|
185
|
+
if value is None:
|
|
186
|
+
return default
|
|
187
|
+
return value.lower() in ('true', '1', 'yes')
|
|
188
|
+
|
|
189
|
+
def _parse_int(self, value: Optional[str], default: int = 0) -> int:
|
|
190
|
+
if value is None:
|
|
191
|
+
return default
|
|
192
|
+
try:
|
|
193
|
+
return int(value)
|
|
194
|
+
except ValueError:
|
|
195
|
+
return default
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Layout attributes helper
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def _apply_layout_attrs(self, node, element: ET.Element):
|
|
202
|
+
"""Apply common layout attributes from XML element to node."""
|
|
203
|
+
node.gap = element.get('gap')
|
|
204
|
+
node.padding = element.get('padding')
|
|
205
|
+
node.margin = element.get('margin')
|
|
206
|
+
node.align = element.get('align')
|
|
207
|
+
node.justify = element.get('justify')
|
|
208
|
+
node.width = element.get('width')
|
|
209
|
+
node.height = element.get('height')
|
|
210
|
+
node.background = element.get('background')
|
|
211
|
+
node.color = element.get('color')
|
|
212
|
+
node.border = element.get('border')
|
|
213
|
+
node.ui_id = element.get('id')
|
|
214
|
+
node.ui_class = element.get('class')
|
|
215
|
+
node.visible = element.get('visible')
|
|
216
|
+
# UI-2: responsive attributes; a breakpoint is sm, md or lg.
|
|
217
|
+
for attr, campo in (('stack-below', 'stack_below'), ('hide-below', 'hide_below'),
|
|
218
|
+
('hide-above', 'hide_above')):
|
|
219
|
+
valor = element.get(attr)
|
|
220
|
+
if valor is not None and valor not in ('sm', 'md', 'lg'):
|
|
221
|
+
raise UIParseError(f'{attr}="{valor}": use sm (640 px), md (768 px) or lg (1024 px)')
|
|
222
|
+
setattr(node, campo, valor)
|
|
223
|
+
node.grow = element.get('grow')
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Animation attributes helper
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def _apply_animation_attrs(self, node, element: ET.Element):
|
|
230
|
+
"""Apply animation attributes from XML element to node.
|
|
231
|
+
|
|
232
|
+
Supports:
|
|
233
|
+
- animate: Animation preset name (e.g., 'slide-in-left', 'fade-in')
|
|
234
|
+
- transition: Transition shorthand (e.g., 'scale:0.95:100ms')
|
|
235
|
+
- type: Animation type for ui:animate
|
|
236
|
+
- duration: Animation duration in ms or CSS format
|
|
237
|
+
- delay: Animation delay in ms or CSS format
|
|
238
|
+
- easing: Easing function (ease, ease-in, ease-out, linear, etc.)
|
|
239
|
+
- repeat: Repeat count ('1', '2', 'infinite')
|
|
240
|
+
- trigger: Animation trigger (on-load, on-hover, on-click, on-visible)
|
|
241
|
+
- direction: Animation direction (normal, reverse, alternate)
|
|
242
|
+
"""
|
|
243
|
+
if not hasattr(node, '_init_animation_attrs'):
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
node.animate = element.get('animate')
|
|
247
|
+
node.transition = element.get('transition')
|
|
248
|
+
node.anim_type = element.get('type')
|
|
249
|
+
node.anim_duration = element.get('duration')
|
|
250
|
+
node.anim_delay = element.get('delay')
|
|
251
|
+
node.anim_easing = element.get('easing')
|
|
252
|
+
node.anim_repeat = element.get('repeat')
|
|
253
|
+
node.anim_trigger = element.get('trigger')
|
|
254
|
+
node.anim_direction = element.get('direction')
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
# Children dispatch - both ui: and q: tags
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def _parse_child(self, child: ET.Element):
|
|
261
|
+
"""Parse a child element that can be either ui: or q: namespace."""
|
|
262
|
+
ns = self._get_namespace(child)
|
|
263
|
+
local = self._get_local_name(child)
|
|
264
|
+
|
|
265
|
+
if ns == 'ui':
|
|
266
|
+
return self.parse_ui_element(local, child)
|
|
267
|
+
elif ns == 'quantum' or self.page_mode:
|
|
268
|
+
return self.parent._parse_statement(child)
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
def _parse_children(self, element: ET.Element, node):
|
|
272
|
+
"""Parse all children of an element, adding them to node.
|
|
273
|
+
|
|
274
|
+
UI-1: inside a page, a container's own text is content too
|
|
275
|
+
(`<ui:card-header>Resumo</ui:card-header>`); it used to vanish. Nodes
|
|
276
|
+
that keep their text in `content` read it themselves."""
|
|
277
|
+
texto_e_conteudo = self.page_mode and not hasattr(node, 'content')
|
|
278
|
+
if texto_e_conteudo and (element.text or '').strip():
|
|
279
|
+
node.add_child(TextNode(element.text.strip()))
|
|
280
|
+
for child in element:
|
|
281
|
+
parsed = self._parse_child(child)
|
|
282
|
+
if parsed:
|
|
283
|
+
node.add_child(parsed)
|
|
284
|
+
if texto_e_conteudo and (child.tail or '').strip():
|
|
285
|
+
node.add_child(TextNode(child.tail.strip()))
|
|
286
|
+
|
|
287
|
+
# ------------------------------------------------------------------
|
|
288
|
+
# Containers
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
def _parse_ui_window(self, element: ET.Element) -> UIWindowNode:
|
|
292
|
+
title = element.get('title')
|
|
293
|
+
node = UIWindowNode(title)
|
|
294
|
+
self._apply_layout_attrs(node, element)
|
|
295
|
+
self._parse_children(element, node)
|
|
296
|
+
return node
|
|
297
|
+
|
|
298
|
+
def _parse_ui_hbox(self, element: ET.Element) -> UIHBoxNode:
|
|
299
|
+
node = UIHBoxNode()
|
|
300
|
+
self._apply_layout_attrs(node, element)
|
|
301
|
+
self._parse_children(element, node)
|
|
302
|
+
return node
|
|
303
|
+
|
|
304
|
+
def _parse_ui_vbox(self, element: ET.Element) -> UIVBoxNode:
|
|
305
|
+
node = UIVBoxNode()
|
|
306
|
+
self._apply_layout_attrs(node, element)
|
|
307
|
+
self._parse_children(element, node)
|
|
308
|
+
return node
|
|
309
|
+
|
|
310
|
+
def _parse_ui_panel(self, element: ET.Element) -> UIPanelNode:
|
|
311
|
+
node = UIPanelNode()
|
|
312
|
+
node.title = element.get('title')
|
|
313
|
+
node.collapsible = self._parse_bool(element.get('collapsible'))
|
|
314
|
+
self._apply_layout_attrs(node, element)
|
|
315
|
+
self._parse_children(element, node)
|
|
316
|
+
return node
|
|
317
|
+
|
|
318
|
+
def _parse_ui_tabpanel(self, element: ET.Element) -> UITabPanelNode:
|
|
319
|
+
node = UITabPanelNode()
|
|
320
|
+
self._apply_layout_attrs(node, element)
|
|
321
|
+
self._parse_children(element, node)
|
|
322
|
+
return node
|
|
323
|
+
|
|
324
|
+
def _parse_ui_tab(self, element: ET.Element) -> UITabNode:
|
|
325
|
+
node = UITabNode()
|
|
326
|
+
node.title = element.get('title', '')
|
|
327
|
+
self._apply_layout_attrs(node, element)
|
|
328
|
+
self._parse_children(element, node)
|
|
329
|
+
return node
|
|
330
|
+
|
|
331
|
+
def _parse_ui_grid(self, element: ET.Element) -> UIGridNode:
|
|
332
|
+
node = UIGridNode()
|
|
333
|
+
node.columns = element.get('columns')
|
|
334
|
+
self._apply_layout_attrs(node, element)
|
|
335
|
+
self._parse_children(element, node)
|
|
336
|
+
return node
|
|
337
|
+
|
|
338
|
+
def _parse_ui_accordion(self, element: ET.Element) -> UIAccordionNode:
|
|
339
|
+
node = UIAccordionNode()
|
|
340
|
+
self._apply_layout_attrs(node, element)
|
|
341
|
+
self._parse_children(element, node)
|
|
342
|
+
return node
|
|
343
|
+
|
|
344
|
+
def _parse_ui_section(self, element: ET.Element) -> UISectionNode:
|
|
345
|
+
node = UISectionNode()
|
|
346
|
+
node.title = element.get('title', '')
|
|
347
|
+
node.expanded = self._parse_bool(element.get('expanded'))
|
|
348
|
+
self._apply_layout_attrs(node, element)
|
|
349
|
+
self._parse_children(element, node)
|
|
350
|
+
return node
|
|
351
|
+
|
|
352
|
+
def _parse_ui_dividedbox(self, element: ET.Element) -> UIDividedBoxNode:
|
|
353
|
+
node = UIDividedBoxNode()
|
|
354
|
+
node.direction = element.get('direction', 'horizontal')
|
|
355
|
+
self._apply_layout_attrs(node, element)
|
|
356
|
+
self._parse_children(element, node)
|
|
357
|
+
return node
|
|
358
|
+
|
|
359
|
+
def _parse_ui_form(self, element: ET.Element) -> UIFormNode:
|
|
360
|
+
node = UIFormNode()
|
|
361
|
+
node.on_submit = element.get('on-submit')
|
|
362
|
+
node.send = element.get('with')
|
|
363
|
+
# UI-9: rules="off" stops the form from taking its fields' rules from the action.
|
|
364
|
+
node.rules = element.get('rules')
|
|
365
|
+
# UI-10: an edit form opens with a row's values; the generated button's text.
|
|
366
|
+
node.values = element.get('values')
|
|
367
|
+
node.submit = element.get('submit')
|
|
368
|
+
if node.rules not in (None, 'on', 'off'):
|
|
369
|
+
raise UIParseError(f'<ui:form rules="{node.rules}">: use on (the default) or off')
|
|
370
|
+
|
|
371
|
+
# Validation attributes
|
|
372
|
+
node.validation_mode = element.get('validation', 'both')
|
|
373
|
+
node.error_display = element.get('error-display', 'inline')
|
|
374
|
+
node.novalidate = self._parse_bool(element.get('novalidate'))
|
|
375
|
+
|
|
376
|
+
self._apply_layout_attrs(node, element)
|
|
377
|
+
|
|
378
|
+
# Parse children, separating validators from regular children
|
|
379
|
+
for child in element:
|
|
380
|
+
ns = self._get_namespace(child)
|
|
381
|
+
local = self._get_local_name(child)
|
|
382
|
+
|
|
383
|
+
if ns == 'ui' and local == 'validator':
|
|
384
|
+
# Parse validator and add to form's validator list
|
|
385
|
+
validator = self._parse_ui_validator(child)
|
|
386
|
+
node.add_validator(validator)
|
|
387
|
+
else:
|
|
388
|
+
# Regular child element
|
|
389
|
+
parsed = self._parse_child(child)
|
|
390
|
+
if parsed:
|
|
391
|
+
node.add_child(parsed)
|
|
392
|
+
|
|
393
|
+
return node
|
|
394
|
+
|
|
395
|
+
def _parse_ui_formitem(self, element: ET.Element) -> UIFormItemNode:
|
|
396
|
+
node = UIFormItemNode()
|
|
397
|
+
node.label = element.get('label')
|
|
398
|
+
self._apply_layout_attrs(node, element)
|
|
399
|
+
self._parse_children(element, node)
|
|
400
|
+
return node
|
|
401
|
+
|
|
402
|
+
def _parse_ui_spacer(self, element: ET.Element) -> UISpacerNode:
|
|
403
|
+
node = UISpacerNode()
|
|
404
|
+
node.size = element.get('size')
|
|
405
|
+
self._apply_layout_attrs(node, element)
|
|
406
|
+
return node
|
|
407
|
+
|
|
408
|
+
def _parse_ui_scrollbox(self, element: ET.Element) -> UIScrollBoxNode:
|
|
409
|
+
node = UIScrollBoxNode()
|
|
410
|
+
self._apply_layout_attrs(node, element)
|
|
411
|
+
self._parse_children(element, node)
|
|
412
|
+
return node
|
|
413
|
+
|
|
414
|
+
# ------------------------------------------------------------------
|
|
415
|
+
# Widgets
|
|
416
|
+
# ------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def _parse_ui_text(self, element: ET.Element) -> UITextNode:
|
|
419
|
+
node = UITextNode()
|
|
420
|
+
node.content = (element.text or '').strip()
|
|
421
|
+
node.size = element.get('size')
|
|
422
|
+
node.weight = element.get('weight')
|
|
423
|
+
# color from layout attrs
|
|
424
|
+
self._apply_layout_attrs(node, element)
|
|
425
|
+
return node
|
|
426
|
+
|
|
427
|
+
def _parse_ui_button(self, element: ET.Element) -> UIButtonNode:
|
|
428
|
+
node = UIButtonNode()
|
|
429
|
+
node.content = (element.text or '').strip()
|
|
430
|
+
node.on_click = element.get('on-click')
|
|
431
|
+
node.send = element.get('with')
|
|
432
|
+
node.variant = element.get('variant')
|
|
433
|
+
node.disabled = self._parse_bool(element.get('disabled'))
|
|
434
|
+
self._apply_layout_attrs(node, element)
|
|
435
|
+
return node
|
|
436
|
+
|
|
437
|
+
def _parse_ui_input(self, element: ET.Element) -> UIInputNode:
|
|
438
|
+
node = UIInputNode()
|
|
439
|
+
node.bind = element.get('bind')
|
|
440
|
+
node.value = element.get('value')
|
|
441
|
+
# UI-12: search as you type — refresh the element with this id.
|
|
442
|
+
node.search = element.get('search')
|
|
443
|
+
espera = element.get('delay')
|
|
444
|
+
if espera is not None and not espera.isdigit():
|
|
445
|
+
raise UIParseError(f'<ui:input delay="{espera}">: milliseconds, e.g. delay="300"')
|
|
446
|
+
node.delay = int(espera) if espera is not None else (300 if node.search else None)
|
|
447
|
+
if node.search is not None and not node.bind:
|
|
448
|
+
raise UIParseError('<ui:input search=...> needs bind= (the URL parameter it sets)')
|
|
449
|
+
node.placeholder = element.get('placeholder')
|
|
450
|
+
node.input_type = element.get('type', 'text')
|
|
451
|
+
node.on_change = element.get('on-change')
|
|
452
|
+
node.on_submit = element.get('on-submit')
|
|
453
|
+
|
|
454
|
+
# Validation attributes
|
|
455
|
+
node.required = self._parse_bool(element.get('required'))
|
|
456
|
+
node.min = element.get('min')
|
|
457
|
+
node.max = element.get('max')
|
|
458
|
+
node.pattern = element.get('pattern')
|
|
459
|
+
node.error_message = element.get('error-message')
|
|
460
|
+
|
|
461
|
+
# Integer-based validation attributes
|
|
462
|
+
minlength_str = element.get('minlength')
|
|
463
|
+
if minlength_str:
|
|
464
|
+
node.minlength = self._parse_int(minlength_str)
|
|
465
|
+
|
|
466
|
+
maxlength_str = element.get('maxlength')
|
|
467
|
+
if maxlength_str:
|
|
468
|
+
node.maxlength = self._parse_int(maxlength_str)
|
|
469
|
+
|
|
470
|
+
# Custom validators (comma-separated list of validator names)
|
|
471
|
+
validators_str = element.get('validators')
|
|
472
|
+
if validators_str:
|
|
473
|
+
node.validators = [v.strip() for v in validators_str.split(',')]
|
|
474
|
+
|
|
475
|
+
self._apply_layout_attrs(node, element)
|
|
476
|
+
return node
|
|
477
|
+
|
|
478
|
+
def _parse_ui_checkbox(self, element: ET.Element) -> UICheckboxNode:
|
|
479
|
+
node = UICheckboxNode()
|
|
480
|
+
node.bind = element.get('bind')
|
|
481
|
+
node.checked = element.get('checked')
|
|
482
|
+
node.label = element.get('label')
|
|
483
|
+
self._apply_layout_attrs(node, element)
|
|
484
|
+
return node
|
|
485
|
+
|
|
486
|
+
def _parse_ui_radio(self, element: ET.Element) -> UIRadioNode:
|
|
487
|
+
node = UIRadioNode()
|
|
488
|
+
node.bind = element.get('bind')
|
|
489
|
+
node.value = element.get('value')
|
|
490
|
+
node.options = element.get('options')
|
|
491
|
+
self._apply_layout_attrs(node, element)
|
|
492
|
+
return node
|
|
493
|
+
|
|
494
|
+
def _parse_ui_switch(self, element: ET.Element) -> UISwitchNode:
|
|
495
|
+
node = UISwitchNode()
|
|
496
|
+
node.bind = element.get('bind')
|
|
497
|
+
node.checked = element.get('checked')
|
|
498
|
+
node.label = element.get('label')
|
|
499
|
+
self._apply_layout_attrs(node, element)
|
|
500
|
+
return node
|
|
501
|
+
|
|
502
|
+
def _parse_ui_select(self, element: ET.Element) -> UISelectNode:
|
|
503
|
+
node = UISelectNode()
|
|
504
|
+
node.bind = element.get('bind')
|
|
505
|
+
node.value = element.get('value')
|
|
506
|
+
node.options = element.get('options')
|
|
507
|
+
node.source = element.get('source')
|
|
508
|
+
self._apply_layout_attrs(node, element)
|
|
509
|
+
# Parse child <ui:option> elements
|
|
510
|
+
self._parse_children(element, node)
|
|
511
|
+
return node
|
|
512
|
+
|
|
513
|
+
def _parse_ui_table(self, element: ET.Element) -> UITableNode:
|
|
514
|
+
node = UITableNode()
|
|
515
|
+
node.source = element.get('source')
|
|
516
|
+
node.as_var = element.get('as')
|
|
517
|
+
# UI-13: sortable headers and cells edited in place.
|
|
518
|
+
node.sort = self._parse_bool(element.get('sort'))
|
|
519
|
+
node.edit = element.get('edit')
|
|
520
|
+
node.edit_datasource = element.get('datasource')
|
|
521
|
+
if node.edit and not node.edit_datasource:
|
|
522
|
+
raise UIParseError(f'<ui:table edit="{node.edit}"> needs datasource= (where the table is)')
|
|
523
|
+
if (node.sort or node.edit) and not node.source:
|
|
524
|
+
raise UIParseError('<ui:table sort/edit> needs source="{query}"')
|
|
525
|
+
self._apply_layout_attrs(node, element)
|
|
526
|
+
# Parse child <ui:column> elements
|
|
527
|
+
self._parse_children(element, node)
|
|
528
|
+
return node
|
|
529
|
+
|
|
530
|
+
def _parse_ui_column(self, element: ET.Element) -> UIColumnNode:
|
|
531
|
+
node = UIColumnNode()
|
|
532
|
+
node.key = element.get('key')
|
|
533
|
+
node.label = element.get('label')
|
|
534
|
+
node.column_width = element.get('width')
|
|
535
|
+
node.align = element.get('align')
|
|
536
|
+
node.edit = element.get('edit', 'true') != 'false'
|
|
537
|
+
self._parse_children(element, node)
|
|
538
|
+
return node
|
|
539
|
+
|
|
540
|
+
def _parse_ui_pager(self, element: ET.Element) -> UIPagerNode:
|
|
541
|
+
node = UIPagerNode()
|
|
542
|
+
node.for_query = element.get('for')
|
|
543
|
+
if not node.for_query:
|
|
544
|
+
raise UIParseError('<ui:pager> needs for="<the paginated q:query>"')
|
|
545
|
+
node.param = element.get('param') or 'page'
|
|
546
|
+
janela = element.get('window')
|
|
547
|
+
if janela is not None and not janela.isdigit():
|
|
548
|
+
raise UIParseError(f'<ui:pager window="{janela}">: the number of pages shown each side of the current one')
|
|
549
|
+
node.window = int(janela) if janela is not None else 2
|
|
550
|
+
self._apply_layout_attrs(node, element)
|
|
551
|
+
return node
|
|
552
|
+
|
|
553
|
+
def _parse_ui_list(self, element: ET.Element) -> UIListNode:
|
|
554
|
+
node = UIListNode()
|
|
555
|
+
node.source = element.get('source')
|
|
556
|
+
node.as_var = element.get('as')
|
|
557
|
+
self._apply_layout_attrs(node, element)
|
|
558
|
+
self._parse_children(element, node)
|
|
559
|
+
return node
|
|
560
|
+
|
|
561
|
+
def _parse_ui_item(self, element: ET.Element) -> UIItemNode:
|
|
562
|
+
node = UIItemNode()
|
|
563
|
+
self._apply_layout_attrs(node, element)
|
|
564
|
+
self._parse_children(element, node)
|
|
565
|
+
return node
|
|
566
|
+
|
|
567
|
+
def _parse_ui_image(self, element: ET.Element) -> UIImageNode:
|
|
568
|
+
node = UIImageNode()
|
|
569
|
+
node.src = element.get('src')
|
|
570
|
+
node.alt = element.get('alt')
|
|
571
|
+
self._apply_layout_attrs(node, element)
|
|
572
|
+
return node
|
|
573
|
+
|
|
574
|
+
def _parse_ui_link(self, element: ET.Element) -> UILinkNode:
|
|
575
|
+
node = UILinkNode()
|
|
576
|
+
node.to = element.get('to')
|
|
577
|
+
node.external = self._parse_bool(element.get('external'))
|
|
578
|
+
node.content = (element.text or '').strip()
|
|
579
|
+
self._apply_layout_attrs(node, element)
|
|
580
|
+
return node
|
|
581
|
+
|
|
582
|
+
def _parse_ui_progress(self, element: ET.Element) -> UIProgressNode:
|
|
583
|
+
node = UIProgressNode()
|
|
584
|
+
node.value = element.get('value')
|
|
585
|
+
node.max = element.get('max')
|
|
586
|
+
self._apply_layout_attrs(node, element)
|
|
587
|
+
return node
|
|
588
|
+
|
|
589
|
+
def _parse_ui_tree(self, element: ET.Element) -> UITreeNode:
|
|
590
|
+
node = UITreeNode()
|
|
591
|
+
node.source = element.get('source')
|
|
592
|
+
node.on_select = element.get('on-select')
|
|
593
|
+
self._apply_layout_attrs(node, element)
|
|
594
|
+
return node
|
|
595
|
+
|
|
596
|
+
def _parse_ui_menu(self, element: ET.Element) -> UIMenuNode:
|
|
597
|
+
node = UIMenuNode()
|
|
598
|
+
self._apply_layout_attrs(node, element)
|
|
599
|
+
self._parse_children(element, node)
|
|
600
|
+
return node
|
|
601
|
+
|
|
602
|
+
def _parse_ui_option(self, element: ET.Element) -> UIOptionNode:
|
|
603
|
+
node = UIOptionNode()
|
|
604
|
+
node.value = element.get('value')
|
|
605
|
+
node.label = element.get('label') or (element.text or '').strip()
|
|
606
|
+
node.on_click = element.get('on-click')
|
|
607
|
+
return node
|
|
608
|
+
|
|
609
|
+
def _parse_ui_log(self, element: ET.Element) -> UILogNode:
|
|
610
|
+
node = UILogNode()
|
|
611
|
+
node.auto_scroll = self._parse_bool(element.get('auto-scroll'), True)
|
|
612
|
+
node.max_lines = self._parse_int(element.get('max-lines')) if element.get('max-lines') else None
|
|
613
|
+
self._apply_layout_attrs(node, element)
|
|
614
|
+
return node
|
|
615
|
+
|
|
616
|
+
def _parse_ui_markdown(self, element: ET.Element) -> UIMarkdownNode:
|
|
617
|
+
node = UIMarkdownNode()
|
|
618
|
+
node.content = (element.text or '').strip()
|
|
619
|
+
self._apply_layout_attrs(node, element)
|
|
620
|
+
return node
|
|
621
|
+
|
|
622
|
+
def _parse_ui_header(self, element: ET.Element) -> UIHeaderNode:
|
|
623
|
+
node = UIHeaderNode()
|
|
624
|
+
node.title = element.get('title')
|
|
625
|
+
node.content = (element.text or '').strip()
|
|
626
|
+
self._apply_layout_attrs(node, element)
|
|
627
|
+
self._parse_children(element, node)
|
|
628
|
+
return node
|
|
629
|
+
|
|
630
|
+
def _parse_ui_footer(self, element: ET.Element) -> UIFooterNode:
|
|
631
|
+
node = UIFooterNode()
|
|
632
|
+
node.content = (element.text or '').strip()
|
|
633
|
+
self._apply_layout_attrs(node, element)
|
|
634
|
+
self._parse_children(element, node)
|
|
635
|
+
return node
|
|
636
|
+
|
|
637
|
+
def _parse_ui_rule(self, element: ET.Element) -> UIRuleNode:
|
|
638
|
+
return UIRuleNode()
|
|
639
|
+
|
|
640
|
+
def _parse_ui_loading(self, element: ET.Element) -> UILoadingNode:
|
|
641
|
+
node = UILoadingNode()
|
|
642
|
+
node.text = element.get('text')
|
|
643
|
+
self._apply_layout_attrs(node, element)
|
|
644
|
+
return node
|
|
645
|
+
|
|
646
|
+
def _parse_ui_badge(self, element: ET.Element) -> UIBadgeNode:
|
|
647
|
+
node = UIBadgeNode()
|
|
648
|
+
node.content = (element.text or '').strip()
|
|
649
|
+
node.variant = element.get('variant')
|
|
650
|
+
node.badge_color = element.get('color')
|
|
651
|
+
self._apply_layout_attrs(node, element)
|
|
652
|
+
return node
|
|
653
|
+
|
|
654
|
+
def _parse_ui_validator(self, element: ET.Element) -> UIValidatorNode:
|
|
655
|
+
"""
|
|
656
|
+
Parse <ui:validator> custom validation rule.
|
|
657
|
+
|
|
658
|
+
Examples:
|
|
659
|
+
<ui:validator name="phone" pattern="^\\d{10}$" message="Invalid phone" />
|
|
660
|
+
<ui:validator name="confirmPassword" match="password"
|
|
661
|
+
message="Passwords must match" />
|
|
662
|
+
<ui:validator name="custom" expression="value.length > 5"
|
|
663
|
+
message="Too short" />
|
|
664
|
+
"""
|
|
665
|
+
node = UIValidatorNode()
|
|
666
|
+
|
|
667
|
+
# Identification
|
|
668
|
+
node.name = element.get('name')
|
|
669
|
+
node.field = element.get('field')
|
|
670
|
+
|
|
671
|
+
# Validation rules
|
|
672
|
+
node.rule_type = element.get('type') # 'pattern', 'email', 'url', etc.
|
|
673
|
+
node.pattern = element.get('pattern')
|
|
674
|
+
node.match = element.get('match')
|
|
675
|
+
node.min = element.get('min')
|
|
676
|
+
node.max = element.get('max')
|
|
677
|
+
node.expression = element.get('expression')
|
|
678
|
+
node.server_expression = element.get('server-expression')
|
|
679
|
+
|
|
680
|
+
# Integer validation attributes
|
|
681
|
+
minlength_str = element.get('minlength')
|
|
682
|
+
if minlength_str:
|
|
683
|
+
node.minlength = self._parse_int(minlength_str)
|
|
684
|
+
|
|
685
|
+
maxlength_str = element.get('maxlength')
|
|
686
|
+
if maxlength_str:
|
|
687
|
+
node.maxlength = self._parse_int(maxlength_str)
|
|
688
|
+
|
|
689
|
+
# Error handling
|
|
690
|
+
node.message = element.get('message')
|
|
691
|
+
node.trigger = element.get('trigger', 'submit')
|
|
692
|
+
|
|
693
|
+
return node
|
|
694
|
+
|
|
695
|
+
# ------------------------------------------------------------------
|
|
696
|
+
# Animation
|
|
697
|
+
# ------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
def _parse_ui_animate(self, element: ET.Element) -> UIAnimateNode:
|
|
700
|
+
"""
|
|
701
|
+
Parse <ui:animate> animation wrapper container.
|
|
702
|
+
|
|
703
|
+
Wraps child elements and applies animation effects based on the
|
|
704
|
+
specified animation type, duration, trigger, and other properties.
|
|
705
|
+
|
|
706
|
+
Syntax examples:
|
|
707
|
+
<ui:animate type="fade" duration="300" trigger="on-load">
|
|
708
|
+
<ui:panel>Content fades in</ui:panel>
|
|
709
|
+
</ui:animate>
|
|
710
|
+
|
|
711
|
+
<ui:animate type="slide-left" delay="200" easing="ease-out">
|
|
712
|
+
<ui:text>Slides in from left</ui:text>
|
|
713
|
+
</ui:animate>
|
|
714
|
+
|
|
715
|
+
<ui:animate type="scale" trigger="on-hover" duration="150">
|
|
716
|
+
<ui:button>Hover me</ui:button>
|
|
717
|
+
</ui:animate>
|
|
718
|
+
|
|
719
|
+
<ui:animate type="bounce" repeat="infinite">
|
|
720
|
+
<ui:badge>NEW</ui:badge>
|
|
721
|
+
</ui:animate>
|
|
722
|
+
|
|
723
|
+
Attributes:
|
|
724
|
+
type: Animation type (fade, slide, scale, rotate, slide-left, etc.)
|
|
725
|
+
duration: Animation duration in ms (default: 300)
|
|
726
|
+
delay: Animation delay in ms (default: 0)
|
|
727
|
+
easing: Easing function (ease, ease-in, ease-out, linear, spring)
|
|
728
|
+
repeat: Repeat count (1, 2, ..., 'infinite')
|
|
729
|
+
trigger: When to trigger (on-load, on-hover, on-click, on-visible)
|
|
730
|
+
direction: Animation direction (normal, reverse, alternate)
|
|
731
|
+
"""
|
|
732
|
+
node = UIAnimateNode()
|
|
733
|
+
|
|
734
|
+
# Apply layout attributes
|
|
735
|
+
self._apply_layout_attrs(node, element)
|
|
736
|
+
|
|
737
|
+
# Apply animation attributes
|
|
738
|
+
self._apply_animation_attrs(node, element)
|
|
739
|
+
|
|
740
|
+
# Parse children
|
|
741
|
+
self._parse_children(element, node)
|
|
742
|
+
|
|
743
|
+
return node
|
|
744
|
+
|
|
745
|
+
# ------------------------------------------------------------------
|
|
746
|
+
# Component Library
|
|
747
|
+
# ------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
def _parse_ui_card(self, element: ET.Element) -> UICardNode:
|
|
750
|
+
"""Parse <ui:card> - Card with header, body, footer."""
|
|
751
|
+
node = UICardNode()
|
|
752
|
+
node.title = element.get('title')
|
|
753
|
+
node.subtitle = element.get('subtitle')
|
|
754
|
+
node.image = element.get('image')
|
|
755
|
+
node.variant = element.get('variant')
|
|
756
|
+
self._apply_layout_attrs(node, element)
|
|
757
|
+
self._parse_children(element, node)
|
|
758
|
+
return node
|
|
759
|
+
|
|
760
|
+
def _parse_ui_card_header(self, element: ET.Element) -> UICardHeaderNode:
|
|
761
|
+
"""Parse <ui:card-header> - Card header section."""
|
|
762
|
+
node = UICardHeaderNode()
|
|
763
|
+
self._apply_layout_attrs(node, element)
|
|
764
|
+
self._parse_children(element, node)
|
|
765
|
+
return node
|
|
766
|
+
|
|
767
|
+
def _parse_ui_card_body(self, element: ET.Element) -> UICardBodyNode:
|
|
768
|
+
"""Parse <ui:card-body> - Card body section."""
|
|
769
|
+
node = UICardBodyNode()
|
|
770
|
+
self._apply_layout_attrs(node, element)
|
|
771
|
+
self._parse_children(element, node)
|
|
772
|
+
return node
|
|
773
|
+
|
|
774
|
+
def _parse_ui_card_footer(self, element: ET.Element) -> UICardFooterNode:
|
|
775
|
+
"""Parse <ui:card-footer> - Card footer section."""
|
|
776
|
+
node = UICardFooterNode()
|
|
777
|
+
self._apply_layout_attrs(node, element)
|
|
778
|
+
self._parse_children(element, node)
|
|
779
|
+
return node
|
|
780
|
+
|
|
781
|
+
def _parse_ui_modal(self, element: ET.Element) -> UIModalNode:
|
|
782
|
+
"""Parse <ui:modal> - Modal/dialog with open/close."""
|
|
783
|
+
node = UIModalNode()
|
|
784
|
+
node.title = element.get('title')
|
|
785
|
+
node.modal_id = element.get('modal-id') or element.get('id')
|
|
786
|
+
node.open = self._parse_bool(element.get('open'))
|
|
787
|
+
node.closable = self._parse_bool(element.get('closable'), True)
|
|
788
|
+
node.size = element.get('size')
|
|
789
|
+
self._apply_layout_attrs(node, element)
|
|
790
|
+
self._parse_children(element, node)
|
|
791
|
+
return node
|
|
792
|
+
|
|
793
|
+
def _parse_ui_chart(self, element: ET.Element) -> UIChartNode:
|
|
794
|
+
"""Parse <ui:chart> - Simple charts (bar, line, pie)."""
|
|
795
|
+
node = UIChartNode()
|
|
796
|
+
node.chart_type = element.get('type', 'bar')
|
|
797
|
+
node.source = element.get('source')
|
|
798
|
+
node.labels = element.get('labels')
|
|
799
|
+
node.values = element.get('values')
|
|
800
|
+
node.title = element.get('title')
|
|
801
|
+
node.colors = element.get('colors')
|
|
802
|
+
self._apply_layout_attrs(node, element)
|
|
803
|
+
return node
|
|
804
|
+
|
|
805
|
+
def _parse_ui_avatar(self, element: ET.Element) -> UIAvatarNode:
|
|
806
|
+
"""Parse <ui:avatar> - User avatar with image/initials."""
|
|
807
|
+
node = UIAvatarNode()
|
|
808
|
+
node.src = element.get('src')
|
|
809
|
+
node.name = element.get('name')
|
|
810
|
+
node.size = element.get('size')
|
|
811
|
+
node.shape = element.get('shape', 'circle')
|
|
812
|
+
node.status = element.get('status')
|
|
813
|
+
self._apply_layout_attrs(node, element)
|
|
814
|
+
return node
|
|
815
|
+
|
|
816
|
+
def _parse_ui_tooltip(self, element: ET.Element) -> UITooltipNode:
|
|
817
|
+
"""Parse <ui:tooltip> - Tooltip on hover."""
|
|
818
|
+
node = UITooltipNode()
|
|
819
|
+
node.content = element.get('content') or (element.text or '').strip()
|
|
820
|
+
node.position = element.get('position', 'top')
|
|
821
|
+
self._apply_layout_attrs(node, element)
|
|
822
|
+
self._parse_children(element, node)
|
|
823
|
+
return node
|
|
824
|
+
|
|
825
|
+
def _parse_ui_dropdown(self, element: ET.Element) -> UIDropdownNode:
|
|
826
|
+
"""Parse <ui:dropdown> - Dropdown menu."""
|
|
827
|
+
node = UIDropdownNode()
|
|
828
|
+
node.label = element.get('label')
|
|
829
|
+
node.trigger = element.get('trigger', 'click')
|
|
830
|
+
node.dropdown_align = element.get('align', 'left')
|
|
831
|
+
self._apply_layout_attrs(node, element)
|
|
832
|
+
self._parse_children(element, node)
|
|
833
|
+
return node
|
|
834
|
+
|
|
835
|
+
def _parse_ui_alert(self, element: ET.Element) -> UIAlertNode:
|
|
836
|
+
"""Parse <ui:alert> - Alert/notification box."""
|
|
837
|
+
node = UIAlertNode()
|
|
838
|
+
node.content = (element.text or '').strip()
|
|
839
|
+
node.title = element.get('title')
|
|
840
|
+
node.variant = element.get('variant', 'info')
|
|
841
|
+
node.dismissible = self._parse_bool(element.get('dismissible'))
|
|
842
|
+
node.icon = element.get('icon')
|
|
843
|
+
self._apply_layout_attrs(node, element)
|
|
844
|
+
self._parse_children(element, node)
|
|
845
|
+
return node
|
|
846
|
+
|
|
847
|
+
def _parse_ui_breadcrumb(self, element: ET.Element) -> UIBreadcrumbNode:
|
|
848
|
+
"""Parse <ui:breadcrumb> - Navigation breadcrumbs."""
|
|
849
|
+
node = UIBreadcrumbNode()
|
|
850
|
+
node.separator = element.get('separator', '/')
|
|
851
|
+
self._apply_layout_attrs(node, element)
|
|
852
|
+
self._parse_children(element, node)
|
|
853
|
+
return node
|
|
854
|
+
|
|
855
|
+
def _parse_ui_breadcrumb_item(self, element: ET.Element) -> UIBreadcrumbItemNode:
|
|
856
|
+
"""Parse <ui:breadcrumb-item> - Individual breadcrumb item."""
|
|
857
|
+
node = UIBreadcrumbItemNode()
|
|
858
|
+
node.label = element.get('label') or (element.text or '').strip()
|
|
859
|
+
node.to = element.get('to')
|
|
860
|
+
node.icon = element.get('icon')
|
|
861
|
+
return node
|
|
862
|
+
|
|
863
|
+
def _parse_ui_pagination(self, element: ET.Element) -> UIPaginationNode:
|
|
864
|
+
"""Parse <ui:pagination> - Pagination controls."""
|
|
865
|
+
node = UIPaginationNode()
|
|
866
|
+
node.total = element.get('total')
|
|
867
|
+
node.page_size = element.get('page-size', '10')
|
|
868
|
+
node.current = element.get('current', '1')
|
|
869
|
+
node.bind = element.get('bind')
|
|
870
|
+
node.on_change = element.get('on-change')
|
|
871
|
+
node.show_total = self._parse_bool(element.get('show-total'))
|
|
872
|
+
node.show_jump = self._parse_bool(element.get('show-jump'))
|
|
873
|
+
self._apply_layout_attrs(node, element)
|
|
874
|
+
return node
|
|
875
|
+
|
|
876
|
+
def _parse_ui_skeleton(self, element: ET.Element) -> UISkeletonNode:
|
|
877
|
+
"""Parse <ui:skeleton> - Loading skeleton placeholder."""
|
|
878
|
+
node = UISkeletonNode()
|
|
879
|
+
node.variant = element.get('variant', 'text')
|
|
880
|
+
node.lines = self._parse_int(element.get('lines'), 1)
|
|
881
|
+
node.animated = self._parse_bool(element.get('animated'), True)
|
|
882
|
+
self._apply_layout_attrs(node, element)
|
|
883
|
+
return node
|
|
884
|
+
|
|
885
|
+
# ------------------------------------------------------------------
|
|
886
|
+
# Toast Notifications
|
|
887
|
+
# ------------------------------------------------------------------
|
|
888
|
+
|
|
889
|
+
def _parse_ui_toast(self, element: ET.Element) -> UIToastNode:
|
|
890
|
+
"""
|
|
891
|
+
Parse <ui:toast> - Temporary notification message.
|
|
892
|
+
|
|
893
|
+
Syntax:
|
|
894
|
+
<ui:toast variant="success" position="top-right" duration="3000">
|
|
895
|
+
File saved successfully!
|
|
896
|
+
</ui:toast>
|
|
897
|
+
|
|
898
|
+
<ui:toast variant="danger" title="Error" message="Could not save"
|
|
899
|
+
show="hasError" on-close="clearError()" />
|
|
900
|
+
|
|
901
|
+
Attributes:
|
|
902
|
+
variant: info, success, warning, danger
|
|
903
|
+
position: top-right, top-left, bottom-right, bottom-left,
|
|
904
|
+
top-center, bottom-center
|
|
905
|
+
duration: auto-dismiss time in ms (0 = no auto-dismiss)
|
|
906
|
+
dismissible: show close button (default true)
|
|
907
|
+
icon: icon name
|
|
908
|
+
title: optional bold heading
|
|
909
|
+
message: body text (falls back to the element text content)
|
|
910
|
+
show: binding expression controlling visibility
|
|
911
|
+
on-close: callback fired when the toast is dismissed
|
|
912
|
+
"""
|
|
913
|
+
node = UIToastNode()
|
|
914
|
+
node.variant = element.get('variant', 'info')
|
|
915
|
+
node.position = element.get('position', 'top-right')
|
|
916
|
+
node.duration = self._parse_int(element.get('duration'), 3000)
|
|
917
|
+
node.dismissible = self._parse_bool(element.get('dismissible'), True)
|
|
918
|
+
node.icon = element.get('icon')
|
|
919
|
+
node.title = element.get('title')
|
|
920
|
+
node.message = element.get('message') or (element.text or '').strip()
|
|
921
|
+
node.show = element.get('show')
|
|
922
|
+
node.on_close = element.get('on-close')
|
|
923
|
+
self._apply_layout_attrs(node, element)
|
|
924
|
+
return node
|
|
925
|
+
|
|
926
|
+
def _parse_ui_toast_container(self, element: ET.Element) -> UIToastContainerNode:
|
|
927
|
+
"""
|
|
928
|
+
Parse <ui:toast-container> - Host container for toast notifications.
|
|
929
|
+
|
|
930
|
+
Syntax:
|
|
931
|
+
<ui:toast-container position="top-right" max-toasts="5" />
|
|
932
|
+
|
|
933
|
+
Attributes:
|
|
934
|
+
position: default position for toasts spawned in this container
|
|
935
|
+
max-toasts: maximum visible toasts (older ones are dismissed)
|
|
936
|
+
"""
|
|
937
|
+
node = UIToastContainerNode()
|
|
938
|
+
node.position = element.get('position', 'top-right')
|
|
939
|
+
node.max_toasts = self._parse_int(element.get('max-toasts'), 5)
|
|
940
|
+
self._apply_layout_attrs(node, element)
|
|
941
|
+
return node
|
|
942
|
+
|
|
943
|
+
# ------------------------------------------------------------------
|
|
944
|
+
# Carousel / Slider
|
|
945
|
+
# ------------------------------------------------------------------
|
|
946
|
+
|
|
947
|
+
def _parse_ui_carousel(self, element: ET.Element) -> UICarouselNode:
|
|
948
|
+
"""
|
|
949
|
+
Parse <ui:carousel> - Slider containing <ui:slide> children.
|
|
950
|
+
|
|
951
|
+
Syntax:
|
|
952
|
+
<ui:carousel auto-play="true" interval="4000"
|
|
953
|
+
show-indicators="true" show-arrows="true"
|
|
954
|
+
animation="slide">
|
|
955
|
+
<ui:slide><ui:image src="a.jpg" /></ui:slide>
|
|
956
|
+
<ui:slide><ui:image src="b.jpg" /></ui:slide>
|
|
957
|
+
</ui:carousel>
|
|
958
|
+
|
|
959
|
+
Attributes:
|
|
960
|
+
auto-play: advance slides automatically (default false)
|
|
961
|
+
interval: ms between slides when auto-playing (default 5000)
|
|
962
|
+
show-indicators: render dot indicators (default true)
|
|
963
|
+
show-arrows: render prev/next arrows (default true)
|
|
964
|
+
loop: wrap around after the last slide (default true)
|
|
965
|
+
animation: slide | fade
|
|
966
|
+
current: initial slide index (0-based)
|
|
967
|
+
bind: binding for the current slide index
|
|
968
|
+
on-change: callback fired when the slide changes
|
|
969
|
+
"""
|
|
970
|
+
node = UICarouselNode()
|
|
971
|
+
node.auto_play = self._parse_bool(element.get('auto-play'))
|
|
972
|
+
node.interval = self._parse_int(element.get('interval'), 5000)
|
|
973
|
+
node.show_indicators = self._parse_bool(element.get('show-indicators'), True)
|
|
974
|
+
node.show_arrows = self._parse_bool(element.get('show-arrows'), True)
|
|
975
|
+
node.loop = self._parse_bool(element.get('loop'), True)
|
|
976
|
+
node.animation = element.get('animation', 'slide')
|
|
977
|
+
node.current = self._parse_int(element.get('current'), 0)
|
|
978
|
+
node.bind = element.get('bind')
|
|
979
|
+
node.on_change = element.get('on-change')
|
|
980
|
+
self._apply_layout_attrs(node, element)
|
|
981
|
+
# Parse child <ui:slide> elements (and any other allowed content)
|
|
982
|
+
self._parse_children(element, node)
|
|
983
|
+
return node
|
|
984
|
+
|
|
985
|
+
def _parse_ui_slide(self, element: ET.Element) -> UISlideNode:
|
|
986
|
+
"""Parse <ui:slide> - Single slide inside a <ui:carousel>."""
|
|
987
|
+
node = UISlideNode()
|
|
988
|
+
self._apply_layout_attrs(node, element)
|
|
989
|
+
self._parse_children(element, node)
|
|
990
|
+
return node
|
|
991
|
+
|
|
992
|
+
# ------------------------------------------------------------------
|
|
993
|
+
# Stepper / Wizard
|
|
994
|
+
# ------------------------------------------------------------------
|
|
995
|
+
|
|
996
|
+
def _parse_ui_stepper(self, element: ET.Element) -> UIStepperNode:
|
|
997
|
+
"""
|
|
998
|
+
Parse <ui:stepper> - Multi-step wizard containing <ui:step> children.
|
|
999
|
+
|
|
1000
|
+
Syntax:
|
|
1001
|
+
<ui:stepper orientation="horizontal" current="0" linear="true">
|
|
1002
|
+
<ui:step title="Account" description="Create account">...</ui:step>
|
|
1003
|
+
<ui:step title="Done" description="All set!">...</ui:step>
|
|
1004
|
+
</ui:stepper>
|
|
1005
|
+
|
|
1006
|
+
Attributes:
|
|
1007
|
+
orientation: horizontal | vertical
|
|
1008
|
+
current: active step index (0-based)
|
|
1009
|
+
linear: steps must be completed in order (default true)
|
|
1010
|
+
show-labels: render step titles under the indicators (default true)
|
|
1011
|
+
clickable: allow clicking an indicator to navigate (default false)
|
|
1012
|
+
bind: binding for the current step index
|
|
1013
|
+
on-change: callback fired when the active step changes
|
|
1014
|
+
on-complete: callback fired when the last step is completed
|
|
1015
|
+
"""
|
|
1016
|
+
node = UIStepperNode()
|
|
1017
|
+
node.orientation = element.get('orientation', 'horizontal')
|
|
1018
|
+
node.current = self._parse_int(element.get('current'), 0)
|
|
1019
|
+
node.linear = self._parse_bool(element.get('linear'), True)
|
|
1020
|
+
node.show_labels = self._parse_bool(element.get('show-labels'), True)
|
|
1021
|
+
node.clickable = self._parse_bool(element.get('clickable'))
|
|
1022
|
+
node.bind = element.get('bind')
|
|
1023
|
+
node.on_change = element.get('on-change')
|
|
1024
|
+
node.on_complete = element.get('on-complete')
|
|
1025
|
+
self._apply_layout_attrs(node, element)
|
|
1026
|
+
# Parse child <ui:step> elements
|
|
1027
|
+
self._parse_children(element, node)
|
|
1028
|
+
return node
|
|
1029
|
+
|
|
1030
|
+
def _parse_ui_step(self, element: ET.Element) -> UIStepNode:
|
|
1031
|
+
"""
|
|
1032
|
+
Parse <ui:step> - Single step inside a <ui:stepper>.
|
|
1033
|
+
|
|
1034
|
+
Attributes:
|
|
1035
|
+
title: step title shown in the indicator (required)
|
|
1036
|
+
description: optional supporting text
|
|
1037
|
+
icon: optional icon name
|
|
1038
|
+
optional: step may be skipped (default false)
|
|
1039
|
+
completed: explicit completed-state override
|
|
1040
|
+
error: error message to display for this step
|
|
1041
|
+
"""
|
|
1042
|
+
node = UIStepNode()
|
|
1043
|
+
node.title = element.get('title', '')
|
|
1044
|
+
node.description = element.get('description')
|
|
1045
|
+
node.icon = element.get('icon')
|
|
1046
|
+
node.optional = self._parse_bool(element.get('optional'))
|
|
1047
|
+
|
|
1048
|
+
# completed is a tri-state override: leave as None when not declared
|
|
1049
|
+
completed_str = element.get('completed')
|
|
1050
|
+
if completed_str is not None:
|
|
1051
|
+
node.completed = self._parse_bool(completed_str)
|
|
1052
|
+
|
|
1053
|
+
node.error = element.get('error')
|
|
1054
|
+
self._apply_layout_attrs(node, element)
|
|
1055
|
+
self._parse_children(element, node)
|
|
1056
|
+
return node
|
|
1057
|
+
|
|
1058
|
+
# ------------------------------------------------------------------
|
|
1059
|
+
# Calendar / Date Picker
|
|
1060
|
+
# ------------------------------------------------------------------
|
|
1061
|
+
|
|
1062
|
+
def _parse_ui_calendar(self, element: ET.Element) -> UICalendarNode:
|
|
1063
|
+
"""
|
|
1064
|
+
Parse <ui:calendar> - Inline calendar for date selection.
|
|
1065
|
+
|
|
1066
|
+
Syntax:
|
|
1067
|
+
<ui:calendar mode="single" bind="selectedDate" min-date="2024-01-01" />
|
|
1068
|
+
<ui:calendar mode="range" bind="dateRange" first-day-of-week="1" />
|
|
1069
|
+
|
|
1070
|
+
Attributes:
|
|
1071
|
+
mode: single | range | multiple
|
|
1072
|
+
value: preselected date(s), ISO format (comma-separated for multiple)
|
|
1073
|
+
bind: binding for the selected date(s)
|
|
1074
|
+
min-date / max-date: selectable range bounds (ISO)
|
|
1075
|
+
disabled-dates: comma-separated ISO dates that cannot be picked
|
|
1076
|
+
disabled-days: comma-separated weekday numbers (0-6) to disable
|
|
1077
|
+
show-week-numbers: render the week-number column (default false)
|
|
1078
|
+
first-day-of-week: 0=Sunday .. 6=Saturday (default 0)
|
|
1079
|
+
locale: locale for month/day names (default 'en')
|
|
1080
|
+
inline: render inline instead of as a popup (default true)
|
|
1081
|
+
on-change: callback fired when the selection changes
|
|
1082
|
+
on-month-change: callback fired when navigating months
|
|
1083
|
+
"""
|
|
1084
|
+
node = UICalendarNode()
|
|
1085
|
+
node.mode = element.get('mode', 'single')
|
|
1086
|
+
node.value = element.get('value')
|
|
1087
|
+
node.bind = element.get('bind')
|
|
1088
|
+
node.min_date = element.get('min-date')
|
|
1089
|
+
node.max_date = element.get('max-date')
|
|
1090
|
+
node.disabled_dates = element.get('disabled-dates')
|
|
1091
|
+
node.disabled_days = element.get('disabled-days')
|
|
1092
|
+
node.show_week_numbers = self._parse_bool(element.get('show-week-numbers'))
|
|
1093
|
+
node.first_day_of_week = self._parse_int(element.get('first-day-of-week'), 0)
|
|
1094
|
+
node.locale = element.get('locale', 'en')
|
|
1095
|
+
node.inline = self._parse_bool(element.get('inline'), True)
|
|
1096
|
+
node.on_change = element.get('on-change')
|
|
1097
|
+
node.on_month_change = element.get('on-month-change')
|
|
1098
|
+
self._apply_layout_attrs(node, element)
|
|
1099
|
+
return node
|
|
1100
|
+
|
|
1101
|
+
def _parse_ui_date_picker(self, element: ET.Element) -> UIDatePickerNode:
|
|
1102
|
+
"""
|
|
1103
|
+
Parse <ui:date-picker> - Text input with a calendar popup.
|
|
1104
|
+
|
|
1105
|
+
Syntax:
|
|
1106
|
+
<ui:date-picker bind="birthDate" placeholder="Select date"
|
|
1107
|
+
format="MM/DD/YYYY" min-date="2024-01-01" />
|
|
1108
|
+
|
|
1109
|
+
Attributes:
|
|
1110
|
+
value: initial date (ISO)
|
|
1111
|
+
bind: binding for the selected date
|
|
1112
|
+
placeholder: input placeholder (default 'Select date')
|
|
1113
|
+
format: display format (default 'YYYY-MM-DD')
|
|
1114
|
+
min-date / max-date: selectable range bounds (ISO)
|
|
1115
|
+
clearable: show the clear button (default true)
|
|
1116
|
+
disabled: render the input as disabled (default false)
|
|
1117
|
+
required: mark the field as required (default false)
|
|
1118
|
+
on-change: callback fired when the date changes
|
|
1119
|
+
"""
|
|
1120
|
+
node = UIDatePickerNode()
|
|
1121
|
+
node.value = element.get('value')
|
|
1122
|
+
node.bind = element.get('bind')
|
|
1123
|
+
node.placeholder = element.get('placeholder', 'Select date')
|
|
1124
|
+
node.format = element.get('format', 'YYYY-MM-DD')
|
|
1125
|
+
node.min_date = element.get('min-date')
|
|
1126
|
+
node.max_date = element.get('max-date')
|
|
1127
|
+
node.clearable = self._parse_bool(element.get('clearable'), True)
|
|
1128
|
+
node.disabled = self._parse_bool(element.get('disabled'))
|
|
1129
|
+
node.required = self._parse_bool(element.get('required'))
|
|
1130
|
+
node.on_change = element.get('on-change')
|
|
1131
|
+
self._apply_layout_attrs(node, element)
|
|
1132
|
+
return node
|
|
1133
|
+
|
|
1134
|
+
# ------------------------------------------------------------------
|
|
1135
|
+
# Theming
|
|
1136
|
+
# ------------------------------------------------------------------
|
|
1137
|
+
|
|
1138
|
+
def _parse_ui_theme(self, element: ET.Element) -> UIThemeNode:
|
|
1139
|
+
"""
|
|
1140
|
+
Parse <ui:theme> theme configuration.
|
|
1141
|
+
|
|
1142
|
+
Syntax examples:
|
|
1143
|
+
<!-- Use a preset theme -->
|
|
1144
|
+
<ui:theme preset="dark" />
|
|
1145
|
+
|
|
1146
|
+
<!-- Custom theme extending a preset -->
|
|
1147
|
+
<ui:theme name="ocean" preset="light">
|
|
1148
|
+
<ui:color name="primary" value="#0ea5e9" />
|
|
1149
|
+
<ui:color name="secondary" value="#06b6d4" />
|
|
1150
|
+
</ui:theme>
|
|
1151
|
+
|
|
1152
|
+
<!-- Auto-switch based on system preference -->
|
|
1153
|
+
<ui:theme preset="light" auto-switch="true" />
|
|
1154
|
+
|
|
1155
|
+
Attributes:
|
|
1156
|
+
name: Custom theme name (optional)
|
|
1157
|
+
preset: Base theme preset ('light', 'dark')
|
|
1158
|
+
auto-switch: Enable automatic dark/light switching
|
|
1159
|
+
"""
|
|
1160
|
+
node = UIThemeNode()
|
|
1161
|
+
|
|
1162
|
+
node.name = element.get('name')
|
|
1163
|
+
node.preset = element.get('preset', 'light')
|
|
1164
|
+
node.auto_switch = self._parse_bool(element.get('auto-switch'))
|
|
1165
|
+
|
|
1166
|
+
# Parse child <ui:color> elements
|
|
1167
|
+
for child in element:
|
|
1168
|
+
local = self._get_local_name(child)
|
|
1169
|
+
if local == 'color':
|
|
1170
|
+
color = self._parse_ui_color(child)
|
|
1171
|
+
node.add_color(color)
|
|
1172
|
+
|
|
1173
|
+
return node
|
|
1174
|
+
|
|
1175
|
+
def _parse_ui_color(self, element: ET.Element) -> UIColorNode:
|
|
1176
|
+
"""
|
|
1177
|
+
Parse <ui:color> custom color definition.
|
|
1178
|
+
|
|
1179
|
+
Syntax:
|
|
1180
|
+
<ui:color name="primary" value="#3b82f6" />
|
|
1181
|
+
<ui:color name="background" value="rgb(15, 23, 42)" />
|
|
1182
|
+
|
|
1183
|
+
Attributes:
|
|
1184
|
+
name: Color token name (e.g., 'primary', 'background')
|
|
1185
|
+
value: CSS color value (hex, rgb, hsl, etc.)
|
|
1186
|
+
"""
|
|
1187
|
+
node = UIColorNode()
|
|
1188
|
+
|
|
1189
|
+
node.name = element.get('name', '')
|
|
1190
|
+
node.value = element.get('value', '')
|
|
1191
|
+
|
|
1192
|
+
return node
|