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
quantum/core/parser.py ADDED
@@ -0,0 +1,1235 @@
1
+ """
2
+ Quantum Parser - Convert .q files (XML) to Quantum AST
3
+ """
4
+
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+ from xml.etree import ElementTree as ET
9
+ from typing import Union, Optional, List
10
+
11
+ # Fix imports
12
+
13
+ from quantum.core.html_compat import normalise_html
14
+ from quantum.core.ast_nodes import (
15
+ QuantumNode, ComponentNode, ApplicationNode, JobNode,
16
+ QuantumParam, QuantumReturn, QuantumRoute,
17
+ DispatchEventNode, OnEventNode, QueryNode, QueryParamNode,
18
+ InvokeHeaderNode, DataNode, ColumnNode, FieldNode, TransformNode,
19
+ FilterNode, SortNode, LimitNode, ComputeNode, HeaderNode,
20
+ HTMLNode, TextNode, DocTypeNode, CommentNode, HTML_VOID_ELEMENTS,
21
+ ImportNode, SlotNode, ComponentCallNode,
22
+ ActionNode, RedirectNode, FlashNode, FileNode, MailNode, TransactionNode,
23
+ LLMNode, LLMMessageNode,
24
+ ScheduleNode, ThreadNode, # Job Execution System
25
+ # Message Queue System
26
+ MessageNode, MessageHeaderNode, SubscribeNode, QueueNode,
27
+ MessageAckNode, MessageNackNode,
28
+ # Python Scripting System
29
+ PythonNode, PyImportNode, PyClassNode, PyDecoratorNode, PyExprNode
30
+ )
31
+ # Import feature AST nodes from their respective modules
32
+ from quantum.core.features.conditionals.src.ast_node import IfNode
33
+ from quantum.core.features.loops.src.ast_node import LoopNode
34
+ from quantum.core.features.state_management.src.ast_node import SetNode, PersistNode
35
+ from quantum.core.features.functions.src.ast_node import FunctionNode, RestConfig
36
+ from quantum.core.features.invocation.src.ast_node import InvokeNode
37
+ from quantum.core.features.logging.src import LogNode, parse_log
38
+ from quantum.core.features.dump.src import DumpNode, parse_dump
39
+ from quantum.core.features.knowledge_base.src import KnowledgeNode, KnowledgeSourceNode, parse_knowledge
40
+ from quantum.core.features.agents.src import (
41
+ AgentNode, AgentInstructionNode, AgentToolNode, AgentToolParamNode, AgentExecuteNode,
42
+ AgentTeamNode, AgentHandoffNode
43
+ )
44
+ from quantum.core.features.websocket.src import (
45
+ WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
46
+ )
47
+ from quantum.core.features.game_engine_2d.src.parser import GameParser, GameParseError
48
+ from quantum.core.features.game_engine_2d.src.ast_nodes import (
49
+ SceneNode, BehaviorNode, PrefabNode, PersistentNode, EnemyNode,
50
+ )
51
+ from quantum.core.features.terminal_engine.src.parser import TerminalParser, TerminalParseError
52
+ from quantum.core.features.terminal_engine.src.ast_nodes import (
53
+ ScreenNode as TerminalScreenNode, KeybindingNode as TerminalKeybindingNode,
54
+ ServiceNode as TerminalServiceNode, CssNode as TerminalCssNode,
55
+ )
56
+ from quantum.core.features.testing_engine.src.parser import TestingParser, TestingParseError
57
+ from quantum.core.features.testing_engine.src.ast_nodes import (
58
+ QTestSuiteNode as TestingTestSuiteNode,
59
+ BrowserConfigNode as TestingBrowserConfigNode,
60
+ FixtureNode_Testing as TestingFixtureNode,
61
+ MockNode_Testing as TestingMockNode,
62
+ AuthNode as TestingAuthNode,
63
+ )
64
+ from quantum.core.features.ui_engine.src.parser import UIParser, UIParseError
65
+ from quantum.core.features.ui_engine.src.ast_nodes import UIWindowNode
66
+ from quantum.core.features.theming.src import UIThemeNode
67
+ from quantum.core.parser_registry import ParserRegistry
68
+ import logging
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ def _create_parser_registry(parser: 'QuantumParser') -> ParserRegistry:
74
+ """
75
+ Create and populate the parser registry with all modular parsers.
76
+
77
+ This factory function creates all modular parsers and registers them.
78
+ The registry can then dispatch parsing to the appropriate parser
79
+ based on tag name.
80
+
81
+ Args:
82
+ parser: QuantumParser instance for parser initialization
83
+
84
+ Returns:
85
+ Populated ParserRegistry
86
+ """
87
+ from quantum.core.parsers import (
88
+ # Control flow
89
+ IfParser, LoopParser, SetParser,
90
+ # Data
91
+ QueryParser, InvokeParser, DataParser, TransactionParser,
92
+ # Services
93
+ LogParser, DumpParser, FileParser, MailParser,
94
+ # AI
95
+ LLMParser, AgentParser, TeamParser, KnowledgeParser,
96
+ # Messaging
97
+ WebSocketParser, WebSocketSendParser, WebSocketCloseParser,
98
+ MessageParser, SubscribeParser, QueueParser,
99
+ # Jobs
100
+ ScheduleParser, ThreadParser, JobParser,
101
+ # Scripting
102
+ PythonParser, PyImportParser, PyClassParser,
103
+ # HTML
104
+ HTMLParser, ComponentCallParser,
105
+ )
106
+ # New modular parsers (Phase 2 - Operacao Guilhotina)
107
+ from quantum.core.parsers.forms import ActionParser, RedirectParser, FlashParser
108
+ from quantum.core.parsers.composition import ImportParser, SlotParser
109
+ from quantum.core.parsers.functions import FunctionParser, ReturnParser, ParamParser
110
+ from quantum.core.parsers.events import DispatchEventParser
111
+ from quantum.core.parsers.persistence import PersistParser
112
+ from quantum.core.parsers.routing import RouteParser
113
+ from quantum.core.parsers.messaging import MessageAckParser, MessageNackParser
114
+ from quantum.core.parsers.scripting import PyDecoratorParser
115
+
116
+ registry = ParserRegistry()
117
+
118
+ # Register all tag-based parsers
119
+ parsers = [
120
+ # Control flow
121
+ IfParser(parser),
122
+ LoopParser(parser),
123
+ SetParser(parser),
124
+ # Data
125
+ QueryParser(parser),
126
+ InvokeParser(parser),
127
+ DataParser(parser),
128
+ TransactionParser(parser),
129
+ # Services
130
+ LogParser(parser),
131
+ DumpParser(parser),
132
+ FileParser(parser),
133
+ MailParser(parser),
134
+ # AI
135
+ LLMParser(parser),
136
+ AgentParser(parser),
137
+ TeamParser(parser),
138
+ KnowledgeParser(parser),
139
+ # Messaging
140
+ WebSocketParser(parser),
141
+ WebSocketSendParser(parser),
142
+ WebSocketCloseParser(parser),
143
+ MessageParser(parser),
144
+ SubscribeParser(parser),
145
+ QueueParser(parser),
146
+ MessageAckParser(parser),
147
+ MessageNackParser(parser),
148
+ # Jobs
149
+ ScheduleParser(parser),
150
+ ThreadParser(parser),
151
+ JobParser(parser),
152
+ # Scripting
153
+ PythonParser(parser),
154
+ PyImportParser(parser),
155
+ PyClassParser(parser),
156
+ PyDecoratorParser(parser),
157
+ # HTML (registers for common HTML tags)
158
+ HTMLParser(parser),
159
+ # Forms & Actions
160
+ ActionParser(parser),
161
+ RedirectParser(parser),
162
+ FlashParser(parser),
163
+ # Component Composition
164
+ ImportParser(parser),
165
+ SlotParser(parser),
166
+ # Functions
167
+ FunctionParser(parser),
168
+ # Note: ReturnParser, ParamParser, RouteParser are not registered here
169
+ # They are internal parsers used by FunctionParser and other parsers
170
+ # Events
171
+ DispatchEventParser(parser),
172
+ # Persistence
173
+ PersistParser(parser),
174
+ ]
175
+
176
+ registry.register_all(parsers)
177
+
178
+ # Store special parsers for fallback handling
179
+ # These are used when no specific tag parser matches
180
+ registry.html_parser = HTMLParser(parser)
181
+ registry.component_call_parser = ComponentCallParser(parser)
182
+
183
+ logger.debug(f"Initialized parser registry with {registry.parser_count} parsers")
184
+
185
+ return registry
186
+
187
+
188
+ class QuantumParseError(Exception):
189
+ """Quantum parsing error"""
190
+ pass
191
+
192
+
193
+ def _explain_xml_error(error, source: str) -> str:
194
+ """Turn a raw ElementTree error into something actionable.
195
+
196
+ A .q file is XML, so a bare `<` or `&` inside SQL or JavaScript breaks
197
+ parsing with a message ("not well-formed (invalid token)") that says
198
+ nothing about the actual cause. This has bitten the same way at least
199
+ twice (a JS `for` loop in q:llm, a SQL `<` in q:schedule), so name it.
200
+ """
201
+ message = f"XML parse error: {error}"
202
+
203
+ line_no = getattr(error, 'position', (None, None))[0]
204
+ offending = ''
205
+ if line_no:
206
+ lines = source.splitlines()
207
+ if 0 < line_no <= len(lines):
208
+ offending = lines[line_no - 1].strip()
209
+
210
+ if offending:
211
+ message += f"\n at line {line_no}: {offending}"
212
+
213
+ if offending and ('<' in offending or '&' in offending):
214
+ message += (
215
+ "\n Hint: a .q file is XML, so a bare '<' or '&' is invalid even "
216
+ "inside SQL or JavaScript.\n"
217
+ " Write '&lt;' / '&amp;', or wrap the body in <![CDATA[ ... ]]>."
218
+ )
219
+
220
+ return message
221
+
222
+
223
+ class QuantumParser:
224
+ """Main parser for .q files"""
225
+
226
+ def __init__(self, use_cache: bool = True, use_modular_parsers: bool = True):
227
+ self.quantum_ns = {'q': 'https://quantum.lang/ns'}
228
+ self._use_cache = use_cache
229
+ self._source_path: Optional[str] = None # Path of the file being parsed
230
+ self._ast_cache = None
231
+ if use_cache:
232
+ try:
233
+ from quantum.runtime.ast_cache import get_ast_cache
234
+ self._ast_cache = get_ast_cache()
235
+ except ImportError:
236
+ self._use_cache = False
237
+
238
+ # === NEW: Parser Registry for modular dispatch ===
239
+ self._use_modular_parsers = use_modular_parsers
240
+ self._parser_registry: ParserRegistry = None
241
+ if use_modular_parsers:
242
+ self._parser_registry = _create_parser_registry(self)
243
+ else:
244
+ import warnings
245
+ warnings.warn(
246
+ "use_modular_parsers=False is deprecated and will be removed in v2.0. "
247
+ "The modular ParserRegistry is now the default.",
248
+ DeprecationWarning,
249
+ stacklevel=2
250
+ )
251
+
252
+ @property
253
+ def parser_registry(self) -> ParserRegistry:
254
+ """Access to parser registry (may be None if not using modular parsers)"""
255
+ return self._parser_registry
256
+
257
+ # Prefixo -> URI que o framework injeta sozinho.
258
+ _NAMESPACES = {
259
+ 'q': 'https://quantum.lang/ns',
260
+ 'qg': 'https://quantum.lang/game',
261
+ 'qt': 'https://quantum.lang/terminal',
262
+ 'qtest': 'https://quantum.lang/testing',
263
+ 'ui': 'https://quantum.lang/ui',
264
+ }
265
+
266
+ # O primeiro elemento do documento, seja ele qual for.
267
+ _FIRST_ELEMENT = re.compile(r'<([A-Za-z_][\w.:-]*)((?:\s[^<>]*?)?)(/?)>')
268
+
269
+ def _inject_namespace(self, content: str) -> str:
270
+ """Declara os namespaces do framework que o autor nao declarou.
271
+
272
+ Isto permite escrever `<q:component name="Foo">` sem cerimonia, em
273
+ vez de `<q:component name="Foo" xmlns:q="https://quantum.lang/ns">`.
274
+
275
+ Duas coisas estavam erradas:
276
+
277
+ 1. **A deteccao era por SUBSTRING.** `if 'xmlns:q' in content` casa
278
+ com `xmlns:qt=`, `xmlns:qg=` e `xmlns:qtest=`. Um arquivo que
279
+ declarava so o namespace da sua engine caia no ramo "ja tem
280
+ namespace", e la dentro toda injecao era um
281
+ `content.replace('xmlns:q="https://quantum.lang/ns"', ...)` que
282
+ nao achava nada. Resultado: NENHUM namespace era declarado e o
283
+ arquivo morria com "unbound prefix" — e a mensagem apontava para
284
+ a linha 1, coluna 0, que nao diz nada a ninguem.
285
+
286
+ 2. **So tres raizes eram reconhecidas** (`q:component`,
287
+ `q:application`, `q:job`). Um arquivo cuja raiz e `<q:behavior>`
288
+ ou `<qg:scene>` — as cenas e comportamentos que o scene-include
289
+ carrega, 16 arquivos entregues no repositorio — nao casava com o
290
+ padrao e tambem nao recebia namespace nenhum.
291
+
292
+ Agora a deteccao e pelo prefixo EXATO (`xmlns:q=`) e a injecao vai no
293
+ primeiro elemento, qualquer que seja ele. Uma raiz que o framework
294
+ nao suporta passa a falhar em `_parse_root_element`, com o nome do
295
+ elemento na mensagem, em vez de "unbound prefix".
296
+ """
297
+ faltando = {}
298
+ for prefixo, uri in self._NAMESPACES.items():
299
+ if f'xmlns:{prefixo}=' in content:
300
+ continue # o autor declarou; respeitar a escolha dele
301
+ # So o que o documento realmente usa.
302
+ if f'<{prefixo}:' in content or f'</{prefixo}:' in content:
303
+ faltando[prefixo] = uri
304
+
305
+ # `type="game"` implica qg: mesmo antes de qualquer tag qg: aparecer,
306
+ # e assim por diante — comportamento que ja existia.
307
+ for atributo, prefixo in (('type="game"', 'qg'),
308
+ ('type="terminal"', 'qt'),
309
+ ('type="testing"', 'qtest'),
310
+ ('type="ui"', 'ui')):
311
+ if atributo in content and f'xmlns:{prefixo}=' not in content:
312
+ faltando[prefixo] = self._NAMESPACES[prefixo]
313
+
314
+ if not faltando:
315
+ return content
316
+
317
+ declaracoes = ''.join(f' xmlns:{p}="{u}"' for p, u in
318
+ sorted(faltando.items()))
319
+
320
+ def declarar(match):
321
+ nome, attrs, fecho = match.groups()
322
+ return f'<{nome}{attrs}{declaracoes}{fecho}>'
323
+
324
+ # Apenas no primeiro ELEMENTO. O prologo — declaracao XML, doctype e
325
+ # comentarios — e pulado inteiro: examples/python-scripting.q abre
326
+ # com um comentario que menciona `<cfscript>`, e casar ali injetaria
327
+ # os namespaces dentro do comentario, deixando o componente de
328
+ # verdade sem nenhum.
329
+ deslocamento = self._offset_do_primeiro_elemento(content)
330
+ substituido = self._FIRST_ELEMENT.sub(
331
+ declarar, content[deslocamento:], count=1)
332
+ return content[:deslocamento] + substituido
333
+
334
+ @staticmethod
335
+ def _offset_do_primeiro_elemento(content: str) -> int:
336
+ """Onde comeca o primeiro elemento de verdade."""
337
+ i = 0
338
+ n = len(content)
339
+ while i < n:
340
+ while i < n and content[i].isspace():
341
+ i += 1
342
+ if content.startswith('<?', i):
343
+ fim = content.find('?>', i)
344
+ if fim == -1:
345
+ return i
346
+ i = fim + 2
347
+ elif content.startswith('<!--', i):
348
+ fim = content.find('-->', i)
349
+ if fim == -1:
350
+ return i
351
+ i = fim + 3
352
+ elif content.startswith('<!', i): # DOCTYPE
353
+ fim = content.find('>', i)
354
+ if fim == -1:
355
+ return i
356
+ i = fim + 1
357
+ else:
358
+ return i
359
+ return 0
360
+
361
+ def parse(self, source: str) -> QuantumNode:
362
+ """Parse Quantum XML from a string."""
363
+ try:
364
+ # Accept the HTML people actually write: boolean attributes
365
+ # (<input required />) and bare ampersands (Forms & Actions) are
366
+ # valid HTML and invalid XML. Ten of the forty-eight components
367
+ # shipped with this framework — login.q among them — failed to
368
+ # parse for exactly those two reasons and returned HTTP 400.
369
+ content = normalise_html(source)
370
+ content = self._inject_namespace(content)
371
+ root = ET.fromstring(content)
372
+ return self._parse_root_element(root, Path("<string>"))
373
+ except ET.ParseError as e:
374
+ raise QuantumParseError(_explain_xml_error(e, source))
375
+ except QuantumParseError:
376
+ raise
377
+ except Exception as e:
378
+ raise QuantumParseError(f"Unexpected error: {e}")
379
+
380
+ def parse_file(self, file_path: str, use_cache: bool = None) -> QuantumNode:
381
+ """Parse .q file and return AST
382
+
383
+ Args:
384
+ file_path: Path to the .q file
385
+ use_cache: Override cache behavior (None = use instance default)
386
+ """
387
+ path = Path(file_path)
388
+
389
+ if not path.exists():
390
+ raise QuantumParseError(f"File not found: {file_path}")
391
+
392
+ if not path.suffix == '.q':
393
+ raise QuantumParseError(f"Invalid extension: {path.suffix}, expected .q")
394
+
395
+ # Determine if we should use cache
396
+ should_cache = use_cache if use_cache is not None else self._use_cache
397
+
398
+ try:
399
+ # Try cache first (Phase 2 optimization)
400
+ if should_cache and self._ast_cache is not None:
401
+ self._source_path = str(path.resolve())
402
+ return self._ast_cache.get_or_parse(file_path, self)
403
+
404
+ # Fallback to direct parsing
405
+ self._source_path = str(path.resolve())
406
+ source = Path(file_path).read_text(encoding="utf-8")
407
+ return self.parse(source)
408
+ except QuantumParseError:
409
+ raise
410
+ except Exception as e:
411
+ raise QuantumParseError(f"Unexpected error: {e}")
412
+
413
+ def invalidate_cache(self, file_path: str = None):
414
+ """Invalidate cached AST for a file or all files
415
+
416
+ Args:
417
+ file_path: Specific file to invalidate, or None for all
418
+ """
419
+ if self._ast_cache is not None:
420
+ self._ast_cache.invalidate(file_path)
421
+
422
+ def _parse_root_element(self, root: ET.Element, path: Path) -> QuantumNode:
423
+ """Parse root element based on type"""
424
+ root_type = self._get_element_name(root)
425
+
426
+ if root_type == 'component':
427
+ return self._parse_component(root, path)
428
+ elif root_type == 'application':
429
+ return self._parse_application(root, path)
430
+ elif root_type == 'job':
431
+ return self._parse_job(root, path)
432
+ else:
433
+ raise QuantumParseError(f"Unknown root element: {root_type}")
434
+
435
+ def _get_element_name(self, element: ET.Element) -> str:
436
+ """Extract element name removing namespace"""
437
+ return element.tag.split('}')[-1] if '}' in element.tag else element.tag.split(':')[-1]
438
+
439
+ def _find_element(self, parent: ET.Element, tag_name: str) -> Optional[ET.Element]:
440
+ """Find element considering namespace"""
441
+ # Try with namespace
442
+ element = parent.find(f"q:{tag_name}", self.quantum_ns)
443
+ if element is not None:
444
+ return element
445
+
446
+ # Try without namespace (fallback)
447
+ return parent.find(tag_name)
448
+
449
+ def _find_all_elements(self, parent: ET.Element, tag_name: str) -> list:
450
+ """Find all elements considering namespace"""
451
+ # Try with namespace
452
+ elements = parent.findall(f"q:{tag_name}", self.quantum_ns)
453
+ if elements:
454
+ return elements
455
+
456
+ # Try without namespace (fallback)
457
+ return parent.findall(tag_name)
458
+
459
+ def _is_html_element(self, element: ET.Element) -> bool:
460
+ """
461
+ Check if element is HTML (not a Quantum tag).
462
+
463
+ Returns True for:
464
+ - Regular HTML tags (div, span, p, etc.)
465
+ - DOCTYPE declarations
466
+ - Comments
467
+
468
+ Returns False for:
469
+ - Quantum tags (q:* namespace)
470
+ - Special Quantum tags (param, return, function, etc.)
471
+ """
472
+ tag = element.tag
473
+
474
+ # Skip namespace declarations and special elements
475
+ if tag.startswith('{'):
476
+ # Has XML namespace - check if it's Quantum namespace
477
+ if '{https://quantum.lang/ns}' in tag:
478
+ return False
479
+ # Other namespaces might be HTML5 or SVG
480
+ return True
481
+
482
+ # Check for q: prefix (Quantum tags)
483
+ if tag.startswith('q:'):
484
+ return False
485
+
486
+ # Quantum tags without prefix (when xmlns:q is set)
487
+ quantum_tags = {
488
+ 'component', 'application', 'job', 'param', 'return', 'route',
489
+ 'if', 'elseif', 'else', 'loop', 'set', 'function', 'dispatchEvent',
490
+ 'onEvent', 'script', 'query', 'invoke', 'data', 'log', 'dump',
491
+ 'import', 'slot', # Phase 2
492
+ 'knowledge', # Knowledge Base / RAG
493
+ 'schedule', 'thread', # Job Execution System
494
+ }
495
+ if tag in quantum_tags:
496
+ return False
497
+
498
+ # Component calls (Phase 2) - Uppercase tags are components, not HTML
499
+ if tag and tag[0].isupper():
500
+ return False
501
+
502
+ # Everything else is HTML
503
+ return True
504
+
505
+ def _parse_component(self, root: ET.Element, path: Path) -> ComponentNode:
506
+ """Parse q:component"""
507
+ name = root.get('name', path.stem)
508
+ component_type = root.get('type', 'pure')
509
+
510
+ component = ComponentNode(name, component_type)
511
+
512
+ # Parse component-level attributes
513
+ component.port = root.get('port')
514
+ component.base_path = root.get('basePath')
515
+ component.health_endpoint = root.get('health')
516
+ component.metrics_provider = root.get('metrics')
517
+ component.trace_provider = root.get('trace')
518
+
519
+ # Phase G: Authentication & Security attributes
520
+ require_auth_attr = root.get('require_auth', 'false').lower()
521
+ component.require_auth = require_auth_attr in ['true', '1', 'yes']
522
+ component.require_role = root.get('require_role')
523
+ component.require_permission = root.get('require_permission')
524
+
525
+ # HTML rendering & interactivity (Phase 1 & future Phase 3)
526
+ interactive_attr = root.get('interactive', 'false').lower()
527
+ component.interactive = interactive_attr in ['true', '1', 'yes']
528
+
529
+ # Parse q:param elements (component-level params)
530
+ for param_el in self._find_all_elements(root, 'param'):
531
+ param = self._parse_param(param_el)
532
+ component.add_param(param)
533
+
534
+ # Parse q:return elements (component-level returns)
535
+ for return_el in self._find_all_elements(root, 'return'):
536
+ return_node = self._parse_return(return_el)
537
+ component.add_return(return_node)
538
+
539
+ # Parse q:function elements
540
+ for func_el in self._find_all_elements(root, 'function'):
541
+ func = self._parse_function(func_el)
542
+ component.add_function(func)
543
+
544
+ # Parse q:onEvent elements
545
+ for event_el in self._find_all_elements(root, 'onEvent'):
546
+ event_handler = self._parse_on_event(event_el)
547
+ component.add_event_handler(event_handler)
548
+
549
+ # Parse q:script elements
550
+ for script_el in self._find_all_elements(root, 'script'):
551
+ script_content = script_el.text or ""
552
+ component.add_script(script_content.strip())
553
+
554
+ # Parse control flow statements (if, loop, set, dispatchEvent, etc)
555
+ self._parse_control_flow_statements(root, component)
556
+
557
+ return component
558
+
559
+ def _parse_control_flow_statements(self, parent: ET.Element, component: ComponentNode):
560
+ """Parse control flow statements - delegates to modular parser registry"""
561
+ for child in parent:
562
+ child_type = self._get_element_name(child)
563
+
564
+ # Special case: inline job definition (needs path parameter)
565
+ if child_type == 'job':
566
+ from pathlib import Path as PathLib
567
+ job_node = self._parse_job(child, PathLib("<inline>"))
568
+ component.add_statement(job_node)
569
+ continue
570
+
571
+ # `<q:else>` / `<q:elseif>` IRMAO de um `<q:if>` no corpo do
572
+ # componente: anexa ao if anterior. A forma irma nao tinha parser
573
+ # e sumia calada — a documentacao a ensinava e arquivos entregues
574
+ # a usavam (ver base.parse_statements para o porque completo).
575
+ if child_type in ('else', 'elseif'):
576
+ anterior = component.statements[-1] if component.statements else None
577
+ if isinstance(anterior, IfNode):
578
+ self._attach_else_sibling(anterior, child_type, child)
579
+ continue
580
+ raise QuantumParseError(
581
+ f"<q:{child_type}> has no matching <q:if> before it. "
582
+ f"Put it inside the if, or right after it: "
583
+ f"<q:if ...>...</q:if> <q:{child_type}>...</q:{child_type}>"
584
+ )
585
+
586
+ # Delegate to modular parser registry via _parse_statement
587
+ node = self._parse_statement(child)
588
+ if node is not None:
589
+ component.add_statement(node)
590
+ # Check if this produces HTML
591
+ if self._is_html_element(child) or (child_type and child_type[0].isupper()):
592
+ component.has_html = True
593
+
594
+ def _attach_else_sibling(self, if_node: 'IfNode', kind: str, element: ET.Element):
595
+ """Anexa um else/elseif IRMAO ao IfNode anterior — mesmo resultado da
596
+ forma aninhada que o IfParser ja monta."""
597
+ if kind == 'elseif':
598
+ condicao = element.get('condition', '')
599
+ corpo = []
600
+ for filho in element:
601
+ stmt = self._parse_statement(filho)
602
+ if stmt:
603
+ corpo.append(stmt)
604
+ if_node.add_elseif_block(condicao, corpo)
605
+ else:
606
+ for filho in element:
607
+ stmt = self._parse_statement(filho)
608
+ if stmt:
609
+ if_node.add_else_statement(stmt)
610
+
611
+ def _parse_statement(self, element: ET.Element) -> Optional[QuantumNode]:
612
+ """Parse individual statement (return, set, dispatchEvent, etc)"""
613
+ element_type = self._get_element_name(element)
614
+
615
+ # === MODULAR PARSER REGISTRY (no fallback) ===
616
+ # PHASE 1 DIAGNOSTIC: Legacy fallback disabled to test modular coverage
617
+ if self._use_modular_parsers and self._parser_registry:
618
+ if self._parser_registry.has_parser(element_type):
619
+ return self._parser_registry.parse(element, element_type)
620
+
621
+ # Component calls (uppercase naming convention)
622
+ if element_type and element_type[0].isupper():
623
+ from quantum.core.parsers import ComponentCallParser
624
+ if ComponentCallParser.is_component_call(element_type):
625
+ return self._parser_registry.component_call_parser.parse(element)
626
+
627
+ # HTML elements
628
+ # <q:return> aninhado. Nao ha parser registrado para 'return' —
629
+ # o nivel de componente o coleta a parte, com _find_all_elements —
630
+ # entao aqui ele caia no `return None` do fim e era DESCARTADO em
631
+ # silencio. `<q:if ...><q:return value="X"/></q:if>` nao retornava
632
+ # X: o componente seguia e devolvia o return seguinte, ou nada.
633
+ # O mesmo valia dentro de q:loop e q:else.
634
+ if element_type == 'return':
635
+ return self._parse_return(element)
636
+
637
+ if self._is_html_element(element):
638
+ if self._parser_registry.html_parser.can_parse(element_type):
639
+ return self._parser_registry.html_parser.parse(element)
640
+
641
+ # No parser found - this is unexpected
642
+ return None
643
+
644
+ # If modular parsers are disabled, raise an error
645
+ raise QuantumParseError("Modular parsers are disabled but legacy fallback has been removed")
646
+
647
+ def _parse_application(self, root: ET.Element, path: Path) -> ApplicationNode:
648
+ """Parse q:application"""
649
+ app_id = root.get('id', path.stem)
650
+ app_type = root.get('type', 'html')
651
+
652
+ app = ApplicationNode(app_id, app_type)
653
+ app.engine = root.get('engine')
654
+
655
+ # Parse theme attribute for UI applications
656
+ theme_attr = root.get('theme')
657
+ if theme_attr and app_type == 'ui':
658
+ app.ui_theme_preset = theme_attr
659
+ # Create a UIThemeNode from the attribute
660
+ theme_node = UIThemeNode()
661
+ theme_node.preset = theme_attr
662
+ app.ui_theme = theme_node
663
+
664
+ # Parse q:route elements
665
+ for route_el in self._find_all_elements(root, 'route'):
666
+ route = self._parse_route(route_el)
667
+ app.add_route(route)
668
+
669
+ # q:set e q:function no topo do <q:application>, para qualquer
670
+ # engine. Os parsers por engine so olham os filhos do proprio
671
+ # namespace (qt:, qg:, ui:), entao estes eram silenciosamente
672
+ # descartados — inclusive a funcao para a qual um on-click apontava.
673
+ self._parse_application_quantum_children(root, app)
674
+
675
+ # Game Engine 2D: parse game children when type="game"
676
+ if app_type == 'game':
677
+ self._parse_game_application_children(root, app)
678
+
679
+ # Terminal Engine: parse terminal children when type="terminal"
680
+ if app_type == 'terminal':
681
+ self._parse_terminal_application_children(root, app)
682
+
683
+ # Testing Engine: parse testing children when type="testing"
684
+ if app_type == 'testing':
685
+ self._parse_testing_application_children(root, app)
686
+
687
+ # UI Engine: parse ui children when type="ui"
688
+ if app_type == 'ui':
689
+ self._parse_ui_application_children(root, app)
690
+
691
+ return app
692
+
693
+ def _parse_application_quantum_children(self, root: ET.Element,
694
+ app: ApplicationNode):
695
+ """Coleta <q:function> e <q:set> escritos direto no q:application."""
696
+ for child in root:
697
+ local_name = self._get_element_name(child)
698
+ if local_name not in ('function', 'set'):
699
+ continue
700
+ tag = child.tag
701
+ # So o namespace q:. Um <ui:set> ou <qg:set> pertence ao seu
702
+ # proprio parser, e um elemento sem prefixo nao e tag do
703
+ # framework.
704
+ if not (tag.startswith('q:')
705
+ or tag.startswith('{https://quantum.lang/ns}')):
706
+ continue
707
+ try:
708
+ node = self._parse_statement(child)
709
+ except QuantumParseError:
710
+ raise
711
+ except Exception:
712
+ continue
713
+ if node is None:
714
+ continue
715
+ if isinstance(node, FunctionNode):
716
+ app.functions.append(node)
717
+ elif isinstance(node, SetNode):
718
+ app.state_vars.append(node)
719
+
720
+ def _parse_game_application_children(self, root: ET.Element, app: ApplicationNode):
721
+ """Parse children of a game application (qg: elements at top level)."""
722
+ game_parser = GameParser(self)
723
+ for child in root:
724
+ local_name = self._get_element_name(child)
725
+ ns = self._get_element_game_namespace(child)
726
+
727
+ if ns == 'game':
728
+ # Handle scene-include specially (resolved at parse time)
729
+ if local_name == 'scene-include':
730
+ self._parse_scene_include(child, app, game_parser)
731
+ continue
732
+
733
+ node = game_parser.parse_game_element(local_name, child)
734
+ if isinstance(node, SceneNode):
735
+ app.scenes.append(node)
736
+ elif isinstance(node, BehaviorNode):
737
+ app.behaviors.append(node)
738
+ elif isinstance(node, PrefabNode):
739
+ app.prefabs.append(node)
740
+ elif isinstance(node, EnemyNode):
741
+ app.enemies.append(node)
742
+ elif isinstance(node, PersistentNode):
743
+ app.persistent.append(node)
744
+
745
+ def _parse_scene_include(self, element: ET.Element, app: ApplicationNode, game_parser):
746
+ """Parse <qg:scene-include src="..."> - Include a scene from external .q file.
747
+
748
+ The external file should contain a <qg:scene> element (with or without
749
+ a <q:application> wrapper). The scene is added to the application's scene list.
750
+ """
751
+ src = element.get('src', '')
752
+ if not src:
753
+ raise QuantumParseError("scene-include requires a 'src' attribute")
754
+
755
+ # Resolve relative path from the source file being parsed
756
+ base_path = getattr(self, '_source_path', None)
757
+ if base_path:
758
+ include_path = Path(base_path).parent / src
759
+ else:
760
+ include_path = Path(src)
761
+
762
+ if not include_path.exists():
763
+ raise QuantumParseError(f"scene-include: file not found: {include_path}")
764
+
765
+ content = include_path.read_text(encoding='utf-8')
766
+
767
+ # O mesmo HTML tolerado em qualquer outro .q. Este caminho le um
768
+ # arquivo .q e ia direto para o ET.fromstring la embaixo, sem passar
769
+ # por aqui: um `<br>`, um `&` cru, um atributo booleano ou um `<`
770
+ # dentro de um valor davam "scene-include: parse error" num arquivo
771
+ # que o parser normal aceita sem reclamar.
772
+ content = normalise_html(content)
773
+
774
+ # Strip XML processing instruction if present
775
+ import re
776
+ content = re.sub(r'<\?xml[^?]*\?>\s*', '', content)
777
+ # Strip XML comments at the top level
778
+ content = re.sub(r'<!--[\s\S]*?-->\s*', '', content, count=1)
779
+ content = content.strip()
780
+
781
+ # Wrap in a root element with namespace declarations
782
+ # This handles bare <qg:scene> files that don't have <q:application>
783
+ if 'xmlns:qg' not in content:
784
+ ns_attrs = 'xmlns:qg="https://quantum.lang/game"'
785
+ if 'q:' in content:
786
+ ns_attrs += ' xmlns:q="https://quantum.lang/ns"'
787
+ content = f'<_root {ns_attrs}>{content}</_root>'
788
+
789
+ try:
790
+ include_root = ET.fromstring(content)
791
+ except ET.ParseError as e:
792
+ raise QuantumParseError(f"scene-include: parse error in {src}: {e}")
793
+
794
+ # Find scene element(s) in the included file
795
+ self._extract_scenes_from_include(include_root, app, game_parser)
796
+
797
+ def _extract_scenes_from_include(self, root: ET.Element, app: ApplicationNode, game_parser):
798
+ """Extract SceneNode(s) from an included file's parsed XML."""
799
+ tag = root.tag
800
+ # Strip namespace
801
+ if '}' in tag:
802
+ local = tag.split('}')[-1]
803
+ elif ':' in tag:
804
+ local = tag.split(':')[-1]
805
+ else:
806
+ local = tag
807
+
808
+ if local == 'scene':
809
+ # Root element is the scene itself
810
+ node = game_parser.parse_game_element('scene', root)
811
+ if isinstance(node, SceneNode):
812
+ app.scenes.append(node)
813
+ else:
814
+ # Root is a wrapper — look for scene children
815
+ for child in root:
816
+ child_ns = self._get_element_game_namespace(child)
817
+ child_local = self._get_element_name(child)
818
+ if child_ns == 'game' and child_local == 'scene':
819
+ node = game_parser.parse_game_element('scene', child)
820
+ if isinstance(node, SceneNode):
821
+ app.scenes.append(node)
822
+
823
+ def _get_element_game_namespace(self, element: ET.Element) -> str:
824
+ """Detect if element belongs to qg: (game) or q: (quantum) namespace."""
825
+ tag = element.tag
826
+ if '{https://quantum.lang/game}' in tag:
827
+ return 'game'
828
+ if '{https://quantum.lang/ns}' in tag:
829
+ return 'quantum'
830
+ if tag.startswith('qg:'):
831
+ return 'game'
832
+ if tag.startswith('q:'):
833
+ return 'quantum'
834
+ return 'html'
835
+
836
+ def _parse_terminal_application_children(self, root: ET.Element, app: ApplicationNode):
837
+ """Parse children of a terminal application (qt: elements at top level)."""
838
+ terminal_parser = TerminalParser(self)
839
+ for child in root:
840
+ local_name = self._get_element_name(child)
841
+ ns = self._get_element_terminal_namespace(child)
842
+
843
+ if ns == 'terminal':
844
+ node = terminal_parser.parse_terminal_element(local_name, child)
845
+ if isinstance(node, TerminalScreenNode):
846
+ app.screens.append(node)
847
+ elif isinstance(node, TerminalKeybindingNode):
848
+ app.keybindings.append(node)
849
+ elif isinstance(node, TerminalServiceNode):
850
+ app.services.append(node)
851
+ elif isinstance(node, TerminalCssNode):
852
+ app.terminal_css += node.content + '\n'
853
+
854
+ def _get_element_terminal_namespace(self, element: ET.Element) -> str:
855
+ """Detect if element belongs to qt: (terminal) or q: (quantum) namespace."""
856
+ tag = element.tag
857
+ if '{https://quantum.lang/terminal}' in tag:
858
+ return 'terminal'
859
+ if '{https://quantum.lang/ns}' in tag:
860
+ return 'quantum'
861
+ if tag.startswith('qt:'):
862
+ return 'terminal'
863
+ if tag.startswith('q:'):
864
+ return 'quantum'
865
+ return 'html'
866
+
867
+ def _parse_testing_application_children(self, root: ET.Element, app: ApplicationNode):
868
+ """Parse children of a testing application (qtest: elements at top level)."""
869
+ testing_parser = TestingParser(self)
870
+ for child in root:
871
+ local_name = self._get_element_name(child)
872
+ ns = self._get_element_testing_namespace(child)
873
+
874
+ if ns == 'testing':
875
+ node = testing_parser.parse_testing_element(local_name, child)
876
+ if isinstance(node, TestingTestSuiteNode):
877
+ app.test_suites.append(node)
878
+ elif isinstance(node, TestingBrowserConfigNode):
879
+ app.test_config = node
880
+ elif isinstance(node, TestingFixtureNode):
881
+ app.test_fixtures.append(node)
882
+ elif isinstance(node, TestingMockNode):
883
+ app.test_mocks.append(node)
884
+ elif isinstance(node, TestingAuthNode):
885
+ app.test_auth_states.append(node)
886
+
887
+ def _get_element_testing_namespace(self, element: ET.Element) -> str:
888
+ """Detect if element belongs to qtest: (testing) or q: (quantum) namespace."""
889
+ tag = element.tag
890
+ if '{https://quantum.lang/testing}' in tag:
891
+ return 'testing'
892
+ if '{https://quantum.lang/ns}' in tag:
893
+ return 'quantum'
894
+ if tag.startswith('qtest:'):
895
+ return 'testing'
896
+ if tag.startswith('q:'):
897
+ return 'quantum'
898
+ return 'html'
899
+
900
+ def _parse_ui_application_children(self, root: ET.Element, app: ApplicationNode):
901
+ """Parse children of a UI application (ui: elements at top level)."""
902
+ ui_parser = UIParser(self)
903
+ for child in root:
904
+ local_name = self._get_element_name(child)
905
+ ns = self._get_element_ui_namespace(child)
906
+
907
+ if ns == 'ui':
908
+ node = ui_parser.parse_ui_element(local_name, child)
909
+ if isinstance(node, UIWindowNode):
910
+ app.ui_windows.append(node)
911
+ elif isinstance(node, UIThemeNode):
912
+ # ui:theme overrides any theme attribute
913
+ app.ui_theme = node
914
+ else:
915
+ app.ui_children.append(node)
916
+ elif ns == 'quantum':
917
+ # Allow q: tags at the top level of a UI app (e.g. q:set)
918
+ statement = self._parse_statement(child)
919
+ if statement:
920
+ app.ui_children.append(statement)
921
+
922
+ def _get_element_ui_namespace(self, element: ET.Element) -> str:
923
+ """Detect if element belongs to ui: (UI engine) or q: (quantum) namespace."""
924
+ tag = element.tag
925
+ if '{https://quantum.lang/ui}' in tag:
926
+ return 'ui'
927
+ if '{https://quantum.lang/ns}' in tag:
928
+ return 'quantum'
929
+ if tag.startswith('ui:'):
930
+ return 'ui'
931
+ if tag.startswith('q:'):
932
+ return 'quantum'
933
+ return 'html'
934
+
935
+ def _parse_job(self, root: ET.Element, path: Path) -> JobNode:
936
+ """
937
+ Parse q:job - Job queue for batch processing.
938
+
939
+ Examples:
940
+ <q:job name="sendEmail" queue="emails" priority="5">
941
+ <q:param name="to" type="string" />
942
+ <q:mail to="{to}" subject="Notification">...</q:mail>
943
+ </q:job>
944
+
945
+ <q:job name="processOrder" action="dispatch" delay="5m" />
946
+ """
947
+ # Support both 'name' and legacy 'id' attribute
948
+ name = root.get('name') or root.get('id', path.stem)
949
+ queue = root.get('queue', 'default')
950
+ action = root.get('action', 'define')
951
+
952
+ job_node = JobNode(name, queue, action)
953
+
954
+ # Parse optional attributes
955
+ job_node.delay = root.get('delay')
956
+ job_node.timeout = root.get('timeout')
957
+ job_node.backoff = root.get('backoff', '30s')
958
+
959
+ priority_attr = root.get('priority')
960
+ if priority_attr:
961
+ try:
962
+ job_node.priority = int(priority_attr)
963
+ except ValueError:
964
+ pass
965
+
966
+ attempts_attr = root.get('attempts')
967
+ if attempts_attr:
968
+ try:
969
+ job_node.attempts = int(attempts_attr)
970
+ except ValueError:
971
+ pass
972
+
973
+ # Legacy support
974
+ job_node.schedule = root.get('schedule')
975
+
976
+ # Parse children (params and body statements)
977
+ for child in root:
978
+ child_type = self._get_element_name(child)
979
+
980
+ if child_type == 'param':
981
+ param = self._parse_param(child)
982
+ job_node.add_param(param)
983
+ else:
984
+ # Parse body statements
985
+ statement = self._parse_statement(child)
986
+ if statement:
987
+ job_node.add_statement(statement)
988
+
989
+ return job_node
990
+
991
+ def _parse_param(self, element: ET.Element) -> QuantumParam:
992
+ """Parse q:param"""
993
+ param = QuantumParam(
994
+ name=element.get('name', ''),
995
+ type=element.get('type', 'string'),
996
+ required=element.get('required', 'false').lower() == 'true',
997
+ default=element.get('default'),
998
+ validation=element.get('validation'),
999
+ description=element.get('description')
1000
+ )
1001
+
1002
+ # REST-specific
1003
+ param.source = element.get('source', 'auto')
1004
+
1005
+ # value= is how a CALL SITE passes an argument — <q:job> dispatch,
1006
+ # <q:invoke>, <q:query>. This function read only default=, so every
1007
+ # <q:param name="to" value="a@b.c" /> inside a q:job arrived with no
1008
+ # value at all and each dispatched job got nulls. Declaration sites
1009
+ # (q:function, q:action) use default= and are unaffected.
1010
+ param.value = element.get('value')
1011
+
1012
+ # Validation
1013
+ param.validate_rule = element.get('validate')
1014
+ param.pattern = element.get('pattern')
1015
+ param.min = element.get('min')
1016
+ param.max = element.get('max')
1017
+
1018
+ minlength_str = element.get('minlength')
1019
+ if minlength_str:
1020
+ try:
1021
+ param.minlength = int(minlength_str)
1022
+ except ValueError:
1023
+ pass
1024
+
1025
+ maxlength_str = element.get('maxlength')
1026
+ if maxlength_str:
1027
+ try:
1028
+ param.maxlength = int(maxlength_str)
1029
+ except ValueError:
1030
+ pass
1031
+
1032
+ param.range = element.get('range')
1033
+ param.enum = element.get('enum')
1034
+
1035
+ # File upload
1036
+ param.maxsize = element.get('maxsize')
1037
+ param.accept = element.get('accept')
1038
+
1039
+ return param
1040
+
1041
+ def _parse_return(self, element: ET.Element) -> QuantumReturn:
1042
+ """Parse q:return"""
1043
+ return QuantumReturn(
1044
+ name=element.get('name'),
1045
+ type=element.get('type', 'string'),
1046
+ value=element.get('value', ''),
1047
+ description=element.get('description')
1048
+ )
1049
+
1050
+ def _parse_route(self, element: ET.Element) -> QuantumRoute:
1051
+ """Parse q:route"""
1052
+ path = element.get('path', '/')
1053
+ method = element.get('method', 'GET').upper()
1054
+
1055
+ route = QuantumRoute(path, method)
1056
+
1057
+ # Parse q:return inside the route
1058
+ for return_el in self._find_all_elements(element, 'return'):
1059
+ return_node = self._parse_return(return_el)
1060
+ route.returns.append(return_node)
1061
+
1062
+ return route
1063
+
1064
+ def _parse_function(self, func_element: ET.Element) -> FunctionNode:
1065
+ """Parse q:function statement"""
1066
+ name = func_element.get('name')
1067
+
1068
+ if not name:
1069
+ raise QuantumParseError("Function requires 'name' attribute")
1070
+
1071
+ func_node = FunctionNode(name)
1072
+
1073
+ # Core attributes
1074
+ func_node.return_type = func_element.get('returnType', 'any')
1075
+ func_node.scope = func_element.get('scope', 'component')
1076
+ func_node.access = func_element.get('access', 'public')
1077
+ func_node.description = func_element.get('description')
1078
+ func_node.hint = func_element.get('hint')
1079
+
1080
+ # Validation
1081
+ func_node.validate_params = func_element.get('validate', 'false').lower() == 'true'
1082
+
1083
+ # Performance
1084
+ cache_attr = func_element.get('cache')
1085
+ if cache_attr:
1086
+ if cache_attr.lower() == 'true':
1087
+ func_node.cache = True
1088
+ elif cache_attr.endswith('s'): # "60s"
1089
+ func_node.cache = True
1090
+ try:
1091
+ func_node.cache_ttl = int(cache_attr[:-1])
1092
+ except ValueError:
1093
+ pass
1094
+
1095
+ func_node.memoize = func_element.get('memoize', 'false').lower() == 'true'
1096
+ func_node.pure = func_element.get('pure', 'false').lower() == 'true'
1097
+
1098
+ # Behavior
1099
+ func_node.async_func = func_element.get('async', 'false').lower() == 'true'
1100
+
1101
+ retry_attr = func_element.get('retry')
1102
+ if retry_attr:
1103
+ try:
1104
+ func_node.retry = int(retry_attr)
1105
+ except ValueError:
1106
+ pass
1107
+
1108
+ timeout_attr = func_element.get('timeout')
1109
+ if timeout_attr:
1110
+ func_node.timeout = timeout_attr
1111
+
1112
+ # REST API (optional)
1113
+ endpoint = func_element.get('endpoint')
1114
+ if endpoint:
1115
+ method = func_element.get('method', 'GET')
1116
+ func_node.enable_rest(endpoint, method)
1117
+
1118
+ # REST-specific attributes
1119
+ if func_element.get('produces'):
1120
+ func_node.rest_config.produces = func_element.get('produces')
1121
+
1122
+ if func_element.get('consumes'):
1123
+ func_node.rest_config.consumes = func_element.get('consumes')
1124
+
1125
+ if func_element.get('auth'):
1126
+ func_node.rest_config.auth = func_element.get('auth')
1127
+
1128
+ if func_element.get('roles'):
1129
+ roles_str = func_element.get('roles')
1130
+ func_node.rest_config.roles = [r.strip() for r in roles_str.split(',')]
1131
+
1132
+ if func_element.get('rateLimit'):
1133
+ func_node.rest_config.rate_limit = func_element.get('rateLimit')
1134
+
1135
+ cors_attr = func_element.get('cors')
1136
+ if cors_attr and cors_attr.lower() == 'true':
1137
+ func_node.rest_config.cors = True
1138
+
1139
+ status_attr = func_element.get('status')
1140
+ if status_attr:
1141
+ try:
1142
+ func_node.rest_config.status = int(status_attr)
1143
+ except ValueError:
1144
+ pass
1145
+
1146
+ # Parse function params and body
1147
+ for child in func_element:
1148
+ child_type = self._get_element_name(child)
1149
+
1150
+ if child_type == 'param':
1151
+ param = self._parse_param(child)
1152
+ func_node.add_param(param)
1153
+ elif child_type == 'return':
1154
+ # ReturnParser is intentionally not in the tag registry, so
1155
+ # _parse_statement() returns None for <q:return> — which
1156
+ # silently dropped the return from every function body,
1157
+ # making every q:function return nothing.
1158
+ from quantum.core.parsers.functions.return_parser import ReturnParser
1159
+ func_node.add_statement(ReturnParser(self).parse(child))
1160
+ else:
1161
+ # Parse body statements
1162
+ statement = self._parse_statement(child)
1163
+ if statement:
1164
+ func_node.add_statement(statement)
1165
+
1166
+ return func_node
1167
+
1168
+ def _parse_dispatch_event(self, event_element: ET.Element) -> DispatchEventNode:
1169
+ """Parse q:dispatchEvent statement"""
1170
+ event = event_element.get('event')
1171
+
1172
+ if not event:
1173
+ raise QuantumParseError("dispatchEvent requires 'event' attribute")
1174
+
1175
+ dispatch_node = DispatchEventNode(event)
1176
+
1177
+ # Optional attributes
1178
+ dispatch_node.data = event_element.get('data')
1179
+ dispatch_node.queue = event_element.get('queue')
1180
+ dispatch_node.exchange = event_element.get('exchange')
1181
+ dispatch_node.routing_key = event_element.get('routingKey')
1182
+ dispatch_node.priority = event_element.get('priority', 'normal')
1183
+ dispatch_node.delay = event_element.get('delay')
1184
+ dispatch_node.ttl = event_element.get('ttl')
1185
+ dispatch_node.metadata = event_element.get('metadata')
1186
+
1187
+ return dispatch_node
1188
+
1189
+ def _parse_on_event(self, event_element: ET.Element) -> OnEventNode:
1190
+ """Parse q:onEvent handler"""
1191
+ event = event_element.get('event')
1192
+
1193
+ if not event:
1194
+ raise QuantumParseError("onEvent requires 'event' attribute")
1195
+
1196
+ event_node = OnEventNode(event)
1197
+
1198
+ # Optional attributes
1199
+ event_node.queue = event_element.get('queue')
1200
+
1201
+ max_retries_attr = event_element.get('maxRetries')
1202
+ if max_retries_attr:
1203
+ try:
1204
+ event_node.max_retries = int(max_retries_attr)
1205
+ except ValueError:
1206
+ pass
1207
+
1208
+ event_node.retry_delay = event_element.get('retryDelay')
1209
+ event_node.dead_letter = event_element.get('deadLetter')
1210
+ event_node.filter = event_element.get('filter')
1211
+
1212
+ concurrent_attr = event_element.get('concurrent')
1213
+ if concurrent_attr:
1214
+ try:
1215
+ event_node.concurrent = int(concurrent_attr)
1216
+ except ValueError:
1217
+ pass
1218
+
1219
+ prefetch_attr = event_element.get('prefetch')
1220
+ if prefetch_attr:
1221
+ try:
1222
+ event_node.prefetch = int(prefetch_attr)
1223
+ except ValueError:
1224
+ pass
1225
+
1226
+ event_node.timeout = event_element.get('timeout')
1227
+
1228
+ # Parse event handler body
1229
+ for child in event_element:
1230
+ statement = self._parse_statement(child)
1231
+ if statement:
1232
+ event_node.add_statement(statement)
1233
+
1234
+ return event_node
1235
+