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.
Files changed (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. 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