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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket Feature Module
|
|
3
|
+
|
|
4
|
+
Provides real-time bidirectional communication via WebSockets.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from quantum.core.features.websocket.src import (
|
|
8
|
+
WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
|
|
9
|
+
)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .ast_node import (
|
|
13
|
+
WebSocketNode,
|
|
14
|
+
WebSocketHandlerNode,
|
|
15
|
+
WebSocketSendNode,
|
|
16
|
+
WebSocketCloseNode
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'WebSocketNode',
|
|
21
|
+
'WebSocketHandlerNode',
|
|
22
|
+
'WebSocketSendNode',
|
|
23
|
+
'WebSocketCloseNode'
|
|
24
|
+
]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket AST Nodes
|
|
3
|
+
|
|
4
|
+
Defines the AST structure for WebSocket connections and messaging.
|
|
5
|
+
|
|
6
|
+
Classes:
|
|
7
|
+
WebSocketHandlerNode - Event handler (on-connect, on-message, etc.)
|
|
8
|
+
WebSocketSendNode - Send message action
|
|
9
|
+
WebSocketNode - Main WebSocket connection definition
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Optional, Any
|
|
14
|
+
|
|
15
|
+
# Import base class
|
|
16
|
+
try:
|
|
17
|
+
from quantum.core.ast_nodes import QuantumNode
|
|
18
|
+
except ImportError:
|
|
19
|
+
from ...ast_nodes import QuantumNode
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class WebSocketHandlerNode(QuantumNode):
|
|
24
|
+
"""
|
|
25
|
+
Event handler for WebSocket events.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
event: Event type (connect, message, error, close, open)
|
|
29
|
+
body: List of AST nodes to execute when event fires
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
<q:on-message>
|
|
33
|
+
<q:set name="lastMessage" value="{data}" />
|
|
34
|
+
<q:set name="messages" operation="append" value="{data}" />
|
|
35
|
+
</q:on-message>
|
|
36
|
+
"""
|
|
37
|
+
event: str = "" # connect, message, error, close, open
|
|
38
|
+
body: List[QuantumNode] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
"""Convert to dictionary."""
|
|
42
|
+
return {
|
|
43
|
+
"event": self.event,
|
|
44
|
+
"body": [b.to_dict() if hasattr(b, 'to_dict') else str(b) for b in self.body]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def validate(self) -> List[str]:
|
|
48
|
+
"""Validate the handler node."""
|
|
49
|
+
errors = []
|
|
50
|
+
valid_events = {"connect", "open", "message", "error", "close", "disconnect"}
|
|
51
|
+
if self.event not in valid_events:
|
|
52
|
+
errors.append(f"Invalid WebSocket event '{self.event}'. Valid: {valid_events}")
|
|
53
|
+
return errors
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class WebSocketSendNode(QuantumNode):
|
|
58
|
+
"""
|
|
59
|
+
Send a message through a WebSocket connection.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
connection: Name of the WebSocket connection to use
|
|
63
|
+
message: Message content to send (supports databinding)
|
|
64
|
+
type: Message type (text, json, binary)
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
<q:websocket-send connection="chat" message="{userInput}" type="json" />
|
|
68
|
+
"""
|
|
69
|
+
connection: str = ""
|
|
70
|
+
message: str = ""
|
|
71
|
+
type: str = "text" # text, json, binary
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict:
|
|
74
|
+
"""Convert to dictionary."""
|
|
75
|
+
return {
|
|
76
|
+
"connection": self.connection,
|
|
77
|
+
"message": self.message,
|
|
78
|
+
"type": self.type
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def validate(self) -> List[str]:
|
|
82
|
+
"""Validate the send node."""
|
|
83
|
+
errors = []
|
|
84
|
+
if not self.connection:
|
|
85
|
+
errors.append("WebSocketSendNode requires 'connection' attribute")
|
|
86
|
+
if not self.message:
|
|
87
|
+
errors.append("WebSocketSendNode requires 'message' attribute")
|
|
88
|
+
valid_types = {"text", "json", "binary"}
|
|
89
|
+
if self.type not in valid_types:
|
|
90
|
+
errors.append(f"Invalid message type '{self.type}'. Valid: {valid_types}")
|
|
91
|
+
return errors
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class WebSocketCloseNode(QuantumNode):
|
|
96
|
+
"""
|
|
97
|
+
Close a WebSocket connection.
|
|
98
|
+
|
|
99
|
+
Attributes:
|
|
100
|
+
connection: Name of the WebSocket connection to close
|
|
101
|
+
code: Close code (optional, default 1000 = normal closure)
|
|
102
|
+
reason: Close reason message (optional)
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
<q:websocket-close connection="chat" code="1000" reason="User logged out" />
|
|
106
|
+
"""
|
|
107
|
+
connection: str = ""
|
|
108
|
+
code: int = 1000
|
|
109
|
+
reason: str = ""
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict:
|
|
112
|
+
"""Convert to dictionary."""
|
|
113
|
+
return {
|
|
114
|
+
"connection": self.connection,
|
|
115
|
+
"code": self.code,
|
|
116
|
+
"reason": self.reason
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def validate(self) -> List[str]:
|
|
120
|
+
"""Validate the close node."""
|
|
121
|
+
errors = []
|
|
122
|
+
if not self.connection:
|
|
123
|
+
errors.append("WebSocketCloseNode requires 'connection' attribute")
|
|
124
|
+
return errors
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class WebSocketNode(QuantumNode):
|
|
129
|
+
"""
|
|
130
|
+
WebSocket connection definition.
|
|
131
|
+
|
|
132
|
+
Creates a persistent WebSocket connection with event handlers
|
|
133
|
+
for real-time bidirectional communication.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
name: Variable name for the connection (required)
|
|
137
|
+
url: WebSocket URL (ws:// or wss://)
|
|
138
|
+
auto_connect: Connect automatically on component mount (default: true)
|
|
139
|
+
reconnect: Auto-reconnect on disconnect (default: true)
|
|
140
|
+
reconnect_delay: Initial delay between reconnects in ms (default: 1000)
|
|
141
|
+
max_reconnects: Maximum reconnection attempts (default: 10)
|
|
142
|
+
heartbeat: Heartbeat interval in ms, 0 to disable (default: 30000)
|
|
143
|
+
protocols: WebSocket sub-protocols (optional, comma-separated)
|
|
144
|
+
handlers: Event handlers (on-connect, on-message, etc.)
|
|
145
|
+
|
|
146
|
+
Result Object:
|
|
147
|
+
{name}: Connection status object with:
|
|
148
|
+
- connected: bool
|
|
149
|
+
- url: str
|
|
150
|
+
- readyState: int (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)
|
|
151
|
+
- lastMessage: any
|
|
152
|
+
- error: str or None
|
|
153
|
+
- messageCount: int
|
|
154
|
+
- connectedAt: datetime or None
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
<q:websocket name="chat" url="wss://api.example.com/chat"
|
|
158
|
+
auto_connect="true" reconnect="true">
|
|
159
|
+
|
|
160
|
+
<q:on-connect>
|
|
161
|
+
<q:log level="info">Connected to chat server</q:log>
|
|
162
|
+
<q:set name="chatStatus" value="online" />
|
|
163
|
+
</q:on-connect>
|
|
164
|
+
|
|
165
|
+
<q:on-message>
|
|
166
|
+
<q:set name="messages" operation="append" value="{data}" />
|
|
167
|
+
</q:on-message>
|
|
168
|
+
|
|
169
|
+
<q:on-error>
|
|
170
|
+
<q:log level="error">WebSocket error: {error}</q:log>
|
|
171
|
+
</q:on-error>
|
|
172
|
+
|
|
173
|
+
<q:on-close>
|
|
174
|
+
<q:set name="chatStatus" value="offline" />
|
|
175
|
+
</q:on-close>
|
|
176
|
+
|
|
177
|
+
</q:websocket>
|
|
178
|
+
|
|
179
|
+
<!-- Send message -->
|
|
180
|
+
<q:action name="sendMessage">
|
|
181
|
+
<q:websocket-send connection="chat" message="{messageInput}" type="json" />
|
|
182
|
+
<q:set name="messageInput" value="" />
|
|
183
|
+
</q:action>
|
|
184
|
+
"""
|
|
185
|
+
name: str = ""
|
|
186
|
+
url: str = ""
|
|
187
|
+
auto_connect: bool = True
|
|
188
|
+
reconnect: bool = True
|
|
189
|
+
reconnect_delay: int = 1000 # ms
|
|
190
|
+
max_reconnects: int = 10
|
|
191
|
+
heartbeat: int = 30000 # ms, 0 to disable
|
|
192
|
+
protocols: str = "" # comma-separated sub-protocols
|
|
193
|
+
|
|
194
|
+
# Event handlers
|
|
195
|
+
handlers: List[WebSocketHandlerNode] = field(default_factory=list)
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> dict:
|
|
198
|
+
"""Convert to dictionary."""
|
|
199
|
+
return {
|
|
200
|
+
"name": self.name,
|
|
201
|
+
"url": self.url,
|
|
202
|
+
"auto_connect": self.auto_connect,
|
|
203
|
+
"reconnect": self.reconnect,
|
|
204
|
+
"reconnect_delay": self.reconnect_delay,
|
|
205
|
+
"max_reconnects": self.max_reconnects,
|
|
206
|
+
"heartbeat": self.heartbeat,
|
|
207
|
+
"protocols": self.protocols,
|
|
208
|
+
"handlers": [h.to_dict() for h in self.handlers]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
def validate(self) -> List[str]:
|
|
212
|
+
"""Validate the WebSocket node."""
|
|
213
|
+
errors = []
|
|
214
|
+
|
|
215
|
+
if not self.name:
|
|
216
|
+
errors.append("WebSocketNode requires 'name' attribute")
|
|
217
|
+
|
|
218
|
+
if not self.url:
|
|
219
|
+
errors.append("WebSocketNode requires 'url' attribute")
|
|
220
|
+
elif not self.url.startswith(("{", "ws://", "wss://")):
|
|
221
|
+
errors.append(f"WebSocket URL must start with ws:// or wss://, got: {self.url}")
|
|
222
|
+
|
|
223
|
+
if self.reconnect_delay < 0:
|
|
224
|
+
errors.append("reconnect_delay must be non-negative")
|
|
225
|
+
|
|
226
|
+
if self.max_reconnects < 0:
|
|
227
|
+
errors.append("max_reconnects must be non-negative")
|
|
228
|
+
|
|
229
|
+
if self.heartbeat < 0:
|
|
230
|
+
errors.append("heartbeat must be non-negative")
|
|
231
|
+
|
|
232
|
+
# Validate handlers
|
|
233
|
+
for handler in self.handlers:
|
|
234
|
+
errors.extend(handler.validate())
|
|
235
|
+
|
|
236
|
+
return errors
|
|
237
|
+
|
|
238
|
+
def get_handler(self, event: str) -> Optional[WebSocketHandlerNode]:
|
|
239
|
+
"""Get handler for specific event."""
|
|
240
|
+
for handler in self.handlers:
|
|
241
|
+
if handler.event == event:
|
|
242
|
+
return handler
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def has_handler(self, event: str) -> bool:
|
|
246
|
+
"""Check if handler exists for event."""
|
|
247
|
+
return self.get_handler(event) is not None
|
quantum/core/guards.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Page guards (AUTH-6): a q:if at the top of a page whose branch has a q:redirect.
|
|
2
|
+
|
|
3
|
+
<q:if condition="not session.authenticated">
|
|
4
|
+
<q:redirect url="/login" />
|
|
5
|
+
</q:if>
|
|
6
|
+
|
|
7
|
+
A q:action does not run the page's statements, so a guard written like this
|
|
8
|
+
used to protect only the GET — the POST ran the action without a session.
|
|
9
|
+
Guards now run before each action of the page as well as before the render.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from quantum.core.ast_nodes import RedirectNode
|
|
15
|
+
from quantum.core.features.conditionals.src.ast_node import IfNode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _branches(node: IfNode) -> List:
|
|
19
|
+
corpo = list(node.if_body)
|
|
20
|
+
for bloco in node.elseif_blocks:
|
|
21
|
+
corpo += list(bloco.get('body', []))
|
|
22
|
+
return corpo + list(node.else_body)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _has_redirect(nodes) -> bool:
|
|
26
|
+
return any(isinstance(n, RedirectNode) or (isinstance(n, IfNode) and _has_redirect(_branches(n)))
|
|
27
|
+
for n in nodes)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def page_guards(component) -> List[IfNode]:
|
|
31
|
+
"""The top-level q:if statements that can redirect, in document order."""
|
|
32
|
+
return [s for s in getattr(component, 'statements', [])
|
|
33
|
+
if isinstance(s, IfNode) and _has_redirect(_branches(s))]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def guard_conditions(node: IfNode) -> List[str]:
|
|
37
|
+
"""Every condition a guard evaluates, nested q:if included."""
|
|
38
|
+
condicoes = [node.condition] + [b.get('condition', '') for b in node.elseif_blocks]
|
|
39
|
+
for filho in _branches(node):
|
|
40
|
+
if isinstance(filho, IfNode):
|
|
41
|
+
condicoes += guard_conditions(filho)
|
|
42
|
+
return [c for c in condicoes if c]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mark_guards(component) -> None:
|
|
46
|
+
"""Flag each guard, nested q:if included, so its condition fails closed (AUTH-6)."""
|
|
47
|
+
def marcar(node):
|
|
48
|
+
node.is_guard = True
|
|
49
|
+
for filho in _branches(node):
|
|
50
|
+
if isinstance(filho, IfNode):
|
|
51
|
+
marcar(filho)
|
|
52
|
+
for guarda in page_guards(component):
|
|
53
|
+
marcar(guarda)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Accept the HTML people actually write.
|
|
3
|
+
|
|
4
|
+
A `.q` file is XML, but its body is HTML, and two things every HTML author
|
|
5
|
+
writes are invalid XML:
|
|
6
|
+
|
|
7
|
+
1. **Boolean attributes** — `<input required autofocus />`. HTML5 allows a
|
|
8
|
+
bare attribute name; XML demands `required="required"`.
|
|
9
|
+
2. **A bare ampersand in text** — `Forms & Actions`. Browsers tolerate it;
|
|
10
|
+
XML does not.
|
|
11
|
+
|
|
12
|
+
Ten of the forty-eight components SHIPPED WITH THE FRAMEWORK failed to parse
|
|
13
|
+
for exactly these two reasons, which meant they returned HTTP 400 — including
|
|
14
|
+
`login.q`, the login screen. The error message told the author to escape the
|
|
15
|
+
character, which is true and useless: the file was written the way HTML is
|
|
16
|
+
written.
|
|
17
|
+
|
|
18
|
+
This normalises both before the XML parser sees the source. It is deliberately
|
|
19
|
+
narrow:
|
|
20
|
+
|
|
21
|
+
- Only known boolean attribute names are expanded, and only inside a start
|
|
22
|
+
tag, never in text.
|
|
23
|
+
- Only an `&` that does not already begin a valid entity is escaped.
|
|
24
|
+
- `<script>`, `<style>`, comments and CDATA sections are left untouched —
|
|
25
|
+
their contents are not XML and rewriting them would corrupt real code.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
|
|
30
|
+
# HTML5 boolean attributes. A closed list: expanding an arbitrary bare word
|
|
31
|
+
# would turn a typo into a silently accepted attribute.
|
|
32
|
+
BOOLEAN_ATTRS = frozenset({
|
|
33
|
+
'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',
|
|
34
|
+
'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert',
|
|
35
|
+
'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate',
|
|
36
|
+
'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected',
|
|
37
|
+
'shadowrootclonable', 'shadowrootdelegatesfocus', 'shadowrootserializable',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
# HTML void elements. `<br>` and `<meta charset="utf-8">` are correct HTML and
|
|
41
|
+
# unclosed tags to an XML parser, which then reports the error far away — at
|
|
42
|
+
# the `</head>` or `</div>` that no longer matches. components/login.q and
|
|
43
|
+
# components/products.q both failed this way, and the reported line was
|
|
44
|
+
# nowhere near the real cause.
|
|
45
|
+
VOID_ELEMENTS = frozenset({
|
|
46
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
|
|
47
|
+
'meta', 'param', 'source', 'track', 'wbr',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# Regions whose contents must not be rewritten.
|
|
51
|
+
_PROTECTED = re.compile(
|
|
52
|
+
r'(<!\[CDATA\[.*?\]\]>'
|
|
53
|
+
r'|<!--.*?-->'
|
|
54
|
+
r'|<script\b[^>]*>.*?</script\s*>'
|
|
55
|
+
r'|<style\b[^>]*>.*?</style\s*>)',
|
|
56
|
+
re.DOTALL | re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Tags whose body is Python source, not markup. HTML gives <script> exactly
|
|
60
|
+
# this treatment; XML gives it to nothing, so a perfectly ordinary line like
|
|
61
|
+
#
|
|
62
|
+
# pattern = re.compile(r'<q:agent\s+([^>]*?)/?>')
|
|
63
|
+
#
|
|
64
|
+
# ended the parse with "not well-formed (invalid token)" pointing at the
|
|
65
|
+
# regex. components/admin/agents.q and components/admin/components.q were
|
|
66
|
+
# both unparseable — HTTP 400 from the server — for that reason, and the
|
|
67
|
+
# author's only recourse was to know to write CDATA by hand.
|
|
68
|
+
#
|
|
69
|
+
# `script` e `style` estao aqui pela MESMA razao. O HTML trata o corpo dos
|
|
70
|
+
# dois como texto cru; o XML nao trata nada assim. Um template literal com
|
|
71
|
+
# markup dentro —
|
|
72
|
+
#
|
|
73
|
+
# lista.innerHTML = todos.map(t => `<div class="item">${t.task}</div>`)
|
|
74
|
+
#
|
|
75
|
+
# — nao e XML: `${t.completed ? 'checked' : ''}` dentro de uma tag e um
|
|
76
|
+
# token invalido. components/islands_demo.q, entregue no repositorio,
|
|
77
|
+
# devolvia HTTP 400 por isso, e nao havia o que o autor pudesse fazer a nao
|
|
78
|
+
# ser escrever CDATA a mao em volta do proprio JavaScript.
|
|
79
|
+
#
|
|
80
|
+
# Medido antes de mudar: dos 47 arquivos .q com <script>/<style>, 46
|
|
81
|
+
# renderizam byte a byte igual e 1 (islands_demo) deixa de ser erro de
|
|
82
|
+
# parse. Nenhum piora.
|
|
83
|
+
_RAW_TEXT_TAGS = ('q:python', 'q:class', 'q:pyclass', 'script', 'style')
|
|
84
|
+
_RAW_TEXT = re.compile(
|
|
85
|
+
r'(<(' + '|'.join(_RAW_TEXT_TAGS) + r')\b[^>]*>)(.*?)(</\2\s*>)',
|
|
86
|
+
re.DOTALL | re.IGNORECASE,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def wrap_raw_text_tags(source: str) -> str:
|
|
91
|
+
"""Put the body of the scripting tags in CDATA when it needs it."""
|
|
92
|
+
def wrap(match):
|
|
93
|
+
open_tag, _name, body, close_tag = match.groups()
|
|
94
|
+
if '<' not in body and '&' not in body:
|
|
95
|
+
return match.group(0)
|
|
96
|
+
if '<![CDATA[' in body:
|
|
97
|
+
return match.group(0)
|
|
98
|
+
|
|
99
|
+
# Dentro de CDATA nada e decodificado, entao a entidade tem de ser
|
|
100
|
+
# desfeita AQUI. Sem isto, `if a < 10:` chegava ao exec() com o
|
|
101
|
+
# `<` literal — SyntaxError. Antes deste embrulho o parser XML
|
|
102
|
+
# decodificava; embrulhar tirou a decodificacao e quebrou 6 arquivos
|
|
103
|
+
# .q entregues no repositorio, que usavam a forma escapada porque era
|
|
104
|
+
# a unica que funcionava.
|
|
105
|
+
#
|
|
106
|
+
# `&` por ultimo: desfazer antes transformaria `&lt;` em `<`.
|
|
107
|
+
for entidade, caractere in (('<', '<'), ('>', '>'),
|
|
108
|
+
('"', '"'), (''', "'"),
|
|
109
|
+
('&', '&')):
|
|
110
|
+
body = body.replace(entidade, caractere)
|
|
111
|
+
# ]]> cannot appear inside a CDATA section; split it across two.
|
|
112
|
+
body = body.replace(']]>', ']]]]><![CDATA[>')
|
|
113
|
+
return f'{open_tag}<![CDATA[{body}]]>{close_tag}'
|
|
114
|
+
|
|
115
|
+
return _RAW_TEXT.sub(wrap, source)
|
|
116
|
+
|
|
117
|
+
# A start tag: <name ...attrs...> or <name ... />
|
|
118
|
+
#
|
|
119
|
+
# The attribute part must understand QUOTED VALUES, because `>` is legal inside
|
|
120
|
+
# one: `<q:validator expression="parseInt(value) >= 18" />`. A pattern of
|
|
121
|
+
# `[^<>]*` stopped at that `>` and mangled the tag — it silently turned
|
|
122
|
+
# `parseInt(value) >= 18` into `parseInt(value)>= 18`, which a test caught.
|
|
123
|
+
#
|
|
124
|
+
# The trailing `/` is NOT matched separately: a greedy attribute run would
|
|
125
|
+
# swallow it anyway (`<meta ... />` came back as `<meta ... / />`, breaking
|
|
126
|
+
# files that were previously fine). fix_tag strips it from the end instead,
|
|
127
|
+
# which leaves interior whitespace exactly as written.
|
|
128
|
+
_TAG = re.compile(
|
|
129
|
+
r'<([A-Za-z_][\w:.-]*)((?:\s(?:[^<>"\']|"[^"]*"|\'[^\']*\')*)?)>',
|
|
130
|
+
re.DOTALL,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# A bare attribute name sitting on its own inside a tag's attribute list.
|
|
134
|
+
_BARE_ATTR = re.compile(r'(^|\s)([A-Za-z_][\w:-]*)(?=\s|$)')
|
|
135
|
+
|
|
136
|
+
# `&` that already starts a well-formed entity.
|
|
137
|
+
_GOOD_ENTITY = re.compile(r'&(#\d+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*);')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _expand_boolean_attrs(attrs: str) -> str:
|
|
141
|
+
"""`required autofocus` -> `required="required" autofocus="autofocus"`."""
|
|
142
|
+
if not attrs.strip():
|
|
143
|
+
return attrs
|
|
144
|
+
|
|
145
|
+
# Split on quoted values so a bare word inside a value is never touched.
|
|
146
|
+
parts = re.split(r'("[^"]*"|\'[^\']*\')', attrs)
|
|
147
|
+
for i in range(0, len(parts), 2): # even indexes are outside quotes
|
|
148
|
+
segment = parts[i]
|
|
149
|
+
|
|
150
|
+
def repl(m):
|
|
151
|
+
lead, name = m.group(1), m.group(2)
|
|
152
|
+
# `foo=` is a normal attribute whose value follows; leave it.
|
|
153
|
+
after = segment[m.end():]
|
|
154
|
+
if after.lstrip().startswith('='):
|
|
155
|
+
return m.group(0)
|
|
156
|
+
if name.lower() in BOOLEAN_ATTRS:
|
|
157
|
+
return f'{lead}{name}="{name}"'
|
|
158
|
+
return m.group(0)
|
|
159
|
+
|
|
160
|
+
parts[i] = _BARE_ATTR.sub(repl, segment)
|
|
161
|
+
return ''.join(parts)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_QUOTED_VALUE = re.compile(r'"([^"]*)"|\'([^\']*)\'')
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _escape_bare_lt_in_values(attrs: str) -> str:
|
|
168
|
+
"""Escapa `<` dentro do VALOR de um atributo.
|
|
169
|
+
|
|
170
|
+
XML proibe `<` num valor de atributo; HTML tolerante e as pessoas nao.
|
|
171
|
+
Estas duas linhas sao naturais de escrever e nao parseavam:
|
|
172
|
+
|
|
173
|
+
<q:if condition="n < 0">
|
|
174
|
+
<q:return value="<h1>Oi</h1>" />
|
|
175
|
+
|
|
176
|
+
Dez exemplos da documentacao — inclusive no quick-start — eram exatamente
|
|
177
|
+
isso. `&` cru e atributo booleano ja eram tolerados aqui; `<` seguia a
|
|
178
|
+
mesma logica e faltava.
|
|
179
|
+
|
|
180
|
+
Escapar preserva o valor: o parser XML desfaz `<` na leitura, entao o
|
|
181
|
+
autor recebe de volta o `<` que escreveu. So o valor entre aspas e
|
|
182
|
+
tocado; o `<` que ABRE a tag esta fora deste trecho.
|
|
183
|
+
"""
|
|
184
|
+
def troca(m):
|
|
185
|
+
aspas = '"' if m.group(1) is not None else "'"
|
|
186
|
+
valor = m.group(1) if m.group(1) is not None else m.group(2)
|
|
187
|
+
return f'{aspas}{valor.replace("<", "<")}{aspas}'
|
|
188
|
+
|
|
189
|
+
return _QUOTED_VALUE.sub(troca, attrs)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# XML predefine CINCO entidades; HTML define mais de dois mil. ` ` — a
|
|
193
|
+
# mais usada de todas — nao existe em XML, entao um arquivo com ela dava
|
|
194
|
+
# "undefined entity" e HTTP 400. Este modulo existe para aceitar o HTML que as
|
|
195
|
+
# pessoas escrevem, e parava um passo antes da propria missao.
|
|
196
|
+
#
|
|
197
|
+
# A traducao e para referencia NUMERICA, que o XML aceita: ` ` -> ` `.
|
|
198
|
+
# O caractere final e o mesmo; so a forma de escrever muda.
|
|
199
|
+
def _named_entities_to_numeric(text: str) -> str:
|
|
200
|
+
if '&' not in text:
|
|
201
|
+
return text
|
|
202
|
+
|
|
203
|
+
from html.entities import html5
|
|
204
|
+
|
|
205
|
+
def troca(m):
|
|
206
|
+
nome = m.group(1)
|
|
207
|
+
# As cinco do XML ficam como estao: sao validas e legiveis.
|
|
208
|
+
if nome in ('lt', 'gt', 'amp', 'quot', 'apos'):
|
|
209
|
+
return m.group(0)
|
|
210
|
+
caractere = html5.get(nome + ';')
|
|
211
|
+
if caractere is None:
|
|
212
|
+
return m.group(0)
|
|
213
|
+
return ''.join(f'&#{ord(c)};' for c in caractere)
|
|
214
|
+
|
|
215
|
+
return re.sub(r'&([A-Za-z][A-Za-z0-9]*);', troca, text)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _escape_bare_ampersands(text: str) -> str:
|
|
219
|
+
"""`Forms & Actions` -> `Forms & Actions`, leaving real entities."""
|
|
220
|
+
if '&' not in text:
|
|
221
|
+
return text
|
|
222
|
+
out = []
|
|
223
|
+
i = 0
|
|
224
|
+
while i < len(text):
|
|
225
|
+
ch = text[i]
|
|
226
|
+
if ch != '&':
|
|
227
|
+
out.append(ch)
|
|
228
|
+
i += 1
|
|
229
|
+
continue
|
|
230
|
+
m = _GOOD_ENTITY.match(text, i)
|
|
231
|
+
if m:
|
|
232
|
+
out.append(m.group(0))
|
|
233
|
+
i = m.end()
|
|
234
|
+
else:
|
|
235
|
+
out.append('&')
|
|
236
|
+
i += 1
|
|
237
|
+
return ''.join(out)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def normalise_html(source: str) -> str:
|
|
241
|
+
"""Make ordinary HTML parseable as XML, without touching code regions."""
|
|
242
|
+
# First, because it CREATES protected regions: Python inside q:python is
|
|
243
|
+
# not markup, and wrapping it in CDATA both makes it parse and keeps the
|
|
244
|
+
# rewrites below away from it.
|
|
245
|
+
source = wrap_raw_text_tags(source)
|
|
246
|
+
|
|
247
|
+
pieces = _PROTECTED.split(source)
|
|
248
|
+
# split() with one capturing group alternates: text, protected, text, ...
|
|
249
|
+
for i in range(0, len(pieces), 2):
|
|
250
|
+
chunk = pieces[i]
|
|
251
|
+
|
|
252
|
+
# 1. boolean attributes, inside start tags only
|
|
253
|
+
def fix_tag(m):
|
|
254
|
+
name, attrs = m.group(1), m.group(2)
|
|
255
|
+
|
|
256
|
+
# Closing tags (</div>) and declarations are not start tags.
|
|
257
|
+
if name.startswith('/'):
|
|
258
|
+
return m.group(0)
|
|
259
|
+
|
|
260
|
+
# Peel a trailing self-closing slash off the attribute run, leaving
|
|
261
|
+
# every other character — including interior whitespace — alone.
|
|
262
|
+
self_closing = attrs.rstrip().endswith('/')
|
|
263
|
+
if self_closing:
|
|
264
|
+
stripped = attrs.rstrip()
|
|
265
|
+
attrs = stripped[:-1]
|
|
266
|
+
|
|
267
|
+
attrs = _expand_boolean_attrs(attrs)
|
|
268
|
+
# A bare & inside an attribute VALUE is invalid XML too, and query
|
|
269
|
+
# strings are full of them: href="/x?a=1&b=2".
|
|
270
|
+
attrs = _escape_bare_ampersands(attrs)
|
|
271
|
+
attrs = _named_entities_to_numeric(attrs)
|
|
272
|
+
# E um `<` cru dentro do valor tambem: `condition="n < 0"` e
|
|
273
|
+
# `value="<h1>Oi</h1>"` sao o que as pessoas escrevem — e o que a
|
|
274
|
+
# propria documentacao ensinava em dez exemplos que nao
|
|
275
|
+
# parseavam.
|
|
276
|
+
attrs = _escape_bare_lt_in_values(attrs)
|
|
277
|
+
|
|
278
|
+
# Close void elements the author left open (<br>, <meta ...>).
|
|
279
|
+
# `name.lower()` casava componente PascalCase com void
|
|
280
|
+
# element: <Link>, <Input>, <Source>, <Meta> viravam
|
|
281
|
+
# `<Link ... />conteudo</Link>` — auto-fechados, com a tag
|
|
282
|
+
# de fechamento orfa. Void element e conceito de HTML, e
|
|
283
|
+
# HTML se escreve em minusculas; chamada de componente e
|
|
284
|
+
# PascalCase por convencao e nao e HTML.
|
|
285
|
+
if self_closing or (name.islower() and name in VOID_ELEMENTS):
|
|
286
|
+
sep = '' if (not attrs or attrs.endswith(' ')) else ' '
|
|
287
|
+
return f'<{name}{attrs}{sep}/>'
|
|
288
|
+
return f'<{name}{attrs}>'
|
|
289
|
+
|
|
290
|
+
chunk = _TAG.sub(fix_tag, chunk)
|
|
291
|
+
|
|
292
|
+
# 2. bare ampersands, in the text between tags only
|
|
293
|
+
segments = re.split(r'(<[^>]*>)', chunk)
|
|
294
|
+
for j in range(0, len(segments), 2): # even indexes are text
|
|
295
|
+
segments[j] = _escape_bare_ampersands(segments[j])
|
|
296
|
+
segments[j] = _named_entities_to_numeric(segments[j])
|
|
297
|
+
pieces[i] = ''.join(segments)
|
|
298
|
+
|
|
299
|
+
return ''.join(pieces)
|