quantum-framework 0.9.0__py3-none-any.whl

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