cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT — live_automation.py: Real-time file change synchronization (v0.7.10 spec #13).
|
|
3
|
+
|
|
4
|
+
Monitors file changes and synchronizes them across:
|
|
5
|
+
- Editor tabs (auto-reload modified files)
|
|
6
|
+
- Preview panel (trigger hot reload)
|
|
7
|
+
- File explorer (refresh tree)
|
|
8
|
+
- AI activity (notify of changes)
|
|
9
|
+
|
|
10
|
+
Architecture:
|
|
11
|
+
FS Watcher -> LiveAutomationManager -> [Editor, Preview, Explorer]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
print("This is a library file and is not meant to be run directly.")
|
|
16
|
+
import sys
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from typing import Callable, Optional, Set
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class FileChangeEvent:
|
|
28
|
+
"""Represents a file system change event."""
|
|
29
|
+
path: str
|
|
30
|
+
change_type: str # "created", "modified", "deleted", "renamed"
|
|
31
|
+
timestamp: float = field(default_factory=time.time)
|
|
32
|
+
is_web_file: bool = False
|
|
33
|
+
is_code_file: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LiveAutomationManager:
|
|
37
|
+
"""Manages real-time file synchronization across CAT components.
|
|
38
|
+
|
|
39
|
+
This class coordinates file change events and triggers appropriate
|
|
40
|
+
updates in the editor, preview, and explorer.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# File extensions that trigger web preview updates
|
|
44
|
+
WEB_EXTENSIONS = {
|
|
45
|
+
".html", ".htm", ".css", ".js", ".mjs", ".json", ".svg",
|
|
46
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp",
|
|
47
|
+
".woff", ".woff2", ".ttf", ".otf",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# File extensions that are code files (for editor updates)
|
|
51
|
+
CODE_EXTENSIONS = {
|
|
52
|
+
".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".scss",
|
|
53
|
+
".json", ".yaml", ".yml", ".toml", ".md", ".rs", ".go", ".java",
|
|
54
|
+
".xml", ".sql", ".sh", ".bash",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def __init__(self, root: str):
|
|
58
|
+
self.root = os.path.abspath(root)
|
|
59
|
+
self._callbacks = {
|
|
60
|
+
"editor": [],
|
|
61
|
+
"preview": [],
|
|
62
|
+
"explorer": [],
|
|
63
|
+
"activity": [],
|
|
64
|
+
}
|
|
65
|
+
self._debounce_timers = {}
|
|
66
|
+
self._lock = threading.Lock()
|
|
67
|
+
self._pending_events = {}
|
|
68
|
+
self._debounce_interval = 0.15 # 150ms debounce
|
|
69
|
+
|
|
70
|
+
def register_callback(self, component: str, callback: Callable):
|
|
71
|
+
"""Register a callback for a specific component type."""
|
|
72
|
+
if component in self._callbacks:
|
|
73
|
+
self._callbacks[component].append(callback)
|
|
74
|
+
|
|
75
|
+
def on_file_changed(self, path: str, change_type: str = "modified"):
|
|
76
|
+
"""Handle a file change event with debouncing."""
|
|
77
|
+
path = os.path.abspath(path)
|
|
78
|
+
event = FileChangeEvent(
|
|
79
|
+
path=path,
|
|
80
|
+
change_type=change_type,
|
|
81
|
+
is_web_file=self._is_web_file(path),
|
|
82
|
+
is_code_file=self._is_code_file(path),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
with self._lock:
|
|
86
|
+
# Debounce rapid changes to the same file
|
|
87
|
+
if path in self._debounce_timers:
|
|
88
|
+
self._debounce_timers[path].cancel()
|
|
89
|
+
|
|
90
|
+
self._pending_events[path] = event
|
|
91
|
+
timer = threading.Timer(
|
|
92
|
+
self._debounce_interval,
|
|
93
|
+
self._process_event,
|
|
94
|
+
args=(path,)
|
|
95
|
+
)
|
|
96
|
+
timer.daemon = True
|
|
97
|
+
self._debounce_timers[path] = timer
|
|
98
|
+
timer.start()
|
|
99
|
+
|
|
100
|
+
def _process_event(self, path: str):
|
|
101
|
+
"""Process a debounced file change event."""
|
|
102
|
+
with self._lock:
|
|
103
|
+
event = self._pending_events.pop(path, None)
|
|
104
|
+
self._debounce_timers.pop(path, None)
|
|
105
|
+
|
|
106
|
+
if event is None:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Notify appropriate components
|
|
110
|
+
if event.is_code_file:
|
|
111
|
+
self._notify("editor", event)
|
|
112
|
+
if event.is_web_file:
|
|
113
|
+
self._notify("preview", event)
|
|
114
|
+
self._notify("explorer", event)
|
|
115
|
+
self._notify("activity", event)
|
|
116
|
+
|
|
117
|
+
def _notify(self, component: str, event: FileChangeEvent):
|
|
118
|
+
"""Notify all callbacks for a component type."""
|
|
119
|
+
for callback in self._callbacks.get(component, []):
|
|
120
|
+
try:
|
|
121
|
+
callback(event)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def _is_web_file(self, path: str) -> bool:
|
|
126
|
+
"""Check if a file is a web file (HTML, CSS, JS, etc.)."""
|
|
127
|
+
ext = os.path.splitext(path)[1].lower()
|
|
128
|
+
return ext in self.WEB_EXTENSIONS
|
|
129
|
+
|
|
130
|
+
def _is_code_file(self, path: str) -> bool:
|
|
131
|
+
"""Check if a file is a code file."""
|
|
132
|
+
ext = os.path.splitext(path)[1].lower()
|
|
133
|
+
return ext in self.CODE_EXTENSIONS
|
|
134
|
+
|
|
135
|
+
def sync_editor(self, editor, event: FileChangeEvent):
|
|
136
|
+
"""Synchronize editor with file changes."""
|
|
137
|
+
if event.change_type == "modified":
|
|
138
|
+
editor.reload_if_open(event.path)
|
|
139
|
+
elif event.change_type == "deleted":
|
|
140
|
+
# Close tab if file was deleted
|
|
141
|
+
if hasattr(editor, '_open_paths') and event.path in editor._open_paths:
|
|
142
|
+
tab_id = editor._open_paths[event.path]
|
|
143
|
+
editor.close_active(tab_id)
|
|
144
|
+
|
|
145
|
+
def sync_preview(self, preview_controller, event: FileChangeEvent):
|
|
146
|
+
"""Synchronize preview with file changes."""
|
|
147
|
+
if event.is_web_file and preview_controller:
|
|
148
|
+
preview_controller.notify_ai_wrote(event.path)
|
|
149
|
+
|
|
150
|
+
def sync_explorer(self, explorer, event: FileChangeEvent):
|
|
151
|
+
"""Synchronize explorer tree with file changes."""
|
|
152
|
+
if hasattr(explorer, 'refresh_tree'):
|
|
153
|
+
explorer.refresh_tree()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Global instance
|
|
157
|
+
_automation_manager = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_automation_manager(root: str = None) -> LiveAutomationManager:
|
|
161
|
+
"""Get or create the global automation manager."""
|
|
162
|
+
global _automation_manager
|
|
163
|
+
if _automation_manager is None or (root and _automation_manager.root != root):
|
|
164
|
+
_automation_manager = LiveAutomationManager(root or os.getcwd())
|
|
165
|
+
return _automation_manager
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notebook-style math rendering for the terminal: stacked fractions,
|
|
3
|
+
subscripts and superscripts using Unicode, so output never looks like
|
|
4
|
+
a raw inline "R0/R" — it looks handwritten, e.g.:
|
|
5
|
+
|
|
6
|
+
2.303
|
|
7
|
+
k = ───────────────── log(R0/R)
|
|
8
|
+
t
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re as _re
|
|
12
|
+
|
|
13
|
+
from . import theme
|
|
14
|
+
|
|
15
|
+
_MUL_RE = _re.compile(r"(?<!\*)\*(?!\*)")
|
|
16
|
+
_DIV_RE = _re.compile(r"(?<=[\w\)\]\u2080-\u2089])\s*/\s*(?=[\w\(\[])")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def mathify(s):
|
|
20
|
+
"""House style, applied to any plain (already-computed) display line:
|
|
21
|
+
never a literal '*' for multiplication (use 'x') and never a literal
|
|
22
|
+
'/' for division (use the fraction slash \u2044, which reads as a
|
|
23
|
+
proper divide rather than a path separator). Formula/substitution
|
|
24
|
+
rows should prefer compose()+frac_part() for a full stacked bar —
|
|
25
|
+
this is the lightweight fallback for single-line calculation text.
|
|
26
|
+
|
|
27
|
+
Also normalizes explicit ^ / _ notation and Greek-letter words to
|
|
28
|
+
their Unicode form. This only fires on explicit markup (a literal
|
|
29
|
+
``^``/``_``, or a whole Greek word), never on bare digits, so
|
|
30
|
+
ordinary numbers like "15" are always left alone.
|
|
31
|
+
"""
|
|
32
|
+
s = _MUL_RE.sub(" x ", str(s))
|
|
33
|
+
s = _DIV_RE.sub("\u2044", s)
|
|
34
|
+
s = convert_explicit_superscripts(s)
|
|
35
|
+
s = convert_explicit_subscripts(s)
|
|
36
|
+
s = convert_greek(s)
|
|
37
|
+
return s
|
|
38
|
+
|
|
39
|
+
SUB = {"0": "\u2080", "1": "\u2081", "2": "\u2082", "3": "\u2083", "4": "\u2084",
|
|
40
|
+
"5": "\u2085", "6": "\u2086", "7": "\u2087", "8": "\u2088", "9": "\u2089"}
|
|
41
|
+
SUP = {"0": "\u2070", "1": "\u00b9", "2": "\u00b2", "3": "\u00b3", "4": "\u2074",
|
|
42
|
+
"5": "\u2075", "6": "\u2076", "7": "\u2077", "8": "\u2078", "9": "\u2079",
|
|
43
|
+
"-": "\u207b", "+": "\u207a"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sub(n):
|
|
47
|
+
return "".join(SUB.get(c, c) for c in str(n))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sup(n):
|
|
51
|
+
return "".join(SUP.get(c, c) for c in str(n))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def frac_part(num, den):
|
|
55
|
+
"""Return a ('frac', num, den) tuple for use in compose()."""
|
|
56
|
+
return ("frac", str(num), str(den))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compose(parts, color=theme.TEXT):
|
|
60
|
+
"""
|
|
61
|
+
Compose a mixed list of plain strings and ('frac', num, den) tuples
|
|
62
|
+
into a 3-row, baseline-aligned block of text, colored uniformly.
|
|
63
|
+
"""
|
|
64
|
+
top_row, mid_row, bot_row = [], [], []
|
|
65
|
+
for p in parts:
|
|
66
|
+
if isinstance(p, tuple) and p[0] == "frac":
|
|
67
|
+
_, num, den = p
|
|
68
|
+
w = max(len(num), len(den))
|
|
69
|
+
top_row.append(" " + num.center(w) + " ")
|
|
70
|
+
mid_row.append(" " + ("\u2500" * w) + " ")
|
|
71
|
+
bot_row.append(" " + den.center(w) + " ")
|
|
72
|
+
else:
|
|
73
|
+
s = str(p)
|
|
74
|
+
top_row.append(" " * len(s))
|
|
75
|
+
mid_row.append(s)
|
|
76
|
+
bot_row.append(" " * len(s))
|
|
77
|
+
|
|
78
|
+
top = "".join(top_row).rstrip()
|
|
79
|
+
mid = "".join(mid_row).rstrip()
|
|
80
|
+
bot = "".join(bot_row).rstrip()
|
|
81
|
+
|
|
82
|
+
lines = [l for l in (top, mid, bot) if l.strip() != ""]
|
|
83
|
+
return "\n".join(theme.fg(l, color) for l in lines) if lines else ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------------- #
|
|
87
|
+
# Scientific rendering engine (v0.6.2) #
|
|
88
|
+
# #
|
|
89
|
+
# Everything below extends the module above without touching it: the #
|
|
90
|
+
# original mathify/sub/sup/frac_part/compose keep their exact old #
|
|
91
|
+
# behavior (mathify only gained two new, narrowly-scoped calls). The #
|
|
92
|
+
# goal of this section is a single entry point — render_math() — that #
|
|
93
|
+
# any AI-generated or user-facing text can be piped through so raw #
|
|
94
|
+
# Markdown/LaTeX markup never reaches the screen; textbook Unicode #
|
|
95
|
+
# comes out the other side instead. #
|
|
96
|
+
# --------------------------------------------------------------------- #
|
|
97
|
+
|
|
98
|
+
GREEK = {
|
|
99
|
+
"alpha": "\u03b1", "beta": "\u03b2", "gamma": "\u03b3", "delta": "\u0394",
|
|
100
|
+
"epsilon": "\u03b5", "zeta": "\u03b6", "eta": "\u03b7", "theta": "\u03b8",
|
|
101
|
+
"iota": "\u03b9", "kappa": "\u03ba", "lambda": "\u03bb", "mu": "\u03bc",
|
|
102
|
+
"nu": "\u03bd", "xi": "\u03be", "pi": "\u03c0", "rho": "\u03c1",
|
|
103
|
+
"sigma": "\u03c3", "tau": "\u03c4", "upsilon": "\u03c5", "phi": "\u03c6",
|
|
104
|
+
"chi": "\u03c7", "psi": "\u03c8", "omega": "\u03c9",
|
|
105
|
+
}
|
|
106
|
+
# Case-preserving alternates the spec calls out explicitly (e.g. capital
|
|
107
|
+
# Delta for change-in, lowercase everywhere else).
|
|
108
|
+
_GREEK_RE = _re.compile(
|
|
109
|
+
r"\\?(?<![A-Za-z])(" + "|".join(sorted(GREEK, key=len, reverse=True)) + r")(?![A-Za-z])",
|
|
110
|
+
_re.IGNORECASE,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def convert_greek(s):
|
|
115
|
+
"""Replace bare Greek-letter words (optionally LaTeX-escaped, e.g.
|
|
116
|
+
'delta' or '\\delta') with their Unicode symbol. Word-boundary
|
|
117
|
+
matched, so it never touches a word that merely contains one of
|
|
118
|
+
these as a substring."""
|
|
119
|
+
def _rep(m):
|
|
120
|
+
return GREEK[m.group(1).lower()]
|
|
121
|
+
return _GREEK_RE.sub(_rep, str(s))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_EXPL_SUP_BRACE_RE = _re.compile(r"\^\{([^{}]+)\}")
|
|
125
|
+
_EXPL_SUP_RE = _re.compile(r"\^(-?[0-9A-Za-z]+)")
|
|
126
|
+
_EXPL_SUB_BRACE_RE = _re.compile(r"_\{([^{}]+)\}")
|
|
127
|
+
_EXPL_SUB_RE = _re.compile(r"_(-?[0-9A-Za-z]+)")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def convert_explicit_superscripts(s):
|
|
131
|
+
"""Turn explicit ``x^2``, ``x^{12}``, ``10^-5`` into Unicode
|
|
132
|
+
superscripts. Only fires on a literal caret, so plain numbers are
|
|
133
|
+
never touched."""
|
|
134
|
+
s = _EXPL_SUP_BRACE_RE.sub(lambda m: sup(m.group(1)), str(s))
|
|
135
|
+
s = _EXPL_SUP_RE.sub(lambda m: sup(m.group(1)), s)
|
|
136
|
+
return s
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def convert_explicit_subscripts(s):
|
|
140
|
+
"""Turn explicit ``H_2``, ``psi_{100}`` into Unicode subscripts.
|
|
141
|
+
Only fires on a literal underscore."""
|
|
142
|
+
s = _EXPL_SUB_BRACE_RE.sub(lambda m: sub(m.group(1)), str(s))
|
|
143
|
+
s = _EXPL_SUB_RE.sub(lambda m: sub(m.group(1)), s)
|
|
144
|
+
return s
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
_SCI_NOTATION_RE = _re.compile(r"(?<![\w.])(\d+(?:\.\d+)?)[eE]([+-]?\d+)(?!\w)")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def convert_scientific_notation(s):
|
|
151
|
+
"""6.022e23 -> 6.022 x 10^23 (Unicode). Requires digits on both
|
|
152
|
+
sides of the 'e' so it can never fire on ordinary words."""
|
|
153
|
+
return _SCI_NOTATION_RE.sub(lambda m: f"{m.group(1)} \u00d7 10{sup(m.group(2))}", str(s))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_LATEX_SYMBOLS = {
|
|
157
|
+
r"\times": "\u00d7", r"\cdot": "\u00b7", r"\pm": "\u00b1", r"\mp": "\u2213",
|
|
158
|
+
r"\geq": "\u2265", r"\leq": "\u2264", r"\neq": "\u2260", r"\approx": "\u2248",
|
|
159
|
+
r"\infty": "\u221e", r"\rightarrow": "\u2192", r"\Rightarrow": "\u21d2",
|
|
160
|
+
r"\leftarrow": "\u2190", r"\to": "\u2192", r"\propto": "\u221d",
|
|
161
|
+
r"\partial": "\u2202", r"\nabla": "\u2207", r"\degree": "\u00b0",
|
|
162
|
+
r"\circ": "\u00b0", r"\perp": "\u22a5", r"\parallel": "\u2225",
|
|
163
|
+
r"\angle": "\u2220", r"\sim": "\u223c", r"\equiv": "\u2261",
|
|
164
|
+
# v0.7.2 roadmap additions: big operators (bounds already come for
|
|
165
|
+
# free — \sum_{i=1}^{n} still has the \sum replaced here, then the
|
|
166
|
+
# existing convert_explicit_sub/superscript passes below pick up
|
|
167
|
+
# the trailing _{i=1}/^{n} the same as they would on any other
|
|
168
|
+
# symbol) plus quantum-mechanics/set notation the spec calls out
|
|
169
|
+
# by name ("Quantum mechanics notation").
|
|
170
|
+
r"\sum": "\u2211", r"\prod": "\u220f", r"\int": "\u222b",
|
|
171
|
+
r"\iint": "\u222c", r"\iiint": "\u222d", r"\oint": "\u222e",
|
|
172
|
+
r"\hbar": "\u210f", r"\langle": "\u27e8", r"\rangle": "\u27e9",
|
|
173
|
+
r"\otimes": "\u2297", r"\oplus": "\u2295", r"\dagger": "\u2020",
|
|
174
|
+
r"\forall": "\u2200", r"\exists": "\u2203", r"\nexists": "\u2204",
|
|
175
|
+
r"\in": "\u2208", r"\notin": "\u2209", r"\subseteq": "\u2286",
|
|
176
|
+
r"\subset": "\u2282", r"\cup": "\u222a", r"\cap": "\u2229",
|
|
177
|
+
r"\emptyset": "\u2205", r"\wedge": "\u2227", r"\vee": "\u2228",
|
|
178
|
+
r"\neg": "\u00ac", r"\therefore": "\u2234", r"\because": "\u2235",
|
|
179
|
+
r"\ell": "\u2113", r"\Re": "\u211c", r"\Im": "\u2111",
|
|
180
|
+
r"\aleph": "\u2135",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _strip_latex_noise(s):
|
|
185
|
+
"""Replace common LaTeX macros with their Unicode glyph, and drop
|
|
186
|
+
the purely-cosmetic ones (\\left, \\right, \\, , \\; , \\quad,
|
|
187
|
+
stray $ / \\[ \\] delimiters) so nothing raw leaks through."""
|
|
188
|
+
for macro, glyph in _LATEX_SYMBOLS.items():
|
|
189
|
+
s = s.replace(macro, glyph)
|
|
190
|
+
for noise in (r"\left", r"\right", r"\quad", r"\qquad", r"\,", r"\;",
|
|
191
|
+
r"\!", "\\\n"):
|
|
192
|
+
s = s.replace(noise, "")
|
|
193
|
+
s = s.replace(r"\[", "").replace(r"\]", "")
|
|
194
|
+
s = _re.sub(r"(?<!\\)\$\$?", "", s)
|
|
195
|
+
return s
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _find_balanced(s, open_idx):
|
|
199
|
+
"""s[open_idx] must be '{'. Returns the index just past the
|
|
200
|
+
matching '}' (handling one level of nesting), or None."""
|
|
201
|
+
depth = 0
|
|
202
|
+
for i in range(open_idx, len(s)):
|
|
203
|
+
if s[i] == "{":
|
|
204
|
+
depth += 1
|
|
205
|
+
elif s[i] == "}":
|
|
206
|
+
depth -= 1
|
|
207
|
+
if depth == 0:
|
|
208
|
+
return i + 1
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def convert_sqrt(s):
|
|
213
|
+
"""\\sqrt{X} -> a Unicode radical with a combining overline across
|
|
214
|
+
X, e.g. \\sqrt{2KE/m} -> '\u221a(2KE/m)' with an overline on the
|
|
215
|
+
inside. Recurses so nested markup inside the radicand still gets
|
|
216
|
+
converted."""
|
|
217
|
+
s = str(s)
|
|
218
|
+
out, i = [], 0
|
|
219
|
+
while True:
|
|
220
|
+
j = s.find(r"\sqrt", i)
|
|
221
|
+
if j == -1:
|
|
222
|
+
out.append(s[i:])
|
|
223
|
+
break
|
|
224
|
+
out.append(s[i:j])
|
|
225
|
+
k = j + len(r"\sqrt")
|
|
226
|
+
if k < len(s) and s[k] == "{":
|
|
227
|
+
end = _find_balanced(s, k)
|
|
228
|
+
if end is None:
|
|
229
|
+
out.append(s[j:])
|
|
230
|
+
break
|
|
231
|
+
inner = render_inline(s[k + 1:end - 1])
|
|
232
|
+
out.append("\u221a\u0305" + "\u0305".join(inner) + "\u0305" if inner else "\u221a")
|
|
233
|
+
i = end
|
|
234
|
+
else:
|
|
235
|
+
out.append("\u221a")
|
|
236
|
+
i = k
|
|
237
|
+
return "".join(out)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def convert_fractions(s):
|
|
241
|
+
"""\\frac{A}{B} -> a real 3-row stacked-bar block (via compose),
|
|
242
|
+
dropped in as its own fenced code section so the Markdown renderer
|
|
243
|
+
preserves the line breaks and monospace alignment a stacked
|
|
244
|
+
fraction depends on. Handles nested \\frac inside numerator or
|
|
245
|
+
denominator by recursing first."""
|
|
246
|
+
s = str(s)
|
|
247
|
+
out, i = [], 0
|
|
248
|
+
while True:
|
|
249
|
+
j = s.find(r"\frac", i)
|
|
250
|
+
if j == -1:
|
|
251
|
+
out.append(s[i:])
|
|
252
|
+
break
|
|
253
|
+
out.append(s[i:j])
|
|
254
|
+
k = j + len(r"\frac")
|
|
255
|
+
if k < len(s) and s[k] == "{":
|
|
256
|
+
num_end = _find_balanced(s, k)
|
|
257
|
+
if num_end is None:
|
|
258
|
+
out.append(s[j:])
|
|
259
|
+
break
|
|
260
|
+
num = s[k + 1:num_end - 1]
|
|
261
|
+
den_start = num_end
|
|
262
|
+
if den_start < len(s) and s[den_start] == "{":
|
|
263
|
+
den_end = _find_balanced(s, den_start)
|
|
264
|
+
if den_end is None:
|
|
265
|
+
out.append(s[j:den_start])
|
|
266
|
+
i = den_start
|
|
267
|
+
continue
|
|
268
|
+
den = s[den_start + 1:den_end - 1]
|
|
269
|
+
block = compose([frac_part(render_inline(num), render_inline(den))])
|
|
270
|
+
out.append("\n```\n" + _strip_ansi(block) + "\n```\n")
|
|
271
|
+
i = den_end
|
|
272
|
+
continue
|
|
273
|
+
out.append(s[j:k])
|
|
274
|
+
i = k
|
|
275
|
+
return "".join(out)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
_ANSI_RE = _re.compile(r"\x1b\[[0-9;]*m")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _strip_ansi(s):
|
|
282
|
+
"""compose() colors its output with terminal ANSI codes for the
|
|
283
|
+
fallback CLI; inside a Markdown fenced block those codes would show
|
|
284
|
+
up as literal garbage, so strip them back to plain Unicode text."""
|
|
285
|
+
return _ANSI_RE.sub("", s)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def render_inline(s):
|
|
289
|
+
"""Apply every *inline-safe* conversion (no fraction blocks, which
|
|
290
|
+
span multiple lines) — Greek, explicit sup/sub, latex symbol noise,
|
|
291
|
+
scientific notation. Used both standalone and as the recursive
|
|
292
|
+
step inside sqrt/frac/matrix parsing."""
|
|
293
|
+
s = _strip_latex_noise(str(s))
|
|
294
|
+
s = convert_greek(s)
|
|
295
|
+
s = convert_explicit_superscripts(s)
|
|
296
|
+
s = convert_explicit_subscripts(s)
|
|
297
|
+
s = convert_scientific_notation(s)
|
|
298
|
+
return s
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
_MATRIX_ENV_RE = _re.compile(
|
|
302
|
+
r"\\begin\{(matrix|bmatrix|pmatrix|vmatrix)\}(.*?)\\end\{\1\}", _re.DOTALL
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def convert_matrices(s):
|
|
307
|
+
"""\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix} -> a bracket-
|
|
308
|
+
bordered Unicode grid, e.g.:
|
|
309
|
+
|
|
310
|
+
⎡a b⎤
|
|
311
|
+
⎣c d⎦
|
|
312
|
+
"""
|
|
313
|
+
def _rep(m):
|
|
314
|
+
env, body = m.group(1), m.group(2)
|
|
315
|
+
rows = [r.strip() for r in body.strip().split("\\\\") if r.strip()]
|
|
316
|
+
grid = [[render_inline(cell.strip()) for cell in row.split("&")] for row in rows]
|
|
317
|
+
if not grid:
|
|
318
|
+
return ""
|
|
319
|
+
widths = [max(len(row[c]) for row in grid) for c in range(len(grid[0]))]
|
|
320
|
+
lines = []
|
|
321
|
+
n = len(grid)
|
|
322
|
+
left_edges = {"matrix": ("", "", ""), "bmatrix": ("\u23a1", "\u23a2", "\u23a3"),
|
|
323
|
+
"pmatrix": ("\u239b", "\u239c", "\u239d"),
|
|
324
|
+
"vmatrix": ("\u2502", "\u2502", "\u2502")}
|
|
325
|
+
right_edges = {"matrix": ("", "", ""), "bmatrix": ("\u23a4", "\u23a5", "\u23a6"),
|
|
326
|
+
"pmatrix": ("\u239e", "\u239f", "\u23a0"),
|
|
327
|
+
"vmatrix": ("\u2502", "\u2502", "\u2502")}
|
|
328
|
+
lft, rgt = left_edges.get(env, ("", "", "")), right_edges.get(env, ("", "", ""))
|
|
329
|
+
for idx, row in enumerate(grid):
|
|
330
|
+
pos = 0 if n == 1 else (0 if idx == 0 else (2 if idx == n - 1 else 1))
|
|
331
|
+
cells = " ".join(cell.rjust(widths[c]) for c, cell in enumerate(row))
|
|
332
|
+
lines.append(f"{lft[pos]}{cells}{rgt[pos]}")
|
|
333
|
+
return "\n```\n" + "\n".join(lines) + "\n```\n"
|
|
334
|
+
|
|
335
|
+
return _MATRIX_ENV_RE.sub(_rep, str(s))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Conservative chemistry-formula matcher: 2+ Element[digits] groups,
|
|
339
|
+
# optionally followed by a trailing ionic charge like ^2- or a bare +/-.
|
|
340
|
+
_CHEM_TOKEN_RE = _re.compile(
|
|
341
|
+
r"\b((?:[A-Z][a-z]?\d*){2,}(?:\^?\d*[+\-])?)\b"
|
|
342
|
+
)
|
|
343
|
+
_CHEM_ELEMENT_RE = _re.compile(r"([A-Z][a-z]?)(\d+)")
|
|
344
|
+
_CHEM_CHARGE_CARET_RE = _re.compile(r"\^(\d*)([+\-])$")
|
|
345
|
+
_CHEM_CHARGE_BARE_RE = _re.compile(r"([+\-])$")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def chem_formula(token):
|
|
349
|
+
"""Render one chemistry token (e.g. 'SO4^2-', 'CaCO3', 'NH4+') with
|
|
350
|
+
Unicode subscripts for atom counts and a Unicode superscript for a
|
|
351
|
+
trailing ionic charge. A caret-prefixed digit (``^2-``) is the
|
|
352
|
+
charge magnitude; a bare trailing sign (``NH4+``) is charge 1, and
|
|
353
|
+
any digit right before it still belongs to the preceding element's
|
|
354
|
+
atom count, not the charge — so 'NH4+' reads N, H4, +1, never
|
|
355
|
+
N, H, charge 4. Meant to be called on a token already known to be
|
|
356
|
+
a formula, not blindly across arbitrary prose."""
|
|
357
|
+
charge = ""
|
|
358
|
+
m = _CHEM_CHARGE_CARET_RE.search(token)
|
|
359
|
+
if m:
|
|
360
|
+
n, sign = m.groups()
|
|
361
|
+
charge = sup((n or "") + sign)
|
|
362
|
+
token = token[:m.start()]
|
|
363
|
+
else:
|
|
364
|
+
m = _CHEM_CHARGE_BARE_RE.search(token)
|
|
365
|
+
if m:
|
|
366
|
+
charge = sup(m.group(1))
|
|
367
|
+
token = token[:m.start()]
|
|
368
|
+
body = _CHEM_ELEMENT_RE.sub(lambda m2: m2.group(1) + sub(m2.group(2)), token)
|
|
369
|
+
return body + charge
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def find_latex_errors(text):
|
|
373
|
+
"""Real (if intentionally narrow) error-highlighting pass — v0.7.2
|
|
374
|
+
roadmap's "Error highlighting" bullet. Checks for the mistakes that
|
|
375
|
+
are actually detectable from the text alone without a real LaTeX
|
|
376
|
+
parser: unbalanced braces after \\frac/\\sqrt, and an unclosed
|
|
377
|
+
\\begin{...} environment. Does NOT validate that a macro name is a
|
|
378
|
+
real LaTeX command (this file only ever converts a known allow-
|
|
379
|
+
list — see _LATEX_SYMBOLS/convert_* above — and silently leaves
|
|
380
|
+
anything else as plain text, which is the deliberately permissive
|
|
381
|
+
choice made throughout this module already: rendering "unknown
|
|
382
|
+
macro" warnings on every \\notarealcommand a user or AI might type
|
|
383
|
+
would be noisier than useful). Returns a list of short strings,
|
|
384
|
+
empty if nothing looks wrong."""
|
|
385
|
+
text = str(text)
|
|
386
|
+
errors = []
|
|
387
|
+
for macro in (r"\frac", r"\sqrt"):
|
|
388
|
+
i = 0
|
|
389
|
+
while True:
|
|
390
|
+
j = text.find(macro, i)
|
|
391
|
+
if j == -1:
|
|
392
|
+
break
|
|
393
|
+
k = j + len(macro)
|
|
394
|
+
if k >= len(text) or text[k] != "{":
|
|
395
|
+
i = k
|
|
396
|
+
continue
|
|
397
|
+
if _find_balanced(text, k) is None:
|
|
398
|
+
errors.append(f"Unbalanced {{}} after {macro}")
|
|
399
|
+
i = k + 1
|
|
400
|
+
for m in _re.finditer(r"\\begin\{([a-zA-Z*]+)\}", text):
|
|
401
|
+
env = m.group(1)
|
|
402
|
+
if f"\\end{{{env}}}" not in text[m.end():]:
|
|
403
|
+
errors.append(f"\\begin{{{env}}} has no matching \\end{{{env}}}")
|
|
404
|
+
return errors
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def render_math(text):
|
|
408
|
+
"""Master entry point: run a full block of AI/user-facing text
|
|
409
|
+
through the scientific rendering pipeline so LaTeX/Markdown math
|
|
410
|
+
markup never reaches the screen as raw text. Fenced code blocks
|
|
411
|
+
(real code, not math) are left completely untouched. Safe to call
|
|
412
|
+
on plain prose with no math in it at all — everything here only
|
|
413
|
+
fires on explicit markers (\\, ^, _, e-notation, a known Greek
|
|
414
|
+
word), so ordinary sentences pass through unchanged."""
|
|
415
|
+
text = str(text)
|
|
416
|
+
if "```" not in text:
|
|
417
|
+
return _render_math_segment(text)
|
|
418
|
+
parts = text.split("```")
|
|
419
|
+
for i in range(0, len(parts), 2): # even indices are outside fences
|
|
420
|
+
parts[i] = _render_math_segment(parts[i])
|
|
421
|
+
return "```".join(parts)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _render_math_segment(segment):
|
|
425
|
+
segment = _strip_latex_noise(segment)
|
|
426
|
+
segment = convert_fractions(segment)
|
|
427
|
+
segment = convert_matrices(segment)
|
|
428
|
+
segment = convert_sqrt(segment)
|
|
429
|
+
segment = convert_greek(segment)
|
|
430
|
+
segment = convert_explicit_superscripts(segment)
|
|
431
|
+
segment = convert_explicit_subscripts(segment)
|
|
432
|
+
segment = convert_scientific_notation(segment)
|
|
433
|
+
return segment
|