quantum-framework 0.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
quantum/core/parser.py ADDED
@@ -0,0 +1,1551 @@
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.xml_lines import fromstring_with_lines, line_of
15
+ from quantum.core.ast_nodes import (
16
+ QuantumNode, ComponentNode, ApplicationNode, JobNode,
17
+ QuantumParam, QuantumReturn, QuantumRoute,
18
+ DispatchEventNode, OnEventNode, QueryNode, QueryParamNode,
19
+ InvokeHeaderNode, DataNode, ColumnNode, FieldNode, TransformNode,
20
+ FilterNode, SortNode, LimitNode, ComputeNode, HeaderNode,
21
+ HTMLNode, TextNode, DocTypeNode, CommentNode, HTML_VOID_ELEMENTS,
22
+ ImportNode, SlotNode, ComponentCallNode,
23
+ ActionNode, RedirectNode, FlashNode, FileNode, MailNode, TransactionNode,
24
+ LLMNode, LLMMessageNode,
25
+ ScheduleNode, ThreadNode, # Job Execution System
26
+ # Message Queue System
27
+ MessageNode, MessageHeaderNode, SubscribeNode, QueueNode,
28
+ MessageAckNode, MessageNackNode,
29
+ # Python Scripting System
30
+ PythonNode, PyImportNode, PyClassNode, PyDecoratorNode, PyExprNode
31
+ )
32
+ # Import feature AST nodes from their respective modules
33
+ from quantum.core.features.conditionals.src.ast_node import IfNode
34
+ from quantum.core.features.loops.src.ast_node import LoopNode
35
+ from quantum.core.features.state_management.src.ast_node import SetNode
36
+ from quantum.core.docs_links import HOW_A_PAGE_RUNS
37
+ from quantum.core.features.functions.src.ast_node import FunctionNode, RestConfig
38
+ from quantum.core.features.invocation.src.ast_node import InvokeNode
39
+ from quantum.core.features.logging.src import LogNode, parse_log
40
+ from quantum.core.features.dump.src import DumpNode, parse_dump
41
+ from quantum.core.features.knowledge_base.src import KnowledgeNode, KnowledgeSourceNode, parse_knowledge
42
+ from quantum.core.features.agents.src import (
43
+ AgentNode, AgentInstructionNode, AgentToolNode, AgentToolParamNode, AgentExecuteNode,
44
+ AgentTeamNode, AgentHandoffNode
45
+ )
46
+ from quantum.core.features.websocket.src import (
47
+ WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
48
+ )
49
+ from quantum.core.features.game_engine_2d.src.parser import GameParser, GameParseError
50
+ from quantum.core.features.game_engine_2d.src.ast_nodes import (
51
+ SceneNode, BehaviorNode, PrefabNode, PersistentNode, EnemyNode,
52
+ )
53
+ from quantum.core.features.terminal_engine.src.parser import TerminalParser, TerminalParseError
54
+ from quantum.core.features.terminal_engine.src.ast_nodes import (
55
+ ScreenNode as TerminalScreenNode, KeybindingNode as TerminalKeybindingNode,
56
+ ServiceNode as TerminalServiceNode, CssNode as TerminalCssNode,
57
+ )
58
+ from quantum.core.features.testing_engine.src.parser import TestingParser, TestingParseError
59
+ from quantum.core.features.testing_engine.src.ast_nodes import (
60
+ QTestSuiteNode as TestingTestSuiteNode,
61
+ BrowserConfigNode as TestingBrowserConfigNode,
62
+ FixtureNode_Testing as TestingFixtureNode,
63
+ MockNode_Testing as TestingMockNode,
64
+ AuthNode as TestingAuthNode,
65
+ )
66
+ from quantum.core.features.ui_engine.src.parser import UIParser, UIParseError
67
+ from quantum.core.features.ui_engine.src.ast_nodes import is_ui_node
68
+ from quantum.core.features.ui_engine.src.ast_nodes import UIWindowNode
69
+ from quantum.core.features.theming.src import UIThemeNode
70
+ from quantum.core.parser_registry import ParserRegistry
71
+ import logging
72
+
73
+ logger = logging.getLogger(__name__)
74
+
75
+
76
+ def _create_parser_registry(parser: 'QuantumParser') -> ParserRegistry:
77
+ """
78
+ Create and populate the parser registry with all modular parsers.
79
+
80
+ This factory function creates all modular parsers and registers them.
81
+ The registry can then dispatch parsing to the appropriate parser
82
+ based on tag name.
83
+
84
+ Args:
85
+ parser: QuantumParser instance for parser initialization
86
+
87
+ Returns:
88
+ Populated ParserRegistry
89
+ """
90
+ from quantum.core.parsers import (
91
+ # Control flow
92
+ IfParser, LoopParser, SetParser,
93
+ # Data
94
+ QueryParser, InvokeParser, DataParser, TransactionParser,
95
+ # Services
96
+ LogParser, DumpParser, FileParser, MailParser,
97
+ # AI
98
+ LLMParser, AgentParser, TeamParser, KnowledgeParser,
99
+ # Messaging
100
+ WebSocketParser, WebSocketSendParser, WebSocketCloseParser,
101
+ MessageParser, SubscribeParser, QueueParser,
102
+ # Jobs
103
+ ScheduleParser, ThreadParser, JobParser,
104
+ # Scripting
105
+ PythonParser, PyImportParser, PyClassParser,
106
+ # HTML
107
+ HTMLParser, ComponentCallParser,
108
+ )
109
+ # New modular parsers (Phase 2 - Operacao Guilhotina)
110
+ from quantum.core.parsers.forms import ActionParser, RedirectParser, FlashParser
111
+ from quantum.core.parsers.composition import ImportParser, SlotParser
112
+ from quantum.core.parsers.functions import FunctionParser, ReturnParser, ParamParser
113
+ from quantum.core.parsers.events import DispatchEventParser
114
+ from quantum.core.parsers.routing import RouteParser
115
+ from quantum.core.parsers.messaging import MessageAckParser, MessageNackParser
116
+ from quantum.core.parsers.scripting import PyDecoratorParser
117
+
118
+ registry = ParserRegistry()
119
+
120
+ # Register all tag-based parsers
121
+ parsers = [
122
+ # Control flow
123
+ IfParser(parser),
124
+ LoopParser(parser),
125
+ SetParser(parser),
126
+ # Data
127
+ QueryParser(parser),
128
+ InvokeParser(parser),
129
+ DataParser(parser),
130
+ TransactionParser(parser),
131
+ # Services
132
+ LogParser(parser),
133
+ DumpParser(parser),
134
+ FileParser(parser),
135
+ MailParser(parser),
136
+ # AI
137
+ LLMParser(parser),
138
+ AgentParser(parser),
139
+ TeamParser(parser),
140
+ KnowledgeParser(parser),
141
+ # Messaging
142
+ WebSocketParser(parser),
143
+ WebSocketSendParser(parser),
144
+ WebSocketCloseParser(parser),
145
+ MessageParser(parser),
146
+ SubscribeParser(parser),
147
+ QueueParser(parser),
148
+ MessageAckParser(parser),
149
+ MessageNackParser(parser),
150
+ # Jobs
151
+ ScheduleParser(parser),
152
+ ThreadParser(parser),
153
+ JobParser(parser),
154
+ # Scripting
155
+ PythonParser(parser),
156
+ PyImportParser(parser),
157
+ PyClassParser(parser),
158
+ PyDecoratorParser(parser),
159
+ # HTML (registers for common HTML tags)
160
+ HTMLParser(parser),
161
+ # Forms & Actions
162
+ ActionParser(parser),
163
+ RedirectParser(parser),
164
+ FlashParser(parser),
165
+ # Component Composition
166
+ ImportParser(parser),
167
+ SlotParser(parser),
168
+ # Functions
169
+ FunctionParser(parser),
170
+ # Note: ReturnParser, ParamParser, RouteParser are not registered here
171
+ # They are internal parsers used by FunctionParser and other parsers
172
+ # Events
173
+ DispatchEventParser(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
+ from quantum.core.parsers.base import ParserError # noqa: E402
189
+
190
+
191
+ class QuantumParseError(Exception):
192
+ """Quantum parsing error"""
193
+ pass
194
+
195
+
196
+ def _explain_xml_error(error, source: str) -> str:
197
+ """Turn a raw ElementTree error into something actionable.
198
+
199
+ A .q file is XML, so a bare `<` or `&` inside SQL or JavaScript breaks
200
+ parsing with a message ("not well-formed (invalid token)") that says
201
+ nothing about the actual cause. This has bitten the same way at least
202
+ twice (a JS `for` loop in q:llm, a SQL `<` in q:schedule), so name it.
203
+ """
204
+ message = f"XML parse error: {error}"
205
+
206
+ line_no = getattr(error, 'position', (None, None))[0]
207
+ offending = ''
208
+ if line_no:
209
+ lines = source.splitlines()
210
+ if 0 < line_no <= len(lines):
211
+ offending = lines[line_no - 1].strip()
212
+
213
+ if offending:
214
+ message += f"\n at line {line_no}: {offending}"
215
+
216
+ if offending and ('<' in offending or '&' in offending):
217
+ message += (
218
+ "\n Hint: a .q file is XML, so a bare '<' or '&' is invalid even "
219
+ "inside SQL or JavaScript.\n"
220
+ " Write '&lt;' / '&amp;', or wrap the body in <![CDATA[ ... ]]>."
221
+ )
222
+
223
+ return message
224
+
225
+
226
+ def _at(erro: 'QuantumParseError', onde) -> 'QuantumParseError':
227
+ """The error belongs to this node's (or element's) line."""
228
+ erro.line = line_of(onde)
229
+ return erro
230
+
231
+
232
+ def _with_line(erro: 'QuantumParseError', source: str, line: Optional[int] = None) -> 'QuantumParseError':
233
+ """DEV-2: a parse error says where it is — `at line N: <the line>`."""
234
+ line = line or getattr(erro, 'line', None)
235
+ erro.line = line
236
+ if line and ' at line ' not in str(erro):
237
+ linhas = source.splitlines()
238
+ trecho = linhas[line - 1].strip() if 0 < line <= len(linhas) else ''
239
+ erro.args = (f"{erro}\n at line {line}: {trecho}" if trecho else f"{erro} (line {line})",)
240
+ return erro
241
+
242
+
243
+ class QuantumParser:
244
+ """Main parser for .q files"""
245
+
246
+ def __init__(self, use_cache: bool = True, use_modular_parsers: bool = True):
247
+ self.quantum_ns = {'q': 'https://quantum.lang/ns'}
248
+ self._use_cache = use_cache
249
+ self._source_path: Optional[str] = None # Path of the file being parsed
250
+ self._ast_cache = None
251
+ if use_cache:
252
+ try:
253
+ from quantum.runtime.ast_cache import get_ast_cache
254
+ self._ast_cache = get_ast_cache()
255
+ except ImportError:
256
+ self._use_cache = False
257
+
258
+ # === NEW: Parser Registry for modular dispatch ===
259
+ self._use_modular_parsers = use_modular_parsers
260
+ self._parser_registry: ParserRegistry = None
261
+ if use_modular_parsers:
262
+ self._parser_registry = _create_parser_registry(self)
263
+ else:
264
+ import warnings
265
+ warnings.warn(
266
+ "use_modular_parsers=False is deprecated and will be removed in v2.0. "
267
+ "The modular ParserRegistry is now the default.",
268
+ DeprecationWarning,
269
+ stacklevel=2
270
+ )
271
+
272
+ @property
273
+ def parser_registry(self) -> ParserRegistry:
274
+ """Access to parser registry (may be None if not using modular parsers)"""
275
+ return self._parser_registry
276
+
277
+ # Prefixo -> URI que o framework injeta sozinho.
278
+ _NAMESPACES = {
279
+ 'q': 'https://quantum.lang/ns',
280
+ 'qg': 'https://quantum.lang/game',
281
+ 'qt': 'https://quantum.lang/terminal',
282
+ 'qtest': 'https://quantum.lang/testing',
283
+ 'ui': 'https://quantum.lang/ui',
284
+ }
285
+
286
+ # O primeiro elemento do documento, seja ele qual for.
287
+ _FIRST_ELEMENT = re.compile(r'<([A-Za-z_][\w.:-]*)((?:\s[^<>]*?)?)(/?)>')
288
+
289
+ def _inject_namespace(self, content: str) -> str:
290
+ """Declara os namespaces do framework que o autor nao declarou.
291
+
292
+ Isto permite escrever `<q:component name="Foo">` sem cerimonia, em
293
+ vez de `<q:component name="Foo" xmlns:q="https://quantum.lang/ns">`.
294
+
295
+ Duas coisas estavam erradas:
296
+
297
+ 1. **A deteccao era por SUBSTRING.** `if 'xmlns:q' in content` casa
298
+ com `xmlns:qt=`, `xmlns:qg=` e `xmlns:qtest=`. Um arquivo que
299
+ declarava so o namespace da sua engine caia no ramo "ja tem
300
+ namespace", e la dentro toda injecao era um
301
+ `content.replace('xmlns:q="https://quantum.lang/ns"', ...)` que
302
+ nao achava nada. Resultado: NENHUM namespace era declarado e o
303
+ arquivo morria com "unbound prefix" — e a mensagem apontava para
304
+ a linha 1, coluna 0, que nao diz nada a ninguem.
305
+
306
+ 2. **So tres raizes eram reconhecidas** (`q:component`,
307
+ `q:application`, `q:job`). Um arquivo cuja raiz e `<q:behavior>`
308
+ ou `<qg:scene>` — as cenas e comportamentos que o scene-include
309
+ carrega, 16 arquivos entregues no repositorio — nao casava com o
310
+ padrao e tambem nao recebia namespace nenhum.
311
+
312
+ Agora a deteccao e pelo prefixo EXATO (`xmlns:q=`) e a injecao vai no
313
+ primeiro elemento, qualquer que seja ele. Uma raiz que o framework
314
+ nao suporta passa a falhar em `_parse_root_element`, com o nome do
315
+ elemento na mensagem, em vez de "unbound prefix".
316
+ """
317
+ faltando = {}
318
+ for prefixo, uri in self._NAMESPACES.items():
319
+ if f'xmlns:{prefixo}=' in content:
320
+ continue # o autor declarou; respeitar a escolha dele
321
+ # So o que o documento realmente usa.
322
+ if f'<{prefixo}:' in content or f'</{prefixo}:' in content:
323
+ faltando[prefixo] = uri
324
+
325
+ # `type="game"` implica qg: mesmo antes de qualquer tag qg: aparecer,
326
+ # e assim por diante — comportamento que ja existia.
327
+ for atributo, prefixo in (('type="game"', 'qg'),
328
+ ('type="terminal"', 'qt'),
329
+ ('type="testing"', 'qtest'),
330
+ ('type="ui"', 'ui')):
331
+ if atributo in content and f'xmlns:{prefixo}=' not in content:
332
+ faltando[prefixo] = self._NAMESPACES[prefixo]
333
+
334
+ if not faltando:
335
+ return content
336
+
337
+ declaracoes = ''.join(f' xmlns:{p}="{u}"' for p, u in
338
+ sorted(faltando.items()))
339
+
340
+ def declarar(match):
341
+ nome, attrs, fecho = match.groups()
342
+ return f'<{nome}{attrs}{declaracoes}{fecho}>'
343
+
344
+ # Apenas no primeiro ELEMENTO. O prologo — declaracao XML, doctype e
345
+ # comentarios — e pulado inteiro: examples/python-scripting.q abre
346
+ # com um comentario que menciona `<cfscript>`, e casar ali injetaria
347
+ # os namespaces dentro do comentario, deixando o componente de
348
+ # verdade sem nenhum.
349
+ deslocamento = self._offset_do_primeiro_elemento(content)
350
+ substituido = self._FIRST_ELEMENT.sub(
351
+ declarar, content[deslocamento:], count=1)
352
+ return content[:deslocamento] + substituido
353
+
354
+ @staticmethod
355
+ def _offset_do_primeiro_elemento(content: str) -> int:
356
+ """Onde comeca o primeiro elemento de verdade."""
357
+ i = 0
358
+ n = len(content)
359
+ while i < n:
360
+ while i < n and content[i].isspace():
361
+ i += 1
362
+ if content.startswith('<?', i):
363
+ fim = content.find('?>', i)
364
+ if fim == -1:
365
+ return i
366
+ i = fim + 2
367
+ elif content.startswith('<!--', i):
368
+ fim = content.find('-->', i)
369
+ if fim == -1:
370
+ return i
371
+ i = fim + 3
372
+ elif content.startswith('<!', i): # DOCTYPE
373
+ fim = content.find('>', i)
374
+ if fim == -1:
375
+ return i
376
+ i = fim + 1
377
+ else:
378
+ return i
379
+ return 0
380
+
381
+ def parse(self, source: str) -> QuantumNode:
382
+ """Parse Quantum XML from a string."""
383
+ try:
384
+ # Accept the HTML people actually write: boolean attributes
385
+ # (<input required />) and bare ampersands (Forms & Actions) are
386
+ # valid HTML and invalid XML. Ten of the forty-eight components
387
+ # shipped with this framework — login.q among them — failed to
388
+ # parse for exactly those two reasons and returned HTTP 400.
389
+ content = normalise_html(source)
390
+ content = self._inject_namespace(content)
391
+ root = fromstring_with_lines(content)
392
+ self._nest_else_siblings(root)
393
+ return self._parse_root_element(root, Path("<string>"))
394
+ except ET.ParseError as e:
395
+ erro = QuantumParseError(_explain_xml_error(e, source))
396
+ erro.line = getattr(e, 'position', (None, None))[0]
397
+ raise erro
398
+ except QuantumParseError as e:
399
+ raise _with_line(e, source)
400
+ except ParserError as e:
401
+ # A tag parser's own error is a parse error with a message meant
402
+ # for the author, not an "Unexpected error".
403
+ raise _with_line(QuantumParseError(str(e)), source, getattr(e, 'line', None)) from e
404
+ except Exception as e:
405
+ raise QuantumParseError(f"Unexpected error: {e}")
406
+
407
+ def parse_file(self, file_path: str, use_cache: bool = None) -> QuantumNode:
408
+ """Parse .q file and return AST
409
+
410
+ Args:
411
+ file_path: Path to the .q file
412
+ use_cache: Override cache behavior (None = use instance default)
413
+ """
414
+ path = Path(file_path)
415
+
416
+ if not path.exists():
417
+ raise QuantumParseError(f"File not found: {file_path}")
418
+
419
+ if not path.suffix == '.q':
420
+ raise QuantumParseError(f"Invalid extension: {path.suffix}, expected .q")
421
+
422
+ # Determine if we should use cache
423
+ should_cache = use_cache if use_cache is not None else self._use_cache
424
+
425
+ try:
426
+ # Try cache first (Phase 2 optimization)
427
+ if should_cache and self._ast_cache is not None:
428
+ self._source_path = str(path.resolve())
429
+ return self._ast_cache.get_or_parse(file_path, self)
430
+
431
+ # Fallback to direct parsing
432
+ self._source_path = str(path.resolve())
433
+ source = Path(file_path).read_text(encoding="utf-8")
434
+ return self.parse(source)
435
+ except QuantumParseError as e:
436
+ if getattr(e, 'file', None) is None:
437
+ e.file = str(path.resolve()) # DEV-2
438
+ raise
439
+ except ParserError as e:
440
+ # A tag parser's own error is a parse error with a message meant
441
+ # for the author, not an "Unexpected error".
442
+ raise QuantumParseError(str(e)) from e
443
+ except Exception as e:
444
+ raise QuantumParseError(f"Unexpected error: {e}")
445
+
446
+ def invalidate_cache(self, file_path: str = None):
447
+ """Invalidate cached AST for a file or all files
448
+
449
+ Args:
450
+ file_path: Specific file to invalidate, or None for all
451
+ """
452
+ if self._ast_cache is not None:
453
+ self._ast_cache.invalidate(file_path)
454
+
455
+ def _parse_root_element(self, root: ET.Element, path: Path) -> QuantumNode:
456
+ """Parse root element based on type"""
457
+ root_type = self._get_element_name(root)
458
+
459
+ if root_type == 'component':
460
+ return self._parse_component(root, path)
461
+ elif root_type == 'application':
462
+ return self._parse_application(root, path)
463
+ elif root_type == 'job':
464
+ return self._parse_job(root, path)
465
+ else:
466
+ raise QuantumParseError(f"Unknown root element: {root_type}")
467
+
468
+ def _nest_else_siblings(self, root: ET.Element) -> None:
469
+ """Move each `<q:else>` / `<q:elseif>` written right after `</q:if>`
470
+ into that if, so both spellings reach IfParser as the nested form.
471
+
472
+ The sibling form used to be paired only in the component body and in
473
+ BaseTagParser.parse_statements. Every other body — q:loop, q:function,
474
+ q:transaction, HTML elements and a dozen more — walks its children one
475
+ by one, so there the else was dropped without a word:
476
+ `<q:loop>...<q:if>..</q:if><q:else>..</q:else></q:loop>` never ran the
477
+ else, and a q:function with a sibling q:elseif returned None. Doing it
478
+ once on the XML tree covers every parser, including future ones.
479
+
480
+ A direct else child of a q:if is already the nested form and stays
481
+ where it is. An else with no q:if right before it is an error.
482
+ """
483
+ for parent in list(root.iter()):
484
+ if self._get_element_name(parent) == 'if':
485
+ continue
486
+ anterior = None
487
+ for child in list(parent):
488
+ if not isinstance(child.tag, str):
489
+ continue # comments, processing instructions
490
+ nome = self._get_element_name(child)
491
+ if nome in ('else', 'elseif') and self._is_quantum_tag(child):
492
+ if anterior is None:
493
+ raise QuantumParseError(
494
+ f"<q:{nome}> has no matching <q:if> before it. "
495
+ f"Put it inside the if, or right after it: "
496
+ f"<q:if ...>...</q:if> <q:{nome}>...</q:{nome}>"
497
+ )
498
+ parent.remove(child)
499
+ anterior.tail = (anterior.tail or '') + (child.tail or '')
500
+ child.tail = None
501
+ anterior.append(child)
502
+ continue
503
+ anterior = child if nome == 'if' and self._is_quantum_tag(child) else None
504
+
505
+ # Tags that existed and were removed, with what to use instead.
506
+ REMOVED_TAGS = {
507
+ 'persist': ('was removed in Quantum 0.16 (SET-2): it kept variables in the browser, '
508
+ 'and a page\'s variables live on the server, for one request. Keep what must '
509
+ 'last in session.x (per user) or in the database.'),
510
+ }
511
+
512
+ def _unknown_tag_message(self, name: str) -> str:
513
+ if name in self.REMOVED_TAGS:
514
+ return f"<q:{name}> {self.REMOVED_TAGS[name]}"
515
+ import difflib
516
+ # Only Quantum tags: the registry also holds the HTML tag names, which
517
+ # made <q:try> suggest <q:tr>.
518
+ conhecidas = {tag for tag in self._parser_registry.registered_tags
519
+ if type(self._parser_registry.get_parser(tag)).__name__ != 'HTMLParser'}
520
+ conhecidas |= {'return', 'else', 'elseif'}
521
+ parecidas = difflib.get_close_matches(name, sorted(conhecidas), n=3, cutoff=0.6)
522
+ dica = (" Did you mean " + " or ".join(f"<q:{p}>" for p in parecidas) + "?") if parecidas else ""
523
+ return f"<q:{name}> is not a Quantum tag (PARSE-1).{dica}"
524
+
525
+ @staticmethod
526
+ def _is_quantum_tag(element: ET.Element) -> bool:
527
+ return element.tag.startswith('{https://quantum.lang/ns}') or element.tag.startswith('q:')
528
+
529
+ def _get_element_name(self, element: ET.Element) -> str:
530
+ """Extract element name removing namespace"""
531
+ return element.tag.split('}')[-1] if '}' in element.tag else element.tag.split(':')[-1]
532
+
533
+ def _find_element(self, parent: ET.Element, tag_name: str) -> Optional[ET.Element]:
534
+ """Find element considering namespace"""
535
+ # Try with namespace
536
+ element = parent.find(f"q:{tag_name}", self.quantum_ns)
537
+ if element is not None:
538
+ return element
539
+
540
+ # Try without namespace (fallback)
541
+ return parent.find(tag_name)
542
+
543
+ def _find_all_elements(self, parent: ET.Element, tag_name: str) -> list:
544
+ """Find all elements considering namespace"""
545
+ # Try with namespace
546
+ elements = parent.findall(f"q:{tag_name}", self.quantum_ns)
547
+ if elements:
548
+ return elements
549
+
550
+ # Try without namespace (fallback)
551
+ return parent.findall(tag_name)
552
+
553
+ def _is_html_element(self, element: ET.Element) -> bool:
554
+ """
555
+ Check if element is HTML (not a Quantum tag).
556
+
557
+ Returns True for:
558
+ - Regular HTML tags (div, span, p, etc.)
559
+ - DOCTYPE declarations
560
+ - Comments
561
+
562
+ Returns False for:
563
+ - Quantum tags (q:* namespace)
564
+ - Special Quantum tags (param, return, function, etc.)
565
+ """
566
+ tag = element.tag
567
+
568
+ # Skip namespace declarations and special elements
569
+ if tag.startswith('{'):
570
+ # Has XML namespace - check if it's Quantum namespace
571
+ if '{https://quantum.lang/ns}' in tag:
572
+ return False
573
+ # Other namespaces might be HTML5 or SVG
574
+ return True
575
+
576
+ # Check for q: prefix (Quantum tags)
577
+ if tag.startswith('q:'):
578
+ return False
579
+
580
+ # Quantum tags without prefix (when xmlns:q is set)
581
+ quantum_tags = {
582
+ 'component', 'application', 'job', 'param', 'return', 'route',
583
+ 'if', 'elseif', 'else', 'loop', 'set', 'function', 'dispatchEvent',
584
+ 'onEvent', 'script', 'query', 'invoke', 'data', 'log', 'dump',
585
+ 'import', 'slot', # Phase 2
586
+ 'knowledge', # Knowledge Base / RAG
587
+ 'schedule', 'thread', # Job Execution System
588
+ }
589
+ if tag in quantum_tags:
590
+ return False
591
+
592
+ # Component calls (Phase 2) - Uppercase tags are components, not HTML
593
+ if tag and tag[0].isupper():
594
+ return False
595
+
596
+ # Everything else is HTML
597
+ return True
598
+
599
+ def _parse_component(self, root: ET.Element, path: Path) -> ComponentNode:
600
+ """Parse q:component"""
601
+ name = root.get('name', path.stem)
602
+ component_type = root.get('type', 'pure')
603
+
604
+ component = ComponentNode(name, component_type)
605
+
606
+ # Parse component-level attributes
607
+ component.port = root.get('port')
608
+ # A2: accepted and never read by anything — refused, like FN-2/DB-5.
609
+ for attr in ('basePath', 'health', 'metrics', 'trace'):
610
+ if root.get(attr) is not None:
611
+ raise QuantumParseError(
612
+ f'<q:component name="{name}"> {attr}= is not supported: it was accepted '
613
+ f'and never did anything.')
614
+
615
+ # Phase G: Authentication & Security attributes
616
+ require_auth_attr = root.get('require_auth', 'false').lower()
617
+ component.require_auth = require_auth_attr in ['true', '1', 'yes']
618
+ component.require_role = root.get('require_role')
619
+ # AUTH-4: where this component sends a visitor without a session.
620
+ component.login_url = root.get('login_url')
621
+ if component.login_url is not None and (not component.login_url.startswith('/')
622
+ or component.login_url.startswith('//')):
623
+ raise QuantumParseError(
624
+ f'<q:component name="{component.name}"> login_url must be a path on this server, '
625
+ f'like /login; got {component.login_url!r}')
626
+ component.require_permission = root.get('require_permission')
627
+
628
+ # HTML rendering & interactivity (Phase 1 & future Phase 3)
629
+ interactive_attr = root.get('interactive', 'false').lower()
630
+ component.interactive = interactive_attr in ['true', '1', 'yes']
631
+
632
+ # Parse q:param elements (component-level params)
633
+ for param_el in self._find_all_elements(root, 'param'):
634
+ param = self._parse_param(param_el)
635
+ component.add_param(param)
636
+
637
+ # Parse q:return elements (component-level returns)
638
+ for return_el in self._find_all_elements(root, 'return'):
639
+ return_node = self._parse_return(return_el)
640
+ component.add_return(return_node)
641
+
642
+ # Parse q:function elements
643
+ for func_el in self._find_all_elements(root, 'function'):
644
+ func = self._parse_function(func_el)
645
+ component.add_function(func)
646
+
647
+ # Parse q:onEvent elements
648
+ for event_el in self._find_all_elements(root, 'onEvent'):
649
+ event_handler = self._parse_on_event(event_el)
650
+ component.add_event_handler(event_handler)
651
+
652
+ # Parse q:script elements
653
+ for script_el in self._find_all_elements(root, 'script'):
654
+ script_content = script_el.text or ""
655
+ component.add_script(script_content.strip())
656
+
657
+ # Parse control flow statements (if, loop, set, dispatchEvent, etc)
658
+ self._parse_control_flow_statements(root, component)
659
+
660
+ self._check_statements_in_markup(component)
661
+ self._check_guards(component)
662
+
663
+ return component
664
+
665
+ def _check_guards(self, component: ComponentNode) -> None:
666
+ """AUTH-6: a guard of a page with actions reads only what exists before the page runs.
667
+
668
+ Guards also run before each q:action, and an action does not run the
669
+ page's other statements. A guard reading a page variable would find it
670
+ missing there, read the condition as false, and let the action run.
671
+ """
672
+ from quantum.core.guards import guard_conditions, mark_guards, page_guards
673
+ from quantum.core.expressions import names_read
674
+
675
+ mark_guards(component)
676
+ if not any(isinstance(s, ActionNode) for s in component.statements):
677
+ return
678
+ guardas = page_guards(component)
679
+ if not guardas:
680
+ return
681
+
682
+ definidos = {p.name for p in getattr(component, 'params', []) or []}
683
+
684
+ def definir(nodes):
685
+ for node in nodes:
686
+ if isinstance(node, (ActionNode, FunctionNode, HTMLNode, TextNode, CommentNode)):
687
+ continue
688
+ if isinstance(node, IfNode):
689
+ corpo = list(node.if_body) + list(node.else_body)
690
+ for bloco in node.elseif_blocks:
691
+ corpo += list(bloco.get('body', []))
692
+ definir(corpo)
693
+ continue
694
+ if isinstance(node, LoopNode):
695
+ definir(node.body)
696
+ continue
697
+ nome = getattr(node, 'name', None)
698
+ if isinstance(nome, str) and nome and '.' not in nome:
699
+ definidos.update({nome, f'{nome}_result'})
700
+
701
+ definir([s for s in component.statements if not any(s is g for g in guardas)])
702
+
703
+ for guarda in guardas:
704
+ for condicao in guard_conditions(guarda):
705
+ lidos = names_read(re.sub(r'\{([^{}]*)\}', r'\1', condicao)) & definidos
706
+ if lidos:
707
+ nome = sorted(lidos)[0]
708
+ raise _at(QuantumParseError(
709
+ f'<q:if condition="{condicao}"> in <q:component name="{component.name}"> '
710
+ f'redirects (AUTH-6), so it also runs before the page\'s q:action — where the page '
711
+ f'has not run and {{{nome}}} does not exist, and the guard would let the '
712
+ f'action through. Read the scope directly (e.g. session.x), or protect the '
713
+ f'page with require_auth / require_role. See {HOW_A_PAGE_RUNS}'), guarda)
714
+
715
+ # Nodes the HTML renderer draws. Anything else inside markup is a statement,
716
+ # and statements run in the execution pass, before the render pass sees
717
+ # the markup around them.
718
+ _RENDERED = (HTMLNode, TextNode, CommentNode, DocTypeNode, LoopNode, IfNode,
719
+ ComponentCallNode, SlotNode, ImportNode)
720
+
721
+ def _check_statements_in_markup(self, component: ComponentNode) -> None:
722
+ """PARSE-2: a statement where it would never run, or run with the wrong values.
723
+
724
+ Execution walks the component's statements and skips markup; the
725
+ renderer walks the markup and skips statements. So a q:set inside a
726
+ <ul> never ran — `<li>{y}</li>` printed the literal `{y}` — and a q:set
727
+ in a q:loop that also renders rows ran once per row in the execution
728
+ pass, leaving every row to render with the LAST value. Both were
729
+ silent.
730
+ """
731
+ def filhos(node):
732
+ if isinstance(node, IfNode):
733
+ corpo = list(node.if_body)
734
+ for bloco in node.elseif_blocks:
735
+ corpo += list(bloco.get('body', []))
736
+ return corpo + list(node.else_body)
737
+ if isinstance(node, LoopNode):
738
+ return list(node.body)
739
+ if isinstance(node, (HTMLNode, ComponentCallNode)) or is_ui_node(node):
740
+ return list(getattr(node, 'children', None) or [])
741
+ return []
742
+
743
+ def renderiza(nodes):
744
+ for node in nodes:
745
+ if isinstance(node, TextNode):
746
+ if node.content.strip():
747
+ return True
748
+ elif isinstance(node, (HTMLNode, ComponentCallNode)) or is_ui_node(node):
749
+ return True
750
+ elif isinstance(node, (IfNode, LoopNode)) and renderiza(filhos(node)):
751
+ return True
752
+ return False
753
+
754
+ def tag(node):
755
+ nome = type(node).__name__
756
+ nome = nome[:-4] if nome.endswith('Node') else nome
757
+ rotulo = f"q:{nome[:1].lower()}{nome[1:]}"
758
+ atributo = getattr(node, 'name', None)
759
+ return f'<{rotulo} name="{atributo}">' if isinstance(atributo, str) else f'<{rotulo}>'
760
+
761
+ def expressoes(nodes):
762
+ """The text the renderer evaluates under `nodes`: contents, attributes, conditions."""
763
+ partes = []
764
+ for node in nodes:
765
+ if isinstance(node, TextNode):
766
+ partes.append(node.content)
767
+ elif isinstance(node, HTMLNode):
768
+ partes.extend(str(v) for v in (node.attributes or {}).values())
769
+ elif isinstance(node, ComponentCallNode):
770
+ partes.extend(str(v) for v in (node.props or {}).values())
771
+ elif isinstance(node, IfNode):
772
+ partes.append('{%s}' % node.condition)
773
+ partes.extend('{%s}' % b.get('condition', '') for b in node.elseif_blocks)
774
+ elif isinstance(node, LoopNode):
775
+ partes.append('{%s}' % (node.items or ''))
776
+ partes.append(expressoes(filhos(node)))
777
+ return '\n'.join(partes)
778
+
779
+ def usado(nome, texto):
780
+ padrao = re.compile(r'(?<![\w.])' + re.escape(nome) + r'(?![\w])')
781
+ return any(padrao.search(m) for m in re.findall(r'\{([^{}]*)\}', texto))
782
+
783
+ def andar(nodes, onde):
784
+ for node in nodes:
785
+ if not isinstance(node, self._RENDERED) and not is_ui_node(node):
786
+ if onde and onde[0] == 'markup':
787
+ raise _at(QuantumParseError(
788
+ f'{tag(node)} inside {onde[1]} in <q:component name="{component.name}"> '
789
+ f'never runs (PARSE-2): statements run before the page is rendered, and markup '
790
+ f'is only rendered. Move it above the markup (or into a q:action). '
791
+ f'See {HOW_A_PAGE_RUNS}'), node)
792
+ nome = getattr(node, 'name', None)
793
+ # A running total read AFTER the loop is fine; a value the
794
+ # loop's own rows read is not.
795
+ if onde and isinstance(nome, str) and usado(nome, onde[1]):
796
+ raise _at(QuantumParseError(
797
+ f'{tag(node)} inside a q:loop whose rows use {{{nome}}} in '
798
+ f'<q:component name="{component.name}">: the loop runs for every item '
799
+ f'before any row is rendered, so every row would show the last value (PARSE-2). '
800
+ f'Compute it in the expression instead, e.g. {{item.price * item.qty}}. '
801
+ f'See {HOW_A_PAGE_RUNS}'), node)
802
+ continue
803
+ if isinstance(node, HTMLNode):
804
+ andar(filhos(node), onde if onde and onde[0] == 'markup' else ('markup', f'<{node.tag}>'))
805
+ elif is_ui_node(node):
806
+ rotulo = f'<ui:{type(node).__name__[2:-4].lower()}>'
807
+ andar(filhos(node), onde if onde and onde[0] == 'markup' else ('markup', rotulo))
808
+ elif isinstance(node, ComponentCallNode):
809
+ andar(filhos(node), onde if onde and onde[0] == 'markup'
810
+ else ('markup', f'the content of <{node.component_name}>'))
811
+ elif isinstance(node, LoopNode):
812
+ corpo = filhos(node)
813
+ andar(corpo, onde or (('loop', expressoes(corpo)) if renderiza(corpo) else None))
814
+ elif isinstance(node, IfNode):
815
+ andar(filhos(node), onde)
816
+
817
+ andar(component.statements, None)
818
+
819
+ def _parse_control_flow_statements(self, parent: ET.Element, component: ComponentNode):
820
+ """Parse control flow statements - delegates to modular parser registry"""
821
+ for child in parent:
822
+ child_type = self._get_element_name(child)
823
+
824
+ # Collected by _parse_component above, not statements.
825
+ if child_type in ('param', 'onEvent', 'script') and self._is_quantum_tag(child):
826
+ continue
827
+
828
+ # Special case: inline job definition (needs path parameter)
829
+ if child_type == 'job':
830
+ from pathlib import Path as PathLib
831
+ job_node = self._parse_job(child, PathLib("<inline>"))
832
+ component.add_statement(job_node)
833
+ continue
834
+
835
+ # `<q:else>` / `<q:elseif>` IRMAO de um `<q:if>` no corpo do
836
+ # componente: anexa ao if anterior. A forma irma nao tinha parser
837
+ # e sumia calada — a documentacao a ensinava e arquivos entregues
838
+ # a usavam (ver base.parse_statements para o porque completo).
839
+ if child_type in ('else', 'elseif'):
840
+ anterior = component.statements[-1] if component.statements else None
841
+ if isinstance(anterior, IfNode):
842
+ self._attach_else_sibling(anterior, child_type, child)
843
+ continue
844
+ raise QuantumParseError(
845
+ f"<q:{child_type}> has no matching <q:if> before it. "
846
+ f"Put it inside the if, or right after it: "
847
+ f"<q:if ...>...</q:if> <q:{child_type}>...</q:{child_type}>"
848
+ )
849
+
850
+ # Delegate to modular parser registry via _parse_statement
851
+ node = self._parse_statement(child)
852
+ if node is not None:
853
+ component.add_statement(node)
854
+ # Check if this produces HTML
855
+ if self._is_html_element(child) or (child_type and child_type[0].isupper()):
856
+ component.has_html = True
857
+
858
+ def _attach_else_sibling(self, if_node: 'IfNode', kind: str, element: ET.Element):
859
+ """Anexa um else/elseif IRMAO ao IfNode anterior — mesmo resultado da
860
+ forma aninhada que o IfParser ja monta, texto incluido (IF-3)."""
861
+ corpo = self._parser_registry.get_parser('if')._body(element)
862
+ if kind == 'elseif':
863
+ if_node.add_elseif_block(element.get('condition', ''), corpo)
864
+ else:
865
+ for stmt in corpo:
866
+ if_node.add_else_statement(stmt)
867
+
868
+ def _parse_statement(self, element: ET.Element) -> Optional[QuantumNode]:
869
+ """Parse one statement, and remember where it is in the file (DEV-2).
870
+
871
+ The node gets `source_line`, which runtime errors report; a parse
872
+ error from this element — or from inside it, if nothing deeper
873
+ claimed it — gets `line`.
874
+ """
875
+ try:
876
+ node = self._parse_statement_node(element)
877
+ except (QuantumParseError, ParserError) as exc:
878
+ if getattr(exc, 'line', None) is None:
879
+ exc.line = getattr(element, 'sourceline', None)
880
+ raise
881
+ if node is not None and getattr(element, 'sourceline', None):
882
+ try:
883
+ node.source_line = element.sourceline
884
+ node.source_file = self._source_path
885
+ except AttributeError:
886
+ pass # a node with __slots__
887
+ return node
888
+
889
+ def _parse_statement_node(self, element: ET.Element) -> Optional[QuantumNode]:
890
+ """Parse individual statement (return, set, dispatchEvent, etc)"""
891
+ element_type = self._get_element_name(element)
892
+
893
+ # === MODULAR PARSER REGISTRY (no fallback) ===
894
+ # PHASE 1 DIAGNOSTIC: Legacy fallback disabled to test modular coverage
895
+ if self._use_modular_parsers and self._parser_registry:
896
+ # UI-1: a ui:* element inside a page is a UI node, drawn by
897
+ # UIPageRenderer with the page's runtime doing the logic. First,
898
+ # before the registry: ui:button would otherwise be the HTML <button>.
899
+ if self._get_element_ui_namespace(element) == 'ui':
900
+ try:
901
+ return UIParser(self, page_mode=True).parse_ui_element(element_type, element)
902
+ except UIParseError as exc:
903
+ erro = QuantumParseError(str(exc))
904
+ erro.line = getattr(exc, 'line', None) # the inner ui:* element's line
905
+ raise erro from exc
906
+
907
+ # PARSE-1: <q:html>, <q:div> — an HTML element name in the q:
908
+ # namespace. The registry answered for it with the HTML parser, so
909
+ # <q:html value="{post.content}"/> became an empty <html> element
910
+ # instead of an unknown tag.
911
+ # q:script is a Quantum tag on purpose: client JavaScript, kept as
912
+ # an HTML node (see ui_desktop_adapter).
913
+ if (self._is_quantum_tag(element) and element_type != 'script'
914
+ and element_type in self._parser_registry.html_parser.tag_names):
915
+ raise QuantumParseError(
916
+ f"<q:{element_type}> is not a Quantum tag: HTML elements are written "
917
+ f"without the q: prefix (<{element_type}>)")
918
+ if self._parser_registry.has_parser(element_type):
919
+ return self._parser_registry.parse(element, element_type)
920
+
921
+ # Component calls (uppercase naming convention)
922
+ if element_type and element_type[0].isupper():
923
+ from quantum.core.parsers import ComponentCallParser
924
+ if ComponentCallParser.is_component_call(element_type):
925
+ return self._parser_registry.component_call_parser.parse(element)
926
+
927
+ # HTML elements
928
+ # <q:return> aninhado. Nao ha parser registrado para 'return' —
929
+ # o nivel de componente o coleta a parte, com _find_all_elements —
930
+ # entao aqui ele caia no `return None` do fim e era DESCARTADO em
931
+ # silencio. `<q:if ...><q:return value="X"/></q:if>` nao retornava
932
+ # X: o componente seguia e devolvia o return seguinte, ou nada.
933
+ # O mesmo valia dentro de q:loop e q:else.
934
+ if element_type == 'return':
935
+ return self._parse_return(element)
936
+
937
+ if self._is_html_element(element):
938
+ if self._parser_registry.html_parser.can_parse(element_type):
939
+ return self._parser_registry.html_parser.parse(element)
940
+
941
+ # PARSE-1: a q: tag nobody parses is an error. It used to return
942
+ # None here and vanish — <q:sett>, <q:retrun> and <q:iff> ran as if
943
+ # they were not there, and so did tags the documentation described
944
+ # but that never existed (q:try/q:catch, q:storedproc).
945
+ if self._is_quantum_tag(element):
946
+ raise QuantumParseError(self._unknown_tag_message(element_type))
947
+ return None
948
+
949
+ # If modular parsers are disabled, raise an error
950
+ raise QuantumParseError("Modular parsers are disabled but legacy fallback has been removed")
951
+
952
+ def _parse_application(self, root: ET.Element, path: Path) -> ApplicationNode:
953
+ """Parse q:application"""
954
+ app_id = root.get('id', path.stem)
955
+ app_type = root.get('type', 'html')
956
+
957
+ from quantum.core.tiers import REMOVED_APP_TYPES, removed_app_type_message
958
+ if app_type in REMOVED_APP_TYPES:
959
+ raise QuantumParseError(removed_app_type_message(app_type, 'type' in root.attrib))
960
+
961
+ app = ApplicationNode(app_id, app_type)
962
+ app.engine = root.get('engine')
963
+
964
+ # Parse theme attribute for UI applications
965
+ theme_attr = root.get('theme')
966
+ if theme_attr and app_type == 'ui':
967
+ app.ui_theme_preset = theme_attr
968
+ # Create a UIThemeNode from the attribute
969
+ theme_node = UIThemeNode()
970
+ theme_node.preset = theme_attr
971
+ app.ui_theme = theme_node
972
+
973
+ # Parse q:route elements
974
+ for route_el in self._find_all_elements(root, 'route'):
975
+ route = self._parse_route(route_el)
976
+ app.add_route(route)
977
+
978
+ # q:set e q:function no topo do <q:application>, para qualquer
979
+ # engine. Os parsers por engine so olham os filhos do proprio
980
+ # namespace (qt:, qg:, ui:), entao estes eram silenciosamente
981
+ # descartados — inclusive a funcao para a qual um on-click apontava.
982
+ self._parse_application_quantum_children(root, app)
983
+
984
+ # Game Engine 2D: parse game children when type="game"
985
+ if app_type == 'game':
986
+ self._parse_game_application_children(root, app)
987
+
988
+ # Terminal Engine: parse terminal children when type="terminal"
989
+ if app_type == 'terminal':
990
+ self._parse_terminal_application_children(root, app)
991
+
992
+ # Testing Engine: parse testing children when type="testing"
993
+ if app_type == 'testing':
994
+ self._parse_testing_application_children(root, app)
995
+
996
+ # UI Engine: parse ui children when type="ui"
997
+ if app_type == 'ui':
998
+ self._parse_ui_application_children(root, app)
999
+
1000
+ return app
1001
+
1002
+ def _parse_application_quantum_children(self, root: ET.Element,
1003
+ app: ApplicationNode):
1004
+ """Coleta <q:function> e <q:set> escritos direto no q:application."""
1005
+ for child in root:
1006
+ local_name = self._get_element_name(child)
1007
+ if local_name not in ('function', 'set'):
1008
+ continue
1009
+ tag = child.tag
1010
+ # So o namespace q:. Um <ui:set> ou <qg:set> pertence ao seu
1011
+ # proprio parser, e um elemento sem prefixo nao e tag do
1012
+ # framework.
1013
+ if not (tag.startswith('q:')
1014
+ or tag.startswith('{https://quantum.lang/ns}')):
1015
+ continue
1016
+ try:
1017
+ node = self._parse_statement(child)
1018
+ except QuantumParseError:
1019
+ raise
1020
+ except Exception:
1021
+ continue
1022
+ if node is None:
1023
+ continue
1024
+ if isinstance(node, FunctionNode):
1025
+ app.functions.append(node)
1026
+ elif isinstance(node, SetNode):
1027
+ app.state_vars.append(node)
1028
+
1029
+ def _parse_game_application_children(self, root: ET.Element, app: ApplicationNode):
1030
+ """Parse children of a game application (qg: elements at top level)."""
1031
+ game_parser = GameParser(self)
1032
+ for child in root:
1033
+ local_name = self._get_element_name(child)
1034
+ ns = self._get_element_game_namespace(child)
1035
+
1036
+ if ns == 'game':
1037
+ # Handle scene-include specially (resolved at parse time)
1038
+ if local_name == 'scene-include':
1039
+ self._parse_scene_include(child, app, game_parser)
1040
+ continue
1041
+
1042
+ node = game_parser.parse_game_element(local_name, child)
1043
+ if isinstance(node, SceneNode):
1044
+ app.scenes.append(node)
1045
+ elif isinstance(node, BehaviorNode):
1046
+ app.behaviors.append(node)
1047
+ elif isinstance(node, PrefabNode):
1048
+ app.prefabs.append(node)
1049
+ elif isinstance(node, EnemyNode):
1050
+ app.enemies.append(node)
1051
+ elif isinstance(node, PersistentNode):
1052
+ app.persistent.append(node)
1053
+
1054
+ def _parse_scene_include(self, element: ET.Element, app: ApplicationNode, game_parser):
1055
+ """Parse <qg:scene-include src="..."> - Include a scene from external .q file.
1056
+
1057
+ The external file should contain a <qg:scene> element (with or without
1058
+ a <q:application> wrapper). The scene is added to the application's scene list.
1059
+ """
1060
+ src = element.get('src', '')
1061
+ if not src:
1062
+ raise QuantumParseError("scene-include requires a 'src' attribute")
1063
+
1064
+ # Resolve relative path from the source file being parsed
1065
+ base_path = getattr(self, '_source_path', None)
1066
+ if base_path:
1067
+ include_path = Path(base_path).parent / src
1068
+ else:
1069
+ include_path = Path(src)
1070
+
1071
+ if not include_path.exists():
1072
+ raise QuantumParseError(f"scene-include: file not found: {include_path}")
1073
+
1074
+ content = include_path.read_text(encoding='utf-8')
1075
+
1076
+ # O mesmo HTML tolerado em qualquer outro .q. Este caminho le um
1077
+ # arquivo .q e ia direto para o ET.fromstring la embaixo, sem passar
1078
+ # por aqui: um `<br>`, um `&` cru, um atributo booleano ou um `<`
1079
+ # dentro de um valor davam "scene-include: parse error" num arquivo
1080
+ # que o parser normal aceita sem reclamar.
1081
+ content = normalise_html(content)
1082
+
1083
+ # Strip XML processing instruction if present
1084
+ import re
1085
+ content = re.sub(r'<\?xml[^?]*\?>\s*', '', content)
1086
+ # Strip XML comments at the top level
1087
+ content = re.sub(r'<!--[\s\S]*?-->\s*', '', content, count=1)
1088
+ content = content.strip()
1089
+
1090
+ # Wrap in a root element with namespace declarations
1091
+ # This handles bare <qg:scene> files that don't have <q:application>
1092
+ if 'xmlns:qg' not in content:
1093
+ ns_attrs = 'xmlns:qg="https://quantum.lang/game"'
1094
+ if 'q:' in content:
1095
+ ns_attrs += ' xmlns:q="https://quantum.lang/ns"'
1096
+ content = f'<_root {ns_attrs}>{content}</_root>'
1097
+
1098
+ try:
1099
+ include_root = ET.fromstring(content)
1100
+ except ET.ParseError as e:
1101
+ raise QuantumParseError(f"scene-include: parse error in {src}: {e}")
1102
+
1103
+ # Find scene element(s) in the included file
1104
+ self._extract_scenes_from_include(include_root, app, game_parser)
1105
+
1106
+ def _extract_scenes_from_include(self, root: ET.Element, app: ApplicationNode, game_parser):
1107
+ """Extract SceneNode(s) from an included file's parsed XML."""
1108
+ tag = root.tag
1109
+ # Strip namespace
1110
+ if '}' in tag:
1111
+ local = tag.split('}')[-1]
1112
+ elif ':' in tag:
1113
+ local = tag.split(':')[-1]
1114
+ else:
1115
+ local = tag
1116
+
1117
+ if local == 'scene':
1118
+ # Root element is the scene itself
1119
+ node = game_parser.parse_game_element('scene', root)
1120
+ if isinstance(node, SceneNode):
1121
+ app.scenes.append(node)
1122
+ else:
1123
+ # Root is a wrapper — look for scene children
1124
+ for child in root:
1125
+ child_ns = self._get_element_game_namespace(child)
1126
+ child_local = self._get_element_name(child)
1127
+ if child_ns == 'game' and child_local == 'scene':
1128
+ node = game_parser.parse_game_element('scene', child)
1129
+ if isinstance(node, SceneNode):
1130
+ app.scenes.append(node)
1131
+
1132
+ def _get_element_game_namespace(self, element: ET.Element) -> str:
1133
+ """Detect if element belongs to qg: (game) or q: (quantum) namespace."""
1134
+ tag = element.tag
1135
+ if '{https://quantum.lang/game}' in tag:
1136
+ return 'game'
1137
+ if '{https://quantum.lang/ns}' in tag:
1138
+ return 'quantum'
1139
+ if tag.startswith('qg:'):
1140
+ return 'game'
1141
+ if tag.startswith('q:'):
1142
+ return 'quantum'
1143
+ return 'html'
1144
+
1145
+ def _parse_terminal_application_children(self, root: ET.Element, app: ApplicationNode):
1146
+ """Parse children of a terminal application (qt: elements at top level)."""
1147
+ terminal_parser = TerminalParser(self)
1148
+ for child in root:
1149
+ local_name = self._get_element_name(child)
1150
+ ns = self._get_element_terminal_namespace(child)
1151
+
1152
+ if ns == 'terminal':
1153
+ node = terminal_parser.parse_terminal_element(local_name, child)
1154
+ if isinstance(node, TerminalScreenNode):
1155
+ app.screens.append(node)
1156
+ elif isinstance(node, TerminalKeybindingNode):
1157
+ app.keybindings.append(node)
1158
+ elif isinstance(node, TerminalServiceNode):
1159
+ app.services.append(node)
1160
+ elif isinstance(node, TerminalCssNode):
1161
+ app.terminal_css += node.content + '\n'
1162
+
1163
+ def _get_element_terminal_namespace(self, element: ET.Element) -> str:
1164
+ """Detect if element belongs to qt: (terminal) or q: (quantum) namespace."""
1165
+ tag = element.tag
1166
+ if '{https://quantum.lang/terminal}' in tag:
1167
+ return 'terminal'
1168
+ if '{https://quantum.lang/ns}' in tag:
1169
+ return 'quantum'
1170
+ if tag.startswith('qt:'):
1171
+ return 'terminal'
1172
+ if tag.startswith('q:'):
1173
+ return 'quantum'
1174
+ return 'html'
1175
+
1176
+ def _parse_testing_application_children(self, root: ET.Element, app: ApplicationNode):
1177
+ """Parse children of a testing application (qtest: elements at top level)."""
1178
+ testing_parser = TestingParser(self)
1179
+ for child in root:
1180
+ local_name = self._get_element_name(child)
1181
+ ns = self._get_element_testing_namespace(child)
1182
+
1183
+ if ns == 'testing':
1184
+ node = testing_parser.parse_testing_element(local_name, child)
1185
+ if isinstance(node, TestingTestSuiteNode):
1186
+ app.test_suites.append(node)
1187
+ elif isinstance(node, TestingBrowserConfigNode):
1188
+ app.test_config = node
1189
+ elif isinstance(node, TestingFixtureNode):
1190
+ app.test_fixtures.append(node)
1191
+ elif isinstance(node, TestingMockNode):
1192
+ app.test_mocks.append(node)
1193
+ elif isinstance(node, TestingAuthNode):
1194
+ app.test_auth_states.append(node)
1195
+
1196
+ def _get_element_testing_namespace(self, element: ET.Element) -> str:
1197
+ """Detect if element belongs to qtest: (testing) or q: (quantum) namespace."""
1198
+ tag = element.tag
1199
+ if '{https://quantum.lang/testing}' in tag:
1200
+ return 'testing'
1201
+ if '{https://quantum.lang/ns}' in tag:
1202
+ return 'quantum'
1203
+ if tag.startswith('qtest:'):
1204
+ return 'testing'
1205
+ if tag.startswith('q:'):
1206
+ return 'quantum'
1207
+ return 'html'
1208
+
1209
+ def _parse_ui_application_children(self, root: ET.Element, app: ApplicationNode):
1210
+ """Parse children of a UI application (ui: elements at top level)."""
1211
+ ui_parser = UIParser(self)
1212
+ for child in root:
1213
+ local_name = self._get_element_name(child)
1214
+ ns = self._get_element_ui_namespace(child)
1215
+
1216
+ if ns == 'ui':
1217
+ node = ui_parser.parse_ui_element(local_name, child)
1218
+ if isinstance(node, UIWindowNode):
1219
+ app.ui_windows.append(node)
1220
+ elif isinstance(node, UIThemeNode):
1221
+ # ui:theme overrides any theme attribute
1222
+ app.ui_theme = node
1223
+ else:
1224
+ app.ui_children.append(node)
1225
+ elif ns == 'quantum':
1226
+ # Allow q: tags at the top level of a UI app (e.g. q:set)
1227
+ statement = self._parse_statement(child)
1228
+ if statement:
1229
+ app.ui_children.append(statement)
1230
+
1231
+ def _get_element_ui_namespace(self, element: ET.Element) -> str:
1232
+ """Detect if element belongs to ui: (UI engine) or q: (quantum) namespace."""
1233
+ tag = element.tag
1234
+ if '{https://quantum.lang/ui}' in tag:
1235
+ return 'ui'
1236
+ if '{https://quantum.lang/ns}' in tag:
1237
+ return 'quantum'
1238
+ if tag.startswith('ui:'):
1239
+ return 'ui'
1240
+ if tag.startswith('q:'):
1241
+ return 'quantum'
1242
+ return 'html'
1243
+
1244
+ def _parse_job(self, root: ET.Element, path: Path) -> JobNode:
1245
+ """
1246
+ Parse q:job - Job queue for batch processing.
1247
+
1248
+ Examples:
1249
+ <q:job name="sendEmail" queue="emails" priority="5">
1250
+ <q:param name="to" type="string" />
1251
+ <q:mail to="{to}" subject="Notification">...</q:mail>
1252
+ </q:job>
1253
+
1254
+ <q:job name="processOrder" action="dispatch" delay="5m" />
1255
+ """
1256
+ # Support both 'name' and legacy 'id' attribute
1257
+ name = root.get('name') or root.get('id', path.stem)
1258
+ queue = root.get('queue', 'default')
1259
+ action = root.get('action', 'define')
1260
+
1261
+ job_node = JobNode(name, queue, action)
1262
+
1263
+ # Parse optional attributes
1264
+ job_node.delay = root.get('delay')
1265
+ job_node.timeout = root.get('timeout')
1266
+ job_node.backoff = root.get('backoff', '30s')
1267
+
1268
+ priority_attr = root.get('priority')
1269
+ if priority_attr:
1270
+ try:
1271
+ job_node.priority = int(priority_attr)
1272
+ except ValueError:
1273
+ pass
1274
+
1275
+ attempts_attr = root.get('attempts')
1276
+ if attempts_attr:
1277
+ try:
1278
+ job_node.attempts = int(attempts_attr)
1279
+ except ValueError:
1280
+ pass
1281
+
1282
+ # Legacy support
1283
+ job_node.schedule = root.get('schedule')
1284
+
1285
+ # Parse children (params and body statements)
1286
+ for child in root:
1287
+ child_type = self._get_element_name(child)
1288
+
1289
+ if child_type == 'param':
1290
+ param = self._parse_param(child)
1291
+ job_node.add_param(param)
1292
+ else:
1293
+ # Parse body statements
1294
+ statement = self._parse_statement(child)
1295
+ if statement:
1296
+ job_node.add_statement(statement)
1297
+
1298
+ return job_node
1299
+
1300
+ def _parse_param(self, element: ET.Element) -> QuantumParam:
1301
+ """Parse q:param"""
1302
+ param = QuantumParam(
1303
+ name=element.get('name', ''),
1304
+ type=element.get('type', 'string'),
1305
+ required=element.get('required', 'false').lower() == 'true',
1306
+ default=element.get('default'),
1307
+ description=element.get('description')
1308
+ )
1309
+ if element.get('validation') is not None:
1310
+ # A2: accepted and never read. The rules are validate=, type=,
1311
+ # pattern=, min/max, minlength/maxlength and enum=.
1312
+ from quantum.core.parser import QuantumParseError
1313
+ raise QuantumParseError(
1314
+ f'<q:param name="{element.get("name", "")}"> validation= is not supported: it was '
1315
+ f'accepted and never did anything. Use type=, pattern=, min/max, '
1316
+ f'minlength/maxlength or enum=.')
1317
+
1318
+ # REST-specific
1319
+ param.source = element.get('source', 'auto')
1320
+
1321
+ # value= is how a CALL SITE passes an argument — <q:job> dispatch,
1322
+ # <q:invoke>, <q:query>. This function read only default=, so every
1323
+ # <q:param name="to" value="a@b.c" /> inside a q:job arrived with no
1324
+ # value at all and each dispatched job got nulls. Declaration sites
1325
+ # (q:function, q:action) use default= and are unaffected.
1326
+ param.value = element.get('value')
1327
+
1328
+ # Validation
1329
+ param.validate_rule = element.get('validate')
1330
+ param.pattern = element.get('pattern')
1331
+ param.min = element.get('min')
1332
+ param.max = element.get('max')
1333
+
1334
+ minlength_str = element.get('minlength')
1335
+ if minlength_str:
1336
+ try:
1337
+ param.minlength = int(minlength_str)
1338
+ except ValueError:
1339
+ pass
1340
+
1341
+ maxlength_str = element.get('maxlength')
1342
+ if maxlength_str:
1343
+ try:
1344
+ param.maxlength = int(maxlength_str)
1345
+ except ValueError:
1346
+ pass
1347
+
1348
+ param.range = element.get('range')
1349
+ param.enum = element.get('enum')
1350
+
1351
+ # File upload
1352
+ param.maxsize = element.get('maxsize')
1353
+ param.accept = element.get('accept')
1354
+
1355
+ return param
1356
+
1357
+ def _parse_return(self, element: ET.Element) -> QuantumReturn:
1358
+ """Parse q:return"""
1359
+ return QuantumReturn(
1360
+ name=element.get('name'),
1361
+ type=element.get('type', 'string'),
1362
+ value=element.get('value', ''),
1363
+ description=element.get('description')
1364
+ )
1365
+
1366
+ def _parse_route(self, element: ET.Element) -> QuantumRoute:
1367
+ """Parse q:route"""
1368
+ path = element.get('path', '/')
1369
+ method = element.get('method', 'GET').upper()
1370
+
1371
+ route = QuantumRoute(path, method)
1372
+
1373
+ # Parse q:return inside the route
1374
+ for return_el in self._find_all_elements(element, 'return'):
1375
+ return_node = self._parse_return(return_el)
1376
+ route.returns.append(return_node)
1377
+
1378
+ return route
1379
+
1380
+ def _parse_function(self, func_element: ET.Element) -> FunctionNode:
1381
+ """Parse q:function statement"""
1382
+ name = func_element.get('name')
1383
+
1384
+ if not name:
1385
+ raise QuantumParseError("Function requires 'name' attribute")
1386
+
1387
+ func_node = FunctionNode(name)
1388
+
1389
+ # Core attributes
1390
+ func_node.return_type = func_element.get('returnType', 'any')
1391
+ func_node.scope = func_element.get('scope', 'component')
1392
+ func_node.access = func_element.get('access', 'public')
1393
+ func_node.description = func_element.get('description')
1394
+ func_node.hint = func_element.get('hint')
1395
+
1396
+ # Validation
1397
+ func_node.validate_params = func_element.get('validate', 'false').lower() == 'true'
1398
+
1399
+ # Performance
1400
+ cache_attr = func_element.get('cache')
1401
+ if cache_attr:
1402
+ if cache_attr.lower() == 'true':
1403
+ func_node.cache = True
1404
+ elif cache_attr.endswith('s'): # "60s"
1405
+ func_node.cache = True
1406
+ try:
1407
+ func_node.cache_ttl = int(cache_attr[:-1])
1408
+ except ValueError:
1409
+ pass
1410
+
1411
+ func_node.memoize = func_element.get('memoize', 'false').lower() == 'true'
1412
+ func_node.pure = func_element.get('pure', 'false').lower() == 'true'
1413
+
1414
+ # Behavior
1415
+ func_node.async_func = func_element.get('async', 'false').lower() == 'true'
1416
+
1417
+ retry_attr = func_element.get('retry')
1418
+ if retry_attr:
1419
+ try:
1420
+ func_node.retry = int(retry_attr)
1421
+ except ValueError:
1422
+ pass
1423
+
1424
+ timeout_attr = func_element.get('timeout')
1425
+ if timeout_attr:
1426
+ func_node.timeout = timeout_attr
1427
+
1428
+ # REST API (optional)
1429
+ endpoint = func_element.get('endpoint')
1430
+ if endpoint:
1431
+ method = func_element.get('method', 'GET')
1432
+ func_node.enable_rest(endpoint, method)
1433
+
1434
+ # REST-specific attributes
1435
+ if func_element.get('produces'):
1436
+ func_node.rest_config.produces = func_element.get('produces')
1437
+
1438
+ if func_element.get('consumes'):
1439
+ func_node.rest_config.consumes = func_element.get('consumes')
1440
+
1441
+ if func_element.get('auth'):
1442
+ func_node.rest_config.auth = func_element.get('auth')
1443
+
1444
+ if func_element.get('roles'):
1445
+ roles_str = func_element.get('roles')
1446
+ func_node.rest_config.roles = [r.strip() for r in roles_str.split(',')]
1447
+
1448
+ if func_element.get('rateLimit'):
1449
+ func_node.rest_config.rate_limit = func_element.get('rateLimit')
1450
+
1451
+ cors_attr = func_element.get('cors')
1452
+ if cors_attr and cors_attr.lower() == 'true':
1453
+ func_node.rest_config.cors = True
1454
+
1455
+ status_attr = func_element.get('status')
1456
+ if status_attr:
1457
+ try:
1458
+ func_node.rest_config.status = int(status_attr)
1459
+ except ValueError:
1460
+ pass
1461
+
1462
+ # Parse function params and body
1463
+ for child in func_element:
1464
+ child_type = self._get_element_name(child)
1465
+
1466
+ if child_type == 'param':
1467
+ param = self._parse_param(child)
1468
+ func_node.add_param(param)
1469
+ elif child_type == 'return':
1470
+ # ReturnParser is intentionally not in the tag registry, so
1471
+ # _parse_statement() returns None for <q:return> — which
1472
+ # silently dropped the return from every function body,
1473
+ # making every q:function return nothing.
1474
+ from quantum.core.parsers.functions.return_parser import ReturnParser
1475
+ func_node.add_statement(ReturnParser(self).parse(child))
1476
+ else:
1477
+ # Parse body statements
1478
+ statement = self._parse_statement(child)
1479
+ if statement:
1480
+ func_node.add_statement(statement)
1481
+
1482
+ return func_node
1483
+
1484
+ def _parse_dispatch_event(self, event_element: ET.Element) -> DispatchEventNode:
1485
+ """Parse q:dispatchEvent statement"""
1486
+ event = event_element.get('event')
1487
+
1488
+ if not event:
1489
+ raise QuantumParseError("dispatchEvent requires 'event' attribute")
1490
+
1491
+ dispatch_node = DispatchEventNode(event)
1492
+
1493
+ # Optional attributes
1494
+ dispatch_node.data = event_element.get('data')
1495
+ dispatch_node.queue = event_element.get('queue')
1496
+ dispatch_node.exchange = event_element.get('exchange')
1497
+ dispatch_node.routing_key = event_element.get('routingKey')
1498
+ dispatch_node.priority = event_element.get('priority', 'normal')
1499
+ dispatch_node.delay = event_element.get('delay')
1500
+ dispatch_node.ttl = event_element.get('ttl')
1501
+ dispatch_node.metadata = event_element.get('metadata')
1502
+
1503
+ return dispatch_node
1504
+
1505
+ def _parse_on_event(self, event_element: ET.Element) -> OnEventNode:
1506
+ """Parse q:onEvent handler"""
1507
+ event = event_element.get('event')
1508
+
1509
+ if not event:
1510
+ raise QuantumParseError("onEvent requires 'event' attribute")
1511
+
1512
+ event_node = OnEventNode(event)
1513
+
1514
+ # Optional attributes
1515
+ event_node.queue = event_element.get('queue')
1516
+
1517
+ max_retries_attr = event_element.get('maxRetries')
1518
+ if max_retries_attr:
1519
+ try:
1520
+ event_node.max_retries = int(max_retries_attr)
1521
+ except ValueError:
1522
+ pass
1523
+
1524
+ event_node.retry_delay = event_element.get('retryDelay')
1525
+ event_node.dead_letter = event_element.get('deadLetter')
1526
+ event_node.filter = event_element.get('filter')
1527
+
1528
+ concurrent_attr = event_element.get('concurrent')
1529
+ if concurrent_attr:
1530
+ try:
1531
+ event_node.concurrent = int(concurrent_attr)
1532
+ except ValueError:
1533
+ pass
1534
+
1535
+ prefetch_attr = event_element.get('prefetch')
1536
+ if prefetch_attr:
1537
+ try:
1538
+ event_node.prefetch = int(prefetch_attr)
1539
+ except ValueError:
1540
+ pass
1541
+
1542
+ event_node.timeout = event_element.get('timeout')
1543
+
1544
+ # Parse event handler body
1545
+ for child in event_element:
1546
+ statement = self._parse_statement(child)
1547
+ if statement:
1548
+ event_node.add_statement(statement)
1549
+
1550
+ return event_node
1551
+