quantum-framework 0.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,734 @@
1
+ """
2
+ Quantum HTML Renderer
3
+
4
+ Converts executed AST to HTML string output.
5
+ Handles databinding, loops, conditionals, and HTML passthrough.
6
+ """
7
+
8
+ import html
9
+ import json
10
+ import logging
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, List
15
+
16
+ # Fix imports
17
+
18
+ from quantum.core.ast_nodes import (
19
+ QuantumNode, ComponentNode, HTMLNode, TextNode, DocTypeNode,
20
+ CommentNode, QueryNode, ComponentCallNode, ImportNode
21
+ )
22
+ from quantum.core.expression_diagnostics import is_absent_scope, report_unresolved
23
+ from quantum.core.expressions import (
24
+ ExpressionEvaluator, ExpressionError, UndefinedError, is_regex_quantifier,
25
+ )
26
+ from quantum.core.features.conditionals.src.ast_node import IfNode
27
+ from quantum.core.features.loops.src.ast_node import LoopNode
28
+ from quantum.core.features.state_management.src.ast_node import SetNode
29
+ from quantum.runtime.execution_context import ExecutionContext
30
+ from quantum.runtime.conditions import evaluate_condition
31
+ from quantum.core.ast_nodes import ActionNode
32
+ from quantum.runtime.ui_page_renderer import UIPageRenderer, is_ui_node
33
+ from quantum.core.xml_lines import tag_error
34
+
35
+ logger = logging.getLogger('quantum.renderer')
36
+
37
+
38
+ class HTMLRenderer:
39
+ """
40
+ Renders Quantum AST to HTML string.
41
+
42
+ Phase 1: Server-side rendering only
43
+ Phase 3: Will support hydration markers for client-side interactivity
44
+
45
+ Handles:
46
+ - HTML elements passthrough
47
+ - Text with databinding {variable}
48
+ - Quantum tags are already executed (loops expanded, conditions evaluated)
49
+ - XSS protection via HTML escaping
50
+ """
51
+
52
+ # Tags whose text content should NOT be processed by databinding or HTML-escaped
53
+ RAW_CONTENT_TAGS = {'style', 'script'}
54
+
55
+ def __init__(self, context: ExecutionContext, components_dir: str = "./components",
56
+ function_resolver=None, config=None):
57
+ """
58
+ Initialize renderer with execution context.
59
+
60
+ Args:
61
+ context: ExecutionContext with all variables and query results
62
+ components_dir: Directory where component files are located (Phase 2)
63
+ function_resolver: the runtime's lookup for the component's
64
+ q:functions (FN-3). Without it `<p>{dobro(2)}</p>` rendered
65
+ the literal text: only q: attributes could call a function.
66
+ """
67
+ self.context = context
68
+ self.components_dir = components_dir
69
+ # COMP-4: child components run with the page's configuration
70
+ # (datasources, services); None means no configuration.
71
+ self.config = config
72
+ # q:import of the component being rendered: name -> (component, from)
73
+ self.imports = {}
74
+ self._raw_mode = False
75
+ # Shared with ComponentRuntime: one evaluator, one grammar, so a
76
+ # {expression} means the same thing in the execute pass and the
77
+ # render pass. They used to be two hand-rolled implementations.
78
+ self._expressions = ExpressionEvaluator(function_resolver=function_resolver)
79
+
80
+ # Lazy-load composer (Phase 2)
81
+ self._composer = None
82
+ self._resolver = None
83
+
84
+
85
+ def render(self, node: QuantumNode) -> str:
86
+ """Render one node; a failure names the line of the innermost node (DEV-2)."""
87
+ try:
88
+ return self._render_node(node)
89
+ except Exception as exc:
90
+ tag_error(exc, node)
91
+ raise
92
+
93
+ def _render_node(self, node: QuantumNode) -> str:
94
+ """
95
+ Main render dispatch method.
96
+
97
+ Args:
98
+ node: AST node to render
99
+
100
+ Returns:
101
+ HTML string
102
+ """
103
+
104
+ if isinstance(node, HTMLNode):
105
+ return self._render_html_node(node)
106
+
107
+ elif is_ui_node(node):
108
+ return self._render_ui(node)
109
+
110
+ elif isinstance(node, TextNode):
111
+ return self._render_text_node(node)
112
+
113
+ elif isinstance(node, DocTypeNode):
114
+ return self._render_doctype(node)
115
+
116
+ elif isinstance(node, CommentNode):
117
+ return self._render_comment(node)
118
+
119
+ elif isinstance(node, ComponentNode):
120
+ # UI-1: the page's actions, which ui:* events post to. The first
121
+ # component rendered is the page; a called component posts to it.
122
+ if not hasattr(self, '_ui_actions'):
123
+ self._ui_actions = [s.name for s in node.statements if isinstance(s, ActionNode)]
124
+ # UI-9: a ui:form takes its fields' rules from the action it posts to.
125
+ self._ui_action_nodes = {s.name: s for s in node.statements if isinstance(s, ActionNode)}
126
+ self.imports = {
127
+ imp.alias or imp.component: (imp.component, imp.from_path)
128
+ for imp in node.statements
129
+ if isinstance(imp, ImportNode) and imp.component
130
+ }
131
+ return self._render_component(node)
132
+
133
+ # Component Composition (Phase 2)
134
+ elif isinstance(node, ComponentCallNode):
135
+ return self._render_component_call(node)
136
+
137
+ elif isinstance(node, ImportNode):
138
+ # Imports are processed at component load time, not render time
139
+ return ''
140
+
141
+ elif type(node).__name__ == "RenderedHTML":
142
+ # Slot content the parent already rendered (component_composer)
143
+ return node.html
144
+
145
+ elif isinstance(node, LoopNode):
146
+ return self._render_loop(node)
147
+
148
+ elif isinstance(node, IfNode):
149
+ return self._render_if(node)
150
+
151
+ # SetNode and QueryNode are executed during runtime, not rendered
152
+ elif isinstance(node, (SetNode, QueryNode)):
153
+ return ''
154
+
155
+ else:
156
+ # Unknown node type - skip (includes QuantumReturn and others)
157
+ return ''
158
+
159
+
160
+ def render_all(self, nodes: List[QuantumNode]) -> str:
161
+ """
162
+ Render list of nodes and concatenate results.
163
+
164
+ Args:
165
+ nodes: List of AST nodes
166
+
167
+ Returns:
168
+ Concatenated HTML string
169
+ """
170
+ return ''.join(self.render(node) for node in nodes)
171
+
172
+
173
+ def _render_html_node(self, node: HTMLNode) -> str:
174
+ """
175
+ Render HTML element with attributes and children.
176
+
177
+ Example:
178
+ HTMLNode(tag='div', attributes={'class': 'container'}, children=[...])
179
+ → <div class="container">...</div>
180
+
181
+ Args:
182
+ node: HTMLNode to render
183
+
184
+ Returns:
185
+ HTML string
186
+ """
187
+ # Build opening tag
188
+ tag_parts = [f'<{node.tag}']
189
+
190
+ # Add attributes with databinding applied
191
+ if node.attributes:
192
+ for key, value in node.attributes.items():
193
+ # Apply databinding to attribute value
194
+ processed_value = self._apply_databinding(value)
195
+ # Escape for HTML attribute safety (prevent XSS)
196
+ escaped_value = html.escape(str(processed_value), quote=True)
197
+ tag_parts.append(f'{key}="{escaped_value}"')
198
+
199
+ opening_tag = ' '.join(tag_parts)
200
+
201
+ # Self-closing tags (void elements)
202
+ if node.self_closing:
203
+ return opening_tag + ' />'
204
+
205
+ # Regular tags with children
206
+ opening_tag += '>'
207
+
208
+ # Style/script tags: render children as raw text (no databinding, no escaping)
209
+ if node.tag in self.RAW_CONTENT_TAGS:
210
+ prev_raw = self._raw_mode
211
+ self._raw_mode = True
212
+ children_html = self.render_all(node.children)
213
+ self._raw_mode = prev_raw
214
+ else:
215
+ children_html = self.render_all(node.children)
216
+
217
+ closing_tag = f'</{node.tag}>'
218
+
219
+ return opening_tag + children_html + closing_tag
220
+
221
+
222
+ def _render_text_node(self, node: TextNode) -> str:
223
+ """
224
+ Render text content with databinding applied.
225
+
226
+ Example:
227
+ TextNode("Hello {user.name}!") with context['user']['name'] = 'John'
228
+ → "Hello John!"
229
+
230
+ Args:
231
+ node: TextNode to render
232
+
233
+ Returns:
234
+ Text string with databinding applied and HTML escaped
235
+ """
236
+
237
+ text = node.content
238
+
239
+ # Inside <style>/<script> tags: return raw content without databinding or escaping
240
+ if self._raw_mode:
241
+ return text
242
+
243
+ # Apply databinding if needed
244
+ if node.has_databinding:
245
+ text = self._apply_databinding(text)
246
+
247
+ # HTML escape to prevent XSS
248
+ # NOTE: This means you can't inject raw HTML via variables (security feature)
249
+ # Guard against non-string values (e.g., bound methods from getattr)
250
+ if callable(text):
251
+ text = ''
252
+ return html.escape(str(text) if text is not None else '')
253
+
254
+
255
+ def _render_doctype(self, node: DocTypeNode) -> str:
256
+ """
257
+ Render DOCTYPE declaration.
258
+
259
+ Example:
260
+ DocTypeNode("html") → "<!DOCTYPE html>"
261
+
262
+ Args:
263
+ node: DocTypeNode to render
264
+
265
+ Returns:
266
+ DOCTYPE string
267
+ """
268
+ return f'<!DOCTYPE {node.value}>'
269
+
270
+
271
+ def _render_loop(self, node: LoopNode) -> str:
272
+ """
273
+ Render q:loop by iterating over items and rendering body for each.
274
+
275
+ Args:
276
+ node: LoopNode to render
277
+
278
+ Returns:
279
+ Concatenated HTML from all loop iterations
280
+ """
281
+ result = []
282
+
283
+ # Get the items to iterate over
284
+ items = self._get_loop_items(node)
285
+
286
+ if not items:
287
+ return ''
288
+
289
+ # Save current context state
290
+ original_vars = self.context.local_vars.copy()
291
+
292
+ try:
293
+ for index, item in enumerate(items):
294
+ # Set loop variable in context
295
+ self.context.set_variable(node.var_name, item, scope="local")
296
+
297
+ # For query loops, set dotted variables (e.g., task.title, task.status)
298
+ if node.loop_type == 'query' and isinstance(item, dict):
299
+ for field_name, field_value in item.items():
300
+ dotted_key = f"{node.var_name}.{field_name}"
301
+ self.context.set_variable(dotted_key, field_value, scope="local")
302
+
303
+ # Set index if specified
304
+ if node.index_name:
305
+ self.context.set_variable(node.index_name, index, scope="local")
306
+
307
+ # Render loop body
308
+ for child in node.body:
309
+ result.append(self.render(child))
310
+
311
+ finally:
312
+ # Restore original context
313
+ self.context.local_vars = original_vars
314
+
315
+ return ''.join(result)
316
+
317
+ def _get_loop_items(self, node: LoopNode) -> list:
318
+ """Get items to iterate over from loop node."""
319
+ if node.loop_type == 'array':
320
+ # Get items expression
321
+ items_expr = node.items
322
+ if not items_expr:
323
+ return []
324
+
325
+ # Apply databinding to resolve variable reference
326
+ if '{' in items_expr and '}' in items_expr:
327
+ resolved = self._apply_databinding(items_expr)
328
+ if isinstance(resolved, list):
329
+ return resolved
330
+ elif isinstance(resolved, str):
331
+ # Try to parse as JSON
332
+ try:
333
+ import json
334
+ return json.loads(resolved)
335
+ except Exception as exc:
336
+ # A loop that renders NOTHING is indistinguishable from
337
+ # an empty collection, so an unresolvable items= used
338
+ # to look like "no results" on the page.
339
+ logger.warning(
340
+ "q:loop items=%r is not a list and not JSON (%s); "
341
+ "rendering zero rows", items_expr, exc
342
+ )
343
+ return []
344
+ logger.warning(
345
+ "q:loop items=%r did not resolve to a list; rendering zero rows",
346
+ items_expr
347
+ )
348
+ return []
349
+
350
+ elif node.loop_type == 'range':
351
+ # Generate range
352
+ try:
353
+ start = int(node.from_value) if node.from_value else 1
354
+ end = int(node.to_value) if node.to_value else 10
355
+ step = node.step_value if node.step_value else 1
356
+ return list(range(start, end + 1, step))
357
+ except Exception as exc:
358
+ # from=/to= that did not resolve (a databinding placeholder,
359
+ # a typo) silently produced an empty range.
360
+ logger.warning(
361
+ "q:loop range from=%r to=%r step=%r is not usable (%s); "
362
+ "rendering zero rows",
363
+ node.from_value, node.to_value, node.step_value, exc
364
+ )
365
+ return []
366
+
367
+ elif node.loop_type == 'query':
368
+ # Query loop - resolve items from query result variable
369
+ items_expr = node.items
370
+ if not items_expr:
371
+ # Shorthand syntax: query_name is in var_name
372
+ query_name = getattr(node, 'query_name', node.var_name)
373
+ try:
374
+ data = self.context.get_variable(query_name)
375
+ if isinstance(data, list):
376
+ return data
377
+ logger.warning(
378
+ "q:loop query=%r resolved to %s, not a list; "
379
+ "rendering zero rows", query_name, type(data).__name__
380
+ )
381
+ except Exception:
382
+ logger.warning(
383
+ "q:loop query=%r is not defined — did the q:query run, "
384
+ "and is the name spelled the same? Rendering zero rows.",
385
+ query_name
386
+ )
387
+ return []
388
+
389
+ # Traditional syntax: items="{tasks}"
390
+ if '{' in items_expr and '}' in items_expr:
391
+ resolved = self._apply_databinding(items_expr)
392
+ if isinstance(resolved, list):
393
+ return resolved
394
+ return []
395
+
396
+ elif node.loop_type == 'list':
397
+ # Split by delimiter
398
+ items_expr = node.items
399
+ if not items_expr:
400
+ return []
401
+ resolved = self._apply_databinding(items_expr) if '{' in items_expr else items_expr
402
+ delimiter = node.delimiter or ','
403
+ return [item.strip() for item in str(resolved).split(delimiter)]
404
+
405
+ return []
406
+
407
+ def _render_if(self, node: IfNode) -> str:
408
+ """
409
+ Render q:if by evaluating condition and rendering appropriate branch.
410
+
411
+ Args:
412
+ node: IfNode to render
413
+
414
+ Returns:
415
+ HTML from the matching branch
416
+ """
417
+ # Evaluate main condition
418
+ if self._evaluate_condition(node.condition):
419
+ return self.render_all(node.if_body)
420
+
421
+ # Check elseif branches.
422
+ #
423
+ # The parser stores these as DICTS — {"condition": ..., "body": ...} —
424
+ # and IfExecutor reads them that way. This pass read them as objects
425
+ # (elseif.condition), so any page using q:elseif died with
426
+ # "'dict' object has no attribute 'condition'" during render. The two
427
+ # passes disagreed about the same structure and only one was ever run
428
+ # in a test. Both shapes are accepted here so a future change to
429
+ # either side cannot resurrect the crash.
430
+ for elseif in (node.elseif_blocks or []):
431
+ if isinstance(elseif, dict):
432
+ cond, body = elseif.get("condition"), elseif.get("body")
433
+ else:
434
+ cond, body = getattr(elseif, "condition", None), getattr(elseif, "body", None)
435
+ if cond is not None and self._evaluate_condition(cond):
436
+ return self.render_all(body or [])
437
+
438
+ # Else branch
439
+ if node.else_body:
440
+ return self.render_all(node.else_body)
441
+
442
+ return ''
443
+
444
+ def _render_ui(self, node) -> str:
445
+ """UI-1: a ui:* element of the page, drawn with the page's runtime."""
446
+ # The CSS goes before the first ui:* element, once — marked before
447
+ # drawing, since the subtree can reach _render_ui again (q:loop).
448
+ css = ''
449
+ if not getattr(self, '_ui_css_done', False):
450
+ self._ui_css_done = True
451
+ css = '<style data-quantum-ui>\n' + UIPageRenderer.css() + '\n</style>\n'
452
+ return css + UIPageRenderer(self, getattr(self, '_ui_actions', [])).render(node)
453
+
454
+ def _evaluate_condition(self, condition: str) -> bool:
455
+ """A condition by the language's rules — see quantum/runtime/conditions.py."""
456
+ return evaluate_condition(self._expressions, condition, self.context.get_all_variables())
457
+
458
+ def _render_comment(self, node: CommentNode) -> str:
459
+ """
460
+ Render HTML comment.
461
+
462
+ Example:
463
+ CommentNode("This is a comment") → "<!-- This is a comment -->"
464
+
465
+ Args:
466
+ node: CommentNode to render
467
+
468
+ Returns:
469
+ HTML comment string
470
+ """
471
+ return f'<!-- {node.content} -->'
472
+
473
+
474
+ # Um handler inline: onclick="salvar()", onchange="atualizar(this)".
475
+ _INLINE_HANDLER = re.compile(
476
+ r'\son[a-z]+\s*=\s*"([^"]*)"|\son[a-z]+\s*=\s*\'([^\']*)\'',
477
+ re.IGNORECASE)
478
+ # O nome chamado dentro dele.
479
+ _CALLED_NAME = re.compile(r'\b([A-Za-z_$][\w$]*)\s*\(')
480
+
481
+ def _render_component(self, node: ComponentNode) -> str:
482
+ """
483
+ Render entire component body.
484
+
485
+ Args:
486
+ node: ComponentNode to render
487
+
488
+ Returns:
489
+ HTML string of component output
490
+ """
491
+ html = self.render_all(node.statements)
492
+ script = self._explain_server_side_handlers(node, html)
493
+ if not script:
494
+ return html
495
+ # Dentro do documento quando ha um: depois de `</html>` o navegador
496
+ # reposiciona o script sozinho, mas o HTML deixa de ser valido e
497
+ # qualquer ferramenta que leia a saida tropeca.
498
+ for fecho in ('</body>', '</html>'):
499
+ posicao = html.rfind(fecho)
500
+ if posicao != -1:
501
+ return html[:posicao] + script + '\n' + html[posicao:]
502
+ return html + script
503
+
504
+ def _explain_server_side_handlers(self, node: ComponentNode,
505
+ html: str) -> str:
506
+ """Um `onclick="minhaFuncao()"` que aponta para uma q:function.
507
+
508
+ O corpo de uma q:function nunca vai para o cliente — ela roda no
509
+ servidor, com q:set/q:query/q:invoke, e nao ha rota para chama-la do
510
+ navegador (esse e o papel do q:action). Mesmo assim o HTML saia com
511
+ `onclick="minhaFuncao()"` e nenhuma definicao de `minhaFuncao` em
512
+ lugar nenhum: o clique dava `ReferenceError` no console do navegador
513
+ e absolutamente nada na tela, no log do servidor ou em qualquer
514
+ outro lugar. Quem escreveu nao tinha como saber.
515
+
516
+ Traduzir o corpo para JavaScript nao e possivel em geral — ele le o
517
+ banco. Entao o que se pode fazer com honestidade e nomear a falha:
518
+ um aviso no log do servidor, na hora de renderizar, e uma definicao
519
+ no cliente que levanta um erro dizendo o que fazer.
520
+ """
521
+ declaradas = {
522
+ f.name for f in getattr(node, 'functions', None) or []
523
+ if getattr(f, 'name', None)
524
+ }
525
+ if not declaradas or not html:
526
+ return ''
527
+
528
+ chamadas = set()
529
+ for m in self._INLINE_HANDLER.finditer(html):
530
+ corpo = m.group(1) if m.group(1) is not None else m.group(2)
531
+ for nome in self._CALLED_NAME.findall(corpo or ''):
532
+ if nome in declaradas:
533
+ chamadas.add(nome)
534
+
535
+ if not chamadas:
536
+ return ''
537
+
538
+ for nome in sorted(chamadas):
539
+ logger.warning(
540
+ "%s: an inline handler calls q:function %r, but a q:function "
541
+ "runs on the server and is never sent to the browser - the "
542
+ "click will raise ReferenceError. Use <q:action> for a "
543
+ "server-side handler, or write the JavaScript in <q:script>.",
544
+ getattr(node, 'name', '<component>'), nome
545
+ )
546
+
547
+ # A mensagem sai numa variavel antes de entrar na f-string. Ter a
548
+ # expressao quebrada em varias linhas DENTRO das chaves e PEP 701,
549
+ # valido so no Python 3.12+ — e o pyproject declara 3.11. Rodando so
550
+ # 3.12 aqui, isso passou despercebido ate o CI (matriz 3.11/3.12)
551
+ # recusar o modulo inteiro com SyntaxError na coleta.
552
+ linhas_js = []
553
+ for nome in sorted(chamadas):
554
+ recado = (
555
+ f'q:function "{nome}" runs on the server and is not '
556
+ f'available in the browser. Use <q:action> to handle this '
557
+ f'from the page, or define the handler in <q:script>.'
558
+ )
559
+ alvo = json.dumps(nome)
560
+ linhas_js.append(
561
+ f" window[{alvo}] = window[{alvo}] || "
562
+ f"function () {{ throw new Error({json.dumps(recado)}); }};"
563
+ )
564
+ corpo_js = '\n'.join(linhas_js)
565
+ # `</` vira `<\/`: nenhum texto dentro do script pode fechar a tag
566
+ # antes da hora.
567
+ corpo_js = corpo_js.replace('</', '<\\/')
568
+ return f"\n<script>\n{corpo_js}\n</script>"
569
+
570
+
571
+ def _apply_databinding(self, text: str) -> Any:
572
+ """
573
+ Replace {variable} and {expression} with actual values from context.
574
+
575
+ Examples:
576
+ "{name}" → "John"
577
+ "{user.email}" → "john@example.com"
578
+ "{price * quantity}" → "49.99"
579
+ "{items.length}" → "5"
580
+ "Total: ${product.price}" → "Total: $29.99"
581
+
582
+ Args:
583
+ text: Text with possible {expression} patterns
584
+
585
+ Returns:
586
+ - For pure expressions like "{items}": the actual value (list, dict, etc.)
587
+ - For mixed content like "Hello {name}": interpolated string
588
+ """
589
+ if not text:
590
+ return text
591
+
592
+ pattern = r'\{([^}]+)\}'
593
+
594
+ # Check if the ENTIRE text is just a single databinding expression
595
+ full_match = re.fullmatch(pattern, text.strip())
596
+ if full_match:
597
+ # Pure expression - return the actual value (not converted to string)
598
+ expression = full_match.group(1).strip()
599
+ try:
600
+ return self._evaluate_expression(expression)
601
+ except Exception as exc:
602
+ report_unresolved(expression, exc)
603
+ # If evaluation fails, return original placeholder
604
+ return text
605
+
606
+ # Mixed content (text + expressions) - need string interpolation
607
+ def replace_binding(match):
608
+ """Replace single {expression} match"""
609
+ expression = match.group(1).strip()
610
+
611
+ try:
612
+ # Evaluate expression from context
613
+ value = self._evaluate_expression(expression)
614
+ return str(value) if value is not None else ''
615
+
616
+ except Exception as exc:
617
+ report_unresolved(expression, exc)
618
+ # The placeholder, same as the pure-expression branch above.
619
+ # This used to emit '{ERROR: <expr>}', so the SAME broken
620
+ # expression rendered two different ways depending on whether
621
+ # it shared a text node with other content — and the marker
622
+ # leaked an internal variable name onto the user's page. The
623
+ # diagnosis belongs in the log, which now has it.
624
+ return match.group(0)
625
+
626
+ # Find and replace all {expression} patterns
627
+ result = re.sub(pattern, replace_binding, text)
628
+
629
+ return result
630
+
631
+
632
+ def _evaluate_expression(self, expression: str) -> Any:
633
+ """
634
+ Evaluate databinding expression from context.
635
+
636
+ Supports:
637
+ - Simple variables: user
638
+ - Nested properties: user.name, user.address.city
639
+ - Array access: items[0], products[2].name
640
+ - Array properties: items.length
641
+ - Simple arithmetic: price * quantity, count + 1
642
+ - Query results: products, products_result.recordCount
643
+
644
+ Args:
645
+ expression: Expression to evaluate
646
+
647
+ Returns:
648
+ Evaluated value
649
+
650
+ Raises:
651
+ Exception: If expression cannot be evaluated
652
+ """
653
+
654
+ # Regex quantifiers ({n}, {n,m}) are literal text, not expressions.
655
+ if is_regex_quantifier(expression):
656
+ raise ValueError(f"'{expression}' is a regex quantifier")
657
+
658
+ # Try to get from context directly (simple variable)
659
+ # get_variable() searches all scopes: local -> function -> component -> session -> parent
660
+ # This has to come first: get_all_variables() stores scoped variables
661
+ # under dotted KEYS ('session.user'), which is a lookup, not an
662
+ # attribute access, so the evaluator cannot see them.
663
+ try:
664
+ value = self.context.get_variable(expression)
665
+ return value
666
+ except:
667
+ pass # Variable not found, try other evaluation methods
668
+
669
+ # The shared evaluator (FRAMEWORK_PLAN.md Fase 2.1) handles arithmetic,
670
+ # comparison, indexing, attribute access and stdlib calls in one place.
671
+ #
672
+ # Four hand-rolled methods used to sit below this as a fallback —
673
+ # _evaluate_nested_property, _evaluate_array_access,
674
+ # _evaluate_comparison and _evaluate_arithmetic. Instrumenting the
675
+ # whole suite after the evaluator went in showed all four at zero
676
+ # calls, so they were deleted rather than kept as reassurance. Their
677
+ # `if value is not None: return value` shape was a bug source anyway:
678
+ # a property that legitimately held None fell through to the next
679
+ # guess instead of resolving.
680
+ try:
681
+ return self._expressions.evaluate(
682
+ expression, self.context.get_all_variables()
683
+ )
684
+ except ExpressionError:
685
+ # Absent scoped variables resolve to '', the same contract
686
+ # ComponentRuntime honours. Without this the runtime rendered ''
687
+ # and the renderer rendered the literal '{form.email}' for the
688
+ # same expression — one broken thing, two outputs, which is the
689
+ # bug this phase set out to remove.
690
+ if is_absent_scope(expression):
691
+ return ''
692
+ # Otherwise raised as-is. Wrapping it in "Cannot evaluate
693
+ # expression: <expr> (<reason>)" repeated the expression the
694
+ # caller already reports and buried the reason — the useful half —
695
+ # in parentheses at the end of a doubled sentence.
696
+ raise
697
+
698
+
699
+ # ============================================
700
+ # COMPONENT COMPOSITION RENDERING (Phase 2)
701
+ # ============================================
702
+
703
+ def _get_composer(self):
704
+ """Lazy-load component composer"""
705
+ if self._composer is None:
706
+ from quantum.runtime.component_resolver import ComponentResolver
707
+ from quantum.runtime.component_composer import ComponentComposer
708
+
709
+ self._resolver = ComponentResolver(self.components_dir)
710
+ self._composer = ComponentComposer(self._resolver)
711
+
712
+ return self._composer
713
+
714
+ def _render_component_call(self, node: ComponentCallNode) -> str:
715
+ """
716
+ Render component call (Phase 2).
717
+
718
+ Examples:
719
+ <Header title="Products" />
720
+ <Button label="Save" color="green" />
721
+ <Card title="Info">Content here</Card>
722
+
723
+ Args:
724
+ node: ComponentCallNode from AST
725
+
726
+ Returns:
727
+ Rendered HTML from child component
728
+ """
729
+
730
+ # COMP-1: a component that is not found, or fails, is an error. It
731
+ # used to become an HTML comment and the page answered 200 with the
732
+ # section silently missing — the database being down looked like a
733
+ # page with no results.
734
+ return self._get_composer().compose(node, self)