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