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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- 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 '<' / '&', 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
|
+
|