initial
This commit is contained in:
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache.db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
4
README.md
Normal file
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# PyPages
|
||||||
|
|
||||||
|
A _currently_ experimental PyDoc WebPage, which gets documentation from Python itself!
|
||||||
|
|
||||||
23
app.py
Normal file
23
app.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Main Flask application entry point.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from flask import Flask, send_from_directory
|
||||||
|
from routes.api import api_bp
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
||||||
|
|
||||||
|
# Register API blueprint (FastAPI-compatible route structure)
|
||||||
|
app.register_blueprint(api_bp)
|
||||||
|
|
||||||
|
# Serve index.html at root
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return send_from_directory(os.path.join(basedir, 'static'), 'index.html')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
|
|
||||||
2
modules/__init__.py
Normal file
2
modules/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Modules package for pydoc translation service."""
|
||||||
|
|
||||||
252
modules/cache.py
Normal file
252
modules/cache.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"""
|
||||||
|
Caching module with extensible backend support.
|
||||||
|
|
||||||
|
To add a new cache backend:
|
||||||
|
1. Create a class that inherits from CacheBackend
|
||||||
|
2. Implement get(), set(), and clear() methods
|
||||||
|
3. Register it in the CacheService class
|
||||||
|
"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class CacheBackend(ABC):
|
||||||
|
"""Abstract base class for cache backends."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
"""Get value from cache."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
||||||
|
"""Set value in cache with optional TTL (time to live in seconds)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear(self) -> bool:
|
||||||
|
"""Clear all cache entries."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryCache(CacheBackend):
|
||||||
|
"""
|
||||||
|
Simple in-memory cache backend.
|
||||||
|
|
||||||
|
Stores data in a dictionary. Data is lost on application restart.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._cache = {}
|
||||||
|
self._ttl = {} # Store expiration times
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
"""Get value from in-memory cache."""
|
||||||
|
if key in self._cache:
|
||||||
|
# Check TTL
|
||||||
|
if key in self._ttl:
|
||||||
|
if datetime.now() > self._ttl[key]:
|
||||||
|
del self._cache[key]
|
||||||
|
del self._ttl[key]
|
||||||
|
return None
|
||||||
|
return self._cache[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
||||||
|
"""Set value in in-memory cache."""
|
||||||
|
self._cache[key] = value
|
||||||
|
if ttl:
|
||||||
|
self._ttl[key] = datetime.now() + timedelta(seconds=ttl)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self) -> bool:
|
||||||
|
"""Clear all cache entries."""
|
||||||
|
self._cache.clear()
|
||||||
|
self._ttl.clear()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteCache(CacheBackend):
|
||||||
|
"""
|
||||||
|
SQLite-based cache backend.
|
||||||
|
|
||||||
|
Persists data to a SQLite database file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = "cache.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""Initialize the SQLite database."""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
expires_at REAL,
|
||||||
|
created_at REAL NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_expires ON cache(expires_at)')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _cleanup_expired(self):
|
||||||
|
"""Remove expired entries."""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < ?',
|
||||||
|
(datetime.now().timestamp(),))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
"""Get value from SQLite cache."""
|
||||||
|
self._cleanup_expired()
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT value FROM cache WHERE key = ?', (key,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
||||||
|
"""Set value in SQLite cache."""
|
||||||
|
expires_at = None
|
||||||
|
if ttl:
|
||||||
|
expires_at = (datetime.now() + timedelta(seconds=ttl)).timestamp()
|
||||||
|
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT OR REPLACE INTO cache (key, value, expires_at, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''', (key, value, expires_at, datetime.now().timestamp()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self) -> bool:
|
||||||
|
"""Clear all cache entries."""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('DELETE FROM cache')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RedisCache(CacheBackend):
|
||||||
|
"""
|
||||||
|
Redis-based cache backend.
|
||||||
|
|
||||||
|
Requires redis library and REDIS_URL environment variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, redis_url: Optional[str] = None):
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
||||||
|
self.client = redis.from_url(self.redis_url, decode_responses=True)
|
||||||
|
# Test connection
|
||||||
|
self.client.ping()
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("redis library not installed. Install with: pip install redis")
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(f"Failed to connect to Redis: {e}")
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[str]:
|
||||||
|
"""Get value from Redis cache."""
|
||||||
|
try:
|
||||||
|
return self.client.get(key)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Redis get error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
||||||
|
"""Set value in Redis cache."""
|
||||||
|
try:
|
||||||
|
if ttl:
|
||||||
|
self.client.setex(key, ttl, value)
|
||||||
|
else:
|
||||||
|
self.client.set(key, value)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Redis set error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear(self) -> bool:
|
||||||
|
"""Clear all cache entries."""
|
||||||
|
try:
|
||||||
|
self.client.flushdb()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Redis clear error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CacheService:
|
||||||
|
"""
|
||||||
|
Cache service that manages cache backends.
|
||||||
|
|
||||||
|
Provides a unified interface for caching translated documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, backend: Optional[str] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize cache service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backend: Backend name ('memory', 'sqlite', 'redis'). Defaults to 'sqlite'.
|
||||||
|
**kwargs: Additional arguments for backend initialization.
|
||||||
|
"""
|
||||||
|
self.backend_name = backend or os.getenv('CACHE_BACKEND', 'sqlite')
|
||||||
|
self.backend = self._create_backend(self.backend_name, **kwargs)
|
||||||
|
|
||||||
|
def _create_backend(self, backend_name: str, **kwargs) -> CacheBackend:
|
||||||
|
"""Create a cache backend instance."""
|
||||||
|
backends = {
|
||||||
|
'memory': InMemoryCache,
|
||||||
|
'sqlite': SQLiteCache,
|
||||||
|
'redis': RedisCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_class = backends.get(backend_name.lower())
|
||||||
|
if not backend_class:
|
||||||
|
raise ValueError(f"Unknown cache backend: {backend_name}")
|
||||||
|
|
||||||
|
return backend_class(**kwargs)
|
||||||
|
|
||||||
|
def _make_key(self, object_name: str, target_lang: str) -> str:
|
||||||
|
"""Generate a cache key from object name and target language."""
|
||||||
|
key_string = f"{object_name}:{target_lang}"
|
||||||
|
return hashlib.md5(key_string.encode()).hexdigest()
|
||||||
|
|
||||||
|
def get(self, object_name: str, target_lang: str) -> Optional[dict]:
|
||||||
|
"""Get cached translation."""
|
||||||
|
key = self._make_key(object_name, target_lang)
|
||||||
|
value = self.backend.get(key)
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, object_name: str, target_lang: str, data: dict, ttl: Optional[int] = None) -> bool:
|
||||||
|
"""Cache a translation."""
|
||||||
|
key = self._make_key(object_name, target_lang)
|
||||||
|
value = json.dumps(data)
|
||||||
|
return self.backend.set(key, value, ttl)
|
||||||
|
|
||||||
|
def clear(self) -> bool:
|
||||||
|
"""Clear all cache entries."""
|
||||||
|
return self.backend.clear()
|
||||||
|
|
||||||
228
modules/course_scraper.py
Normal file
228
modules/course_scraper.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"""
|
||||||
|
Module for scraping and organizing Python course content.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class CourseScraper:
|
||||||
|
"""Scrapes Python course content from external sources."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scrape_course_content(url: str = None) -> Dict[str, any]:
|
||||||
|
"""
|
||||||
|
Scrape course content from URLs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: Base URL to scrape (optional, will use default if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with course structure and content
|
||||||
|
"""
|
||||||
|
course_data = {
|
||||||
|
'title': 'Python Kurs - Gymnasium Hartberg',
|
||||||
|
'sections': [],
|
||||||
|
'navigation': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# List of course pages to scrape
|
||||||
|
course_pages = [
|
||||||
|
('https://benschi11.github.io/python/class5.html', '5. Klasse'),
|
||||||
|
('https://benschi11.github.io/python/', 'Overview')
|
||||||
|
]
|
||||||
|
|
||||||
|
for page_url, section_title in course_pages:
|
||||||
|
try:
|
||||||
|
response = requests.get(page_url, timeout=10, headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
})
|
||||||
|
response.raise_for_status()
|
||||||
|
soup = BeautifulSoup(response.content, 'html.parser')
|
||||||
|
|
||||||
|
# Find main content
|
||||||
|
main_content = soup.find('main', {'id': 'content'}) or soup.find('main') or soup.find('body')
|
||||||
|
|
||||||
|
if not main_content:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract markdown content from main content
|
||||||
|
markdown_content = []
|
||||||
|
current_section = None
|
||||||
|
current_subsection = None
|
||||||
|
section_id = None
|
||||||
|
|
||||||
|
# Process all elements in order to build markdown
|
||||||
|
for elem in main_content.find_all(['h1', 'h2', 'h3', 'h4', 'p', 'ul', 'li', 'div', 'pre', 'code']):
|
||||||
|
tag_name = elem.name
|
||||||
|
|
||||||
|
# Handle headings
|
||||||
|
if tag_name in ['h1', 'h2']:
|
||||||
|
# Save previous section
|
||||||
|
if current_section and markdown_content:
|
||||||
|
current_section['markdown'] = '\n\n'.join(markdown_content)
|
||||||
|
course_data['sections'].append(current_section)
|
||||||
|
course_data['navigation'].append({
|
||||||
|
'title': current_section['title'],
|
||||||
|
'level': current_section['level'],
|
||||||
|
'id': section_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Start new section
|
||||||
|
text = elem.get_text().strip()
|
||||||
|
if text and not text.startswith('Python Kurs'):
|
||||||
|
section_id = CourseScraper._slugify(text)
|
||||||
|
current_section = {
|
||||||
|
'title': text,
|
||||||
|
'level': int(tag_name[1]),
|
||||||
|
'markdown': '',
|
||||||
|
'id': section_id
|
||||||
|
}
|
||||||
|
markdown_content = [f"{'#' * int(tag_name[1])} {text}"]
|
||||||
|
current_subsection = None
|
||||||
|
|
||||||
|
elif tag_name == 'h3' and current_section:
|
||||||
|
# Subsection heading
|
||||||
|
text = elem.get_text().strip()
|
||||||
|
if text:
|
||||||
|
markdown_content.append(f"\n### {text}")
|
||||||
|
current_subsection = text
|
||||||
|
|
||||||
|
elif tag_name == 'h4' and current_section:
|
||||||
|
# Sub-subsection heading
|
||||||
|
text = elem.get_text().strip()
|
||||||
|
if text:
|
||||||
|
markdown_content.append(f"\n#### {text}")
|
||||||
|
|
||||||
|
elif current_section and tag_name == 'p':
|
||||||
|
# Paragraph
|
||||||
|
text = elem.get_text().strip()
|
||||||
|
if text and len(text) > 5:
|
||||||
|
markdown_content.append(text)
|
||||||
|
|
||||||
|
elif current_section and tag_name == 'ul':
|
||||||
|
# Unordered list
|
||||||
|
list_items = []
|
||||||
|
for li in elem.find_all('li', recursive=False):
|
||||||
|
li_text = li.get_text().strip()
|
||||||
|
if li_text:
|
||||||
|
list_items.append(f"- {li_text}")
|
||||||
|
if list_items:
|
||||||
|
markdown_content.append('\n'.join(list_items))
|
||||||
|
|
||||||
|
elif current_section and tag_name == 'div':
|
||||||
|
# Check for code blocks
|
||||||
|
code_elem = elem.find('code', class_=lambda x: x and 'language-python' in ' '.join(x))
|
||||||
|
if code_elem:
|
||||||
|
code_text = code_elem.get_text().strip()
|
||||||
|
if code_text:
|
||||||
|
markdown_content.append(f"```python\n{code_text}\n```")
|
||||||
|
elif 'language-python' in str(elem.get('class', [])):
|
||||||
|
code_text = elem.get_text().strip()
|
||||||
|
if code_text:
|
||||||
|
markdown_content.append(f"```python\n{code_text}\n```")
|
||||||
|
|
||||||
|
elif current_section and tag_name == 'pre':
|
||||||
|
# Preformatted code
|
||||||
|
code_elem = elem.find('code')
|
||||||
|
if code_elem:
|
||||||
|
code_text = code_elem.get_text().strip()
|
||||||
|
if code_text:
|
||||||
|
# Check if it's Python code
|
||||||
|
lang = 'python'
|
||||||
|
if 'language-python' in str(code_elem.get('class', [])):
|
||||||
|
lang = 'python'
|
||||||
|
markdown_content.append(f"```{lang}\n{code_text}\n```")
|
||||||
|
|
||||||
|
# Save last section
|
||||||
|
if current_section:
|
||||||
|
if markdown_content:
|
||||||
|
current_section['markdown'] = '\n\n'.join(markdown_content)
|
||||||
|
course_data['sections'].append(current_section)
|
||||||
|
course_data['navigation'].append({
|
||||||
|
'title': current_section['title'],
|
||||||
|
'level': current_section['level'],
|
||||||
|
'id': section_id
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error scraping {page_url}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If no content was scraped, return default
|
||||||
|
if not course_data['sections']:
|
||||||
|
return CourseScraper._get_default_course()
|
||||||
|
|
||||||
|
return course_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _slugify(text: str) -> str:
|
||||||
|
"""Convert text to URL-friendly slug."""
|
||||||
|
text = text.lower()
|
||||||
|
text = re.sub(r'[^\w\s-]', '', text)
|
||||||
|
text = re.sub(r'[-\s]+', '-', text)
|
||||||
|
return text.strip('-')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_default_course() -> Dict[str, any]:
|
||||||
|
"""Get default course structure when scraping fails."""
|
||||||
|
return {
|
||||||
|
'title': 'Python Kurs - Gymnasium Hartberg',
|
||||||
|
'sections': [
|
||||||
|
{
|
||||||
|
'title': '5. Klasse',
|
||||||
|
'level': 2,
|
||||||
|
'content': [
|
||||||
|
'Grundlagen der Programmierung mit Python',
|
||||||
|
'Variablen und Datentypen',
|
||||||
|
'Eingabe und Ausgabe',
|
||||||
|
'Bedingte Anweisungen',
|
||||||
|
'Schleifen',
|
||||||
|
'Listen und Dictionaries'
|
||||||
|
],
|
||||||
|
'subsections': []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': '6. Klasse',
|
||||||
|
'level': 2,
|
||||||
|
'content': [
|
||||||
|
'Funktionen',
|
||||||
|
'Module und Pakete',
|
||||||
|
'Dateiverarbeitung',
|
||||||
|
'Fehlerbehandlung',
|
||||||
|
'Objektorientierte Programmierung'
|
||||||
|
],
|
||||||
|
'subsections': []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Objektorientierte Programmierung',
|
||||||
|
'level': 2,
|
||||||
|
'content': [
|
||||||
|
'Klassen und Objekte',
|
||||||
|
'Vererbung',
|
||||||
|
'Polymorphismus',
|
||||||
|
'Abstrakte Klassen'
|
||||||
|
],
|
||||||
|
'subsections': []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Grafische Oberflächen',
|
||||||
|
'level': 2,
|
||||||
|
'content': [
|
||||||
|
'Einführung in GUI-Programmierung',
|
||||||
|
'Tkinter Grundlagen',
|
||||||
|
'Event-Handling',
|
||||||
|
'Layout-Management'
|
||||||
|
],
|
||||||
|
'subsections': []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'navigation': [
|
||||||
|
{'title': '5. Klasse', 'level': 2, 'id': '5-klasse'},
|
||||||
|
{'title': '6. Klasse', 'level': 2, 'id': '6-klasse'},
|
||||||
|
{'title': 'Objektorientierte Programmierung', 'level': 2, 'id': 'objektorientierte-programmierung'},
|
||||||
|
{'title': 'Grafische Oberflächen', 'level': 2, 'id': 'grafische-oberflaechen'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
242
modules/doc_extractor.py
Normal file
242
modules/doc_extractor.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
"""
|
||||||
|
Module for extracting documentation from Python objects using pydoc and inspect.
|
||||||
|
"""
|
||||||
|
import pydoc
|
||||||
|
import inspect
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class DocExtractor:
|
||||||
|
"""
|
||||||
|
Extracts documentation from Python objects.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Modules
|
||||||
|
- Classes
|
||||||
|
- Functions
|
||||||
|
- Methods
|
||||||
|
- Builtins
|
||||||
|
- Any object accessible through pydoc
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_doc(object_name: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract documentation for a Python object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
object_name: Dot-separated path to the object (e.g., 'dict.update', 'os.path', 'builtins.BaseException')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- 'original': Original English documentation
|
||||||
|
- 'object_name': Name of the object
|
||||||
|
- 'object_type': Type of object (module, class, function, etc.)
|
||||||
|
- 'signature': Function/method signature if applicable
|
||||||
|
- 'error': Error message if extraction failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
obj = None
|
||||||
|
resolved_name = object_name
|
||||||
|
|
||||||
|
# For builtins, resolve directly first (pydoc.resolve can be unreliable)
|
||||||
|
if object_name.startswith('builtins.'):
|
||||||
|
try:
|
||||||
|
import builtins
|
||||||
|
name = object_name.replace('builtins.', '', 1)
|
||||||
|
if hasattr(builtins, name):
|
||||||
|
obj = getattr(builtins, name)
|
||||||
|
# Verify we got the right object
|
||||||
|
obj_name = getattr(obj, '__name__', None)
|
||||||
|
if obj_name == name:
|
||||||
|
resolved_name = object_name
|
||||||
|
else:
|
||||||
|
obj = None # Wrong object, try other methods
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If not a builtin or builtin resolution failed, try direct import first
|
||||||
|
# This is more reliable than pydoc.resolve for standard library modules
|
||||||
|
if obj is None:
|
||||||
|
try:
|
||||||
|
parts = object_name.split('.')
|
||||||
|
if len(parts) == 1:
|
||||||
|
# Simple module name (e.g., 'asyncio')
|
||||||
|
obj = __import__(object_name)
|
||||||
|
# Verify it's actually a module
|
||||||
|
if not inspect.ismodule(obj):
|
||||||
|
obj = None
|
||||||
|
elif len(parts) > 1:
|
||||||
|
# Dotted name (e.g., 'os.path', 'collections.abc')
|
||||||
|
module_name = '.'.join(parts[:-1])
|
||||||
|
attr_name = parts[-1]
|
||||||
|
module = __import__(module_name, fromlist=[attr_name])
|
||||||
|
obj = getattr(module, attr_name)
|
||||||
|
resolved_name = object_name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If direct import failed, try pydoc.resolve as fallback
|
||||||
|
if obj is None:
|
||||||
|
try:
|
||||||
|
resolved_obj = pydoc.resolve(object_name)
|
||||||
|
# Verify the resolved object is correct
|
||||||
|
obj = resolved_obj
|
||||||
|
except (ImportError, AttributeError, ValueError) as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
raise ValueError(f"Could not resolve object: {object_name}")
|
||||||
|
|
||||||
|
# Verify we got the right object by checking its name and type
|
||||||
|
# This helps catch cases where pydoc.resolve returns wrong object
|
||||||
|
try:
|
||||||
|
parts = object_name.split('.')
|
||||||
|
expected_name = parts[-1]
|
||||||
|
actual_name = getattr(obj, '__name__', None) or getattr(obj, '__qualname__', None)
|
||||||
|
|
||||||
|
# For modules, check module name
|
||||||
|
if inspect.ismodule(obj):
|
||||||
|
module_name = getattr(obj, '__name__', '')
|
||||||
|
if module_name != object_name and not module_name.endswith('.' + object_name):
|
||||||
|
# Wrong module - try direct import
|
||||||
|
try:
|
||||||
|
correct_obj = __import__(object_name)
|
||||||
|
if inspect.ismodule(correct_obj) and getattr(correct_obj, '__name__', '') == object_name:
|
||||||
|
obj = correct_obj
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# For non-modules, verify the name matches
|
||||||
|
elif actual_name and actual_name != expected_name:
|
||||||
|
# Object name doesn't match - try to get it more directly
|
||||||
|
if len(parts) == 2 and parts[0] == 'builtins':
|
||||||
|
import builtins
|
||||||
|
if hasattr(builtins, parts[1]):
|
||||||
|
new_obj = getattr(builtins, parts[1])
|
||||||
|
new_name = getattr(new_obj, '__name__', None)
|
||||||
|
if new_name == expected_name:
|
||||||
|
obj = new_obj
|
||||||
|
elif len(parts) > 1:
|
||||||
|
# Try direct import for standard library
|
||||||
|
try:
|
||||||
|
module_name = '.'.join(parts[:-1])
|
||||||
|
attr_name = parts[-1]
|
||||||
|
module = __import__(module_name, fromlist=[attr_name])
|
||||||
|
new_obj = getattr(module, attr_name)
|
||||||
|
new_name = getattr(new_obj, '__name__', None) or getattr(new_obj, '__qualname__', None)
|
||||||
|
if new_name == expected_name or new_name == attr_name:
|
||||||
|
obj = new_obj
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass # Continue even if verification fails
|
||||||
|
|
||||||
|
# Get the docstring
|
||||||
|
docstring = inspect.getdoc(obj) or pydoc.getdoc(obj) or ""
|
||||||
|
|
||||||
|
# Additional verification: check if docstring matches tuple (common wrong result)
|
||||||
|
# This catches cases where pydoc.resolve returns tuple instead of the requested object
|
||||||
|
if docstring and "Built-in immutable sequence" in docstring and "tuple" in docstring.lower():
|
||||||
|
# This looks like tuple documentation - verify we didn't request tuple
|
||||||
|
if object_name.lower() != 'tuple' and not object_name.lower().endswith('.tuple'):
|
||||||
|
# We got tuple docs but didn't ask for tuple - this is wrong!
|
||||||
|
# Try to get the correct object
|
||||||
|
try:
|
||||||
|
parts = object_name.split('.')
|
||||||
|
if len(parts) == 1:
|
||||||
|
# Simple module - try direct import
|
||||||
|
correct_obj = __import__(object_name)
|
||||||
|
if inspect.ismodule(correct_obj):
|
||||||
|
correct_doc = inspect.getdoc(correct_obj) or pydoc.getdoc(correct_obj) or ""
|
||||||
|
# If the correct doc doesn't mention tuple, use it
|
||||||
|
if "tuple" not in correct_doc.lower() or "Built-in immutable sequence" not in correct_doc:
|
||||||
|
obj = correct_obj
|
||||||
|
docstring = correct_doc
|
||||||
|
elif len(parts) > 1:
|
||||||
|
# Dotted name - try direct import
|
||||||
|
module_name = '.'.join(parts[:-1])
|
||||||
|
attr_name = parts[-1]
|
||||||
|
module = __import__(module_name, fromlist=[attr_name])
|
||||||
|
correct_obj = getattr(module, attr_name)
|
||||||
|
correct_doc = inspect.getdoc(correct_obj) or pydoc.getdoc(correct_obj) or ""
|
||||||
|
# If the correct doc doesn't mention tuple, use it
|
||||||
|
if "tuple" not in correct_doc.lower() or "Built-in immutable sequence" not in correct_doc:
|
||||||
|
obj = correct_obj
|
||||||
|
docstring = correct_doc
|
||||||
|
except Exception:
|
||||||
|
pass # If correction fails, continue with what we have
|
||||||
|
|
||||||
|
# Determine object type
|
||||||
|
if inspect.ismodule(obj):
|
||||||
|
obj_type = "module"
|
||||||
|
elif inspect.isclass(obj):
|
||||||
|
obj_type = "class"
|
||||||
|
elif inspect.isfunction(obj) or inspect.ismethod(obj):
|
||||||
|
obj_type = "function"
|
||||||
|
else:
|
||||||
|
obj_type = "object"
|
||||||
|
|
||||||
|
# Get signature if it's a callable
|
||||||
|
signature = None
|
||||||
|
if inspect.isclass(obj) or inspect.isfunction(obj) or inspect.ismethod(obj):
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(obj)
|
||||||
|
signature = str(sig)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If docstring is empty, try to get help text
|
||||||
|
if not docstring:
|
||||||
|
try:
|
||||||
|
help_text = pydoc.render_doc(obj, renderer=pydoc.plaintext)
|
||||||
|
# Extract just the docstring part (first paragraph after object name)
|
||||||
|
lines = help_text.split('\n')
|
||||||
|
# Skip empty lines and find the actual docstring
|
||||||
|
start_idx = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip() and not line.strip().startswith(object_name.split('.')[-1]):
|
||||||
|
start_idx = i
|
||||||
|
break
|
||||||
|
docstring = '\n'.join(lines[start_idx:]).strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Final fallback: use help() output
|
||||||
|
if not docstring:
|
||||||
|
try:
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
help_output = io.StringIO()
|
||||||
|
sys.stdout = help_output
|
||||||
|
help(obj)
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
help_text = help_output.getvalue()
|
||||||
|
# Extract meaningful parts
|
||||||
|
lines = help_text.split('\n')
|
||||||
|
docstring = '\n'.join([l for l in lines if l.strip() and not l.strip().startswith('Help on')])[:500]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'original': docstring,
|
||||||
|
'object_name': resolved_name, # Use resolved name, not original
|
||||||
|
'object_type': obj_type,
|
||||||
|
'signature': signature,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_msg = str(e)
|
||||||
|
# Don't expose full traceback to user, but log it
|
||||||
|
print(f"Error extracting doc for {object_name}: {error_msg}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'original': None,
|
||||||
|
'object_name': object_name,
|
||||||
|
'object_type': None,
|
||||||
|
'signature': None,
|
||||||
|
'error': f"Could not extract documentation: {error_msg}"
|
||||||
|
}
|
||||||
|
|
||||||
152
modules/module_list.py
Normal file
152
modules/module_list.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
Module for listing available Python modules and objects.
|
||||||
|
"""
|
||||||
|
import pydoc
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleList:
|
||||||
|
"""
|
||||||
|
Provides lists of available Python modules and objects for documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_standard_modules() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get list of standard library modules.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries with module information
|
||||||
|
"""
|
||||||
|
modules = []
|
||||||
|
stdlib_paths = [p for p in sys.path if 'site-packages' not in p]
|
||||||
|
|
||||||
|
# Common standard library modules
|
||||||
|
common_modules = [
|
||||||
|
'os', 'sys', 'json', 'datetime', 'collections', 'itertools',
|
||||||
|
'functools', 'operator', 'string', 're', 'math', 'random',
|
||||||
|
'statistics', 'decimal', 'fractions', 'array', 'bisect',
|
||||||
|
'heapq', 'copy', 'pickle', 'sqlite3', 'hashlib', 'hmac',
|
||||||
|
'secrets', 'uuid', 'pathlib', 'shutil', 'glob', 'fnmatch',
|
||||||
|
'linecache', 'tempfile', 'fileinput', 'csv', 'configparser',
|
||||||
|
'netrc', 'xdrlib', 'plistlib', 'codecs', 'unicodedata',
|
||||||
|
'stringprep', 'readline', 'rlcompleter', 'struct', 'codecs',
|
||||||
|
'types', 'copyreg', 'pprint', 'reprlib', 'enum', 'numbers',
|
||||||
|
'collections.abc', 'io', 'argparse', 'getopt', 'logging',
|
||||||
|
'getpass', 'curses', 'platform', 'errno', 'ctypes', 'threading',
|
||||||
|
'multiprocessing', 'concurrent', 'subprocess', 'sched', 'queue',
|
||||||
|
'select', 'selectors', 'asyncio', 'socket', 'ssl', 'email',
|
||||||
|
'html', 'http', 'urllib', 'xml', 'webbrowser', 'cgi', 'cgitb',
|
||||||
|
'wsgiref', 'urllib', 'xmlrpc', 'ipaddress', 'audioop', 'aifc',
|
||||||
|
'sunau', 'wave', 'chunk', 'colorsys', 'imghdr', 'sndhdr',
|
||||||
|
'ossaudiodev', 'gettext', 'locale', 'calendar', 'cmd', 'shlex',
|
||||||
|
'tkinter', 'turtle', 'pydoc', 'doctest', 'unittest', 'test',
|
||||||
|
'lib2to3', 'typing', 'dataclasses', 'contextlib', 'abc',
|
||||||
|
'atexit', 'traceback', 'future', 'gc', 'inspect', 'site',
|
||||||
|
'fpectl', 'distutils', 'ensurepip', 'venv', 'zipapp', 'faulthandler',
|
||||||
|
'pdb', 'profile', 'pstats', 'timeit', 'trace', 'tracemalloc',
|
||||||
|
'warnings', 'contextvars', 'dataclasses', 'weakref', 'types',
|
||||||
|
'copy', 'pprint', 'reprlib', 'enum', 'numbers', 'collections.abc'
|
||||||
|
]
|
||||||
|
|
||||||
|
for mod_name in common_modules:
|
||||||
|
try:
|
||||||
|
if mod_name in sys.modules:
|
||||||
|
mod = sys.modules[mod_name]
|
||||||
|
else:
|
||||||
|
mod = __import__(mod_name)
|
||||||
|
|
||||||
|
if inspect.ismodule(mod):
|
||||||
|
modules.append({
|
||||||
|
'name': mod_name,
|
||||||
|
'type': 'module',
|
||||||
|
'doc': inspect.getdoc(mod) or ''
|
||||||
|
})
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return sorted(modules, key=lambda x: x['name'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builtin_objects() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get list of builtin objects (types, functions, etc.).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries with builtin object information
|
||||||
|
"""
|
||||||
|
objects = []
|
||||||
|
builtins_module = __import__('builtins')
|
||||||
|
|
||||||
|
for name in dir(builtins_module):
|
||||||
|
if not name.startswith('_'):
|
||||||
|
try:
|
||||||
|
obj = getattr(builtins_module, name)
|
||||||
|
obj_type = 'function' if inspect.isbuiltin(obj) or inspect.isfunction(obj) else 'type'
|
||||||
|
objects.append({
|
||||||
|
'name': name,
|
||||||
|
'type': obj_type,
|
||||||
|
'full_name': f'builtins.{name}',
|
||||||
|
'doc': ''
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return sorted(objects, key=lambda x: x['name'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_module_contents(module_name: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get list of objects in a module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_name: Name of the module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries with object information
|
||||||
|
"""
|
||||||
|
objects = []
|
||||||
|
try:
|
||||||
|
if module_name in sys.modules:
|
||||||
|
mod = sys.modules[module_name]
|
||||||
|
else:
|
||||||
|
mod = __import__(module_name)
|
||||||
|
|
||||||
|
if not inspect.ismodule(mod):
|
||||||
|
return objects
|
||||||
|
|
||||||
|
for name in dir(mod):
|
||||||
|
if name.startswith('_'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = getattr(mod, name)
|
||||||
|
obj_type = 'unknown'
|
||||||
|
if inspect.ismodule(obj):
|
||||||
|
obj_type = 'module'
|
||||||
|
elif inspect.isclass(obj):
|
||||||
|
obj_type = 'class'
|
||||||
|
elif inspect.isfunction(obj) or inspect.ismethod(obj):
|
||||||
|
obj_type = 'function'
|
||||||
|
elif inspect.isbuiltin(obj):
|
||||||
|
obj_type = 'function'
|
||||||
|
else:
|
||||||
|
obj_type = 'object'
|
||||||
|
|
||||||
|
full_name = f"{module_name}.{name}"
|
||||||
|
objects.append({
|
||||||
|
'name': name,
|
||||||
|
'type': obj_type,
|
||||||
|
'full_name': full_name,
|
||||||
|
'doc': ''
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting module contents for {module_name}: {e}")
|
||||||
|
|
||||||
|
return sorted(objects, key=lambda x: x['name'])
|
||||||
|
|
||||||
288
modules/translator.py
Normal file
288
modules/translator.py
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
"""
|
||||||
|
Translation module with extensible backend support.
|
||||||
|
|
||||||
|
To add a new translation provider:
|
||||||
|
1. Create a class that inherits from TranslationBackend
|
||||||
|
2. Implement the translate() method
|
||||||
|
3. Register it in the TranslationService class
|
||||||
|
"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationBackend(ABC):
|
||||||
|
"""Abstract base class for translation backends."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def translate(self, text: str, target_lang: str, source_lang: str = "en") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Translate text from source language to target language.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Text to translate
|
||||||
|
target_lang: Target language code (e.g., 'de', 'fr', 'es')
|
||||||
|
source_lang: Source language code (default: 'en')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Translated text, or None if translation fails
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HuggingFaceTranslator(TranslationBackend):
|
||||||
|
"""
|
||||||
|
Translation backend using HuggingFace transformers.
|
||||||
|
|
||||||
|
Uses Helsinki-NLP models for translation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._model = None
|
||||||
|
self._tokenizer = None
|
||||||
|
self._model_name = None
|
||||||
|
self._device = 'cpu' # Default device
|
||||||
|
|
||||||
|
def _load_model(self, target_lang: str):
|
||||||
|
"""Lazy load the translation model."""
|
||||||
|
try:
|
||||||
|
from transformers import MarianMTModel, MarianTokenizer
|
||||||
|
import torch
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"transformers library not installed. "
|
||||||
|
"Install with: pip install transformers torch"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check for SentencePiece (required by MarianTokenizer)
|
||||||
|
import sentencepiece
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"SentencePiece library not installed. "
|
||||||
|
"Install with: pip install sentencepiece"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map language codes to model names
|
||||||
|
model_map = {
|
||||||
|
'de': 'Helsinki-NLP/opus-mt-en-de',
|
||||||
|
'fr': 'Helsinki-NLP/opus-mt-en-fr',
|
||||||
|
'es': 'Helsinki-NLP/opus-mt-en-es',
|
||||||
|
'it': 'Helsinki-NLP/opus-mt-en-it',
|
||||||
|
'pt': 'Helsinki-NLP/opus-mt-en-pt',
|
||||||
|
'ru': 'Helsinki-NLP/opus-mt-en-ru',
|
||||||
|
}
|
||||||
|
|
||||||
|
model_name = model_map.get(target_lang)
|
||||||
|
if not model_name:
|
||||||
|
raise ValueError(f"No model available for language: {target_lang}")
|
||||||
|
|
||||||
|
# Only reload if language changed
|
||||||
|
if self._model_name != model_name:
|
||||||
|
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||||
|
|
||||||
|
# Load tokenizer first (doesn't need device)
|
||||||
|
self._tokenizer = MarianTokenizer.from_pretrained(model_name)
|
||||||
|
|
||||||
|
# Load model - try to load directly to device to avoid meta tensor issues
|
||||||
|
try:
|
||||||
|
# For CPU, load normally
|
||||||
|
if device == 'cpu':
|
||||||
|
self._model = MarianMTModel.from_pretrained(model_name)
|
||||||
|
self._model.eval()
|
||||||
|
else:
|
||||||
|
# For CUDA, try loading with device_map or load then move
|
||||||
|
try:
|
||||||
|
# Try loading with device_map if supported
|
||||||
|
self._model = MarianMTModel.from_pretrained(
|
||||||
|
model_name,
|
||||||
|
device_map='auto'
|
||||||
|
)
|
||||||
|
self._model.eval()
|
||||||
|
# Update device based on where model actually ended up
|
||||||
|
actual_device = next(self._model.parameters()).device.type
|
||||||
|
device = actual_device if actual_device in ['cuda', 'cpu'] else 'cpu'
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Fallback: load to CPU first, then move
|
||||||
|
self._model = MarianMTModel.from_pretrained(model_name)
|
||||||
|
self._model.eval()
|
||||||
|
try:
|
||||||
|
self._model = self._model.to(device)
|
||||||
|
except Exception:
|
||||||
|
# If moving fails, keep on CPU
|
||||||
|
device = 'cpu'
|
||||||
|
except Exception as e:
|
||||||
|
# Ultimate fallback: load to CPU
|
||||||
|
print(f"Warning: Error loading model to {device}, using CPU: {e}")
|
||||||
|
self._model = MarianMTModel.from_pretrained(model_name)
|
||||||
|
self._model.eval()
|
||||||
|
device = 'cpu'
|
||||||
|
|
||||||
|
self._model_name = model_name
|
||||||
|
self._device = device
|
||||||
|
|
||||||
|
def translate(self, text: str, target_lang: str, source_lang: str = "en") -> Optional[str]:
|
||||||
|
"""Translate using HuggingFace model."""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._load_model(target_lang)
|
||||||
|
import torch
|
||||||
|
|
||||||
|
# Split text into paragraphs first, then sentences
|
||||||
|
paragraphs = text.split('\n\n')
|
||||||
|
translated_paragraphs = []
|
||||||
|
|
||||||
|
for para in paragraphs:
|
||||||
|
if not para.strip():
|
||||||
|
translated_paragraphs.append(para)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Split into sentences (simple approach)
|
||||||
|
sentences = para.split('\n')
|
||||||
|
translated_sentences = []
|
||||||
|
|
||||||
|
for sentence in sentences:
|
||||||
|
if not sentence.strip():
|
||||||
|
translated_sentences.append(sentence)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Tokenize and move to device
|
||||||
|
inputs = self._tokenizer(
|
||||||
|
[sentence],
|
||||||
|
return_tensors="pt",
|
||||||
|
padding=True,
|
||||||
|
truncation=True,
|
||||||
|
max_length=512
|
||||||
|
).to(self._device)
|
||||||
|
|
||||||
|
# Generate translation
|
||||||
|
with torch.no_grad():
|
||||||
|
translated = self._model.generate(**inputs, max_length=512)
|
||||||
|
|
||||||
|
translated_text = self._tokenizer.decode(translated[0], skip_special_tokens=True)
|
||||||
|
translated_sentences.append(translated_text)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error translating sentence: {e}")
|
||||||
|
translated_sentences.append(sentence) # Fallback to original
|
||||||
|
|
||||||
|
translated_paragraphs.append('\n'.join(translated_sentences))
|
||||||
|
|
||||||
|
return '\n\n'.join(translated_paragraphs)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Translation error: {e}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleTranslateBackend(TranslationBackend):
|
||||||
|
"""
|
||||||
|
Translation backend using Google Translate API.
|
||||||
|
|
||||||
|
Requires GOOGLE_TRANSLATE_API_KEY environment variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = os.getenv('GOOGLE_TRANSLATE_API_KEY')
|
||||||
|
if not self.api_key:
|
||||||
|
raise ValueError("GOOGLE_TRANSLATE_API_KEY environment variable not set")
|
||||||
|
|
||||||
|
def translate(self, text: str, target_lang: str, source_lang: str = "en") -> Optional[str]:
|
||||||
|
"""Translate using Google Translate API."""
|
||||||
|
try:
|
||||||
|
from googletrans import Translator
|
||||||
|
translator = Translator()
|
||||||
|
result = translator.translate(text, dest=target_lang, src=source_lang)
|
||||||
|
return result.text
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("googletrans library not installed. Install with: pip install googletrans==4.0.0rc1")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Google Translate error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DeepLTranslator(TranslationBackend):
|
||||||
|
"""
|
||||||
|
Translation backend using DeepL API.
|
||||||
|
|
||||||
|
Requires DEEPL_API_KEY environment variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = os.getenv('DEEPL_API_KEY')
|
||||||
|
if not self.api_key:
|
||||||
|
raise ValueError("DEEPL_API_KEY environment variable not set")
|
||||||
|
|
||||||
|
def translate(self, text: str, target_lang: str, source_lang: str = "en") -> Optional[str]:
|
||||||
|
"""Translate using DeepL API."""
|
||||||
|
try:
|
||||||
|
import deepl
|
||||||
|
translator = deepl.Translator(self.api_key)
|
||||||
|
result = translator.translate_text(text, target_lang=target_lang.upper(), source_lang=source_lang.upper())
|
||||||
|
return result.text
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("deepl library not installed. Install with: pip install deepl")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DeepL translation error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TranslationService:
|
||||||
|
"""
|
||||||
|
Translation service that manages multiple translation backends.
|
||||||
|
|
||||||
|
Automatically selects the best available backend based on configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, backend: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Initialize translation service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backend: Backend name ('huggingface', 'google', 'deepl').
|
||||||
|
If None, auto-selects based on availability.
|
||||||
|
"""
|
||||||
|
self.backend_name = backend or self._auto_select_backend()
|
||||||
|
self.backend = self._create_backend(self.backend_name)
|
||||||
|
|
||||||
|
def _auto_select_backend(self) -> str:
|
||||||
|
"""Auto-select the best available backend."""
|
||||||
|
# Priority: DeepL > Google > HuggingFace
|
||||||
|
if os.getenv('DEEPL_API_KEY'):
|
||||||
|
return 'deepl'
|
||||||
|
elif os.getenv('GOOGLE_TRANSLATE_API_KEY'):
|
||||||
|
return 'google'
|
||||||
|
else:
|
||||||
|
return 'huggingface' # Default to local model
|
||||||
|
|
||||||
|
def _create_backend(self, backend_name: str) -> TranslationBackend:
|
||||||
|
"""Create a translation backend instance."""
|
||||||
|
backends = {
|
||||||
|
'huggingface': HuggingFaceTranslator,
|
||||||
|
'google': GoogleTranslateBackend,
|
||||||
|
'deepl': DeepLTranslator,
|
||||||
|
}
|
||||||
|
|
||||||
|
backend_class = backends.get(backend_name.lower())
|
||||||
|
if not backend_class:
|
||||||
|
raise ValueError(f"Unknown backend: {backend_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return backend_class()
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to HuggingFace if other backends fail
|
||||||
|
if backend_name != 'huggingface':
|
||||||
|
print(f"Failed to initialize {backend_name}, falling back to HuggingFace: {e}")
|
||||||
|
return HuggingFaceTranslator()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def translate(self, text: str, target_lang: str, source_lang: str = "en") -> Optional[str]:
|
||||||
|
"""Translate text using the configured backend."""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return self.backend.translate(text, target_lang, source_lang)
|
||||||
|
|
||||||
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
# Translation backends (install as needed)
|
||||||
|
transformers==4.35.0 # For HuggingFace backend
|
||||||
|
sentencepiece>=0.1.99 # Required by MarianTokenizer
|
||||||
|
sacremoses>=0.0.53 # Required by MarianTokenizer for tokenization
|
||||||
|
# torch==2.1.0 # Required by transformers (install separately: pip install torch)
|
||||||
|
# googletrans==4.0.0rc1 # For Google Translate backend
|
||||||
|
# deepl==1.15.0 # For DeepL backend
|
||||||
|
# redis==5.0.1 # For Redis cache backend
|
||||||
|
# Course scraping
|
||||||
|
beautifulsoup4>=4.12.0 # For parsing HTML content
|
||||||
|
requests>=2.31.0 # For HTTP requests
|
||||||
|
|
||||||
2
routes/__init__.py
Normal file
2
routes/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Routes package for Flask API."""
|
||||||
|
|
||||||
157
routes/api.py
Normal file
157
routes/api.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"""
|
||||||
|
API routes for the pydoc translation service.
|
||||||
|
|
||||||
|
FastAPI-compatible route structure implemented in Flask.
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from modules.doc_extractor import DocExtractor
|
||||||
|
from modules.translator import TranslationService
|
||||||
|
from modules.cache import CacheService
|
||||||
|
from modules.module_list import ModuleList
|
||||||
|
from modules.course_scraper import CourseScraper
|
||||||
|
|
||||||
|
api_bp = Blueprint('api', __name__, url_prefix='')
|
||||||
|
|
||||||
|
# Initialize services (singleton pattern)
|
||||||
|
doc_extractor = DocExtractor()
|
||||||
|
translator = TranslationService()
|
||||||
|
cache = CacheService()
|
||||||
|
module_list = ModuleList()
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/docs', methods=['GET'])
|
||||||
|
def get_docs():
|
||||||
|
"""
|
||||||
|
Get documentation for a Python object with optional translation.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
object: Python object name (e.g., 'dict.update', 'os.path')
|
||||||
|
lang: Target language code (e.g., 'de', 'fr', 'es'). Optional.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response with:
|
||||||
|
- original: Original English documentation
|
||||||
|
- translated: Translated documentation (if lang provided)
|
||||||
|
- object_name: Name of the object
|
||||||
|
- object_type: Type of object
|
||||||
|
- signature: Function signature if applicable
|
||||||
|
- error: Error message if any
|
||||||
|
- cached: Whether the result was served from cache
|
||||||
|
"""
|
||||||
|
object_name = request.args.get('object')
|
||||||
|
target_lang = request.args.get('lang')
|
||||||
|
|
||||||
|
if not object_name:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Missing required parameter: object',
|
||||||
|
'original': None,
|
||||||
|
'translated': None
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Check cache first if language is specified
|
||||||
|
cached_result = None
|
||||||
|
if target_lang:
|
||||||
|
cached_result = cache.get(object_name, target_lang)
|
||||||
|
if cached_result:
|
||||||
|
# Verify cached result matches the requested object
|
||||||
|
if cached_result.get('object_name') == object_name:
|
||||||
|
cached_result['cached'] = True
|
||||||
|
return jsonify(cached_result)
|
||||||
|
else:
|
||||||
|
# Cache mismatch - clear it and continue
|
||||||
|
print(f"Cache mismatch for {object_name}: got {cached_result.get('object_name')}")
|
||||||
|
|
||||||
|
# Extract documentation
|
||||||
|
doc_data = doc_extractor.extract_doc(object_name)
|
||||||
|
|
||||||
|
if doc_data.get('error'):
|
||||||
|
return jsonify({
|
||||||
|
'error': doc_data['error'],
|
||||||
|
'original': None,
|
||||||
|
'translated': None,
|
||||||
|
'object_name': object_name
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# Always translate if language is specified (auto-translate)
|
||||||
|
translated = None
|
||||||
|
if target_lang and doc_data.get('original'):
|
||||||
|
try:
|
||||||
|
translated = translator.translate(doc_data['original'], target_lang)
|
||||||
|
if not translated:
|
||||||
|
# If translation fails, try to return original with error note
|
||||||
|
translated = None
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Translation error: {e}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
translated = None
|
||||||
|
|
||||||
|
# Prepare response
|
||||||
|
response = {
|
||||||
|
'original': doc_data['original'],
|
||||||
|
'translated': translated,
|
||||||
|
'object_name': doc_data['object_name'],
|
||||||
|
'object_type': doc_data['object_type'],
|
||||||
|
'signature': doc_data['signature'],
|
||||||
|
'error': doc_data['error'],
|
||||||
|
'cached': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache the result if translation was successful
|
||||||
|
if target_lang and translated:
|
||||||
|
cache.set(object_name, target_lang, response, ttl=86400) # Cache for 24 hours
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/modules', methods=['GET'])
|
||||||
|
def get_modules():
|
||||||
|
"""
|
||||||
|
Get list of available Python modules.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
module: Optional module name to get contents of that module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of available modules or module contents
|
||||||
|
"""
|
||||||
|
module_name = request.args.get('module')
|
||||||
|
|
||||||
|
if module_name:
|
||||||
|
# Get contents of specific module
|
||||||
|
contents = module_list.get_module_contents(module_name)
|
||||||
|
return jsonify({
|
||||||
|
'module': module_name,
|
||||||
|
'contents': contents
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Get list of standard modules
|
||||||
|
modules = module_list.get_standard_modules()
|
||||||
|
builtins = module_list.get_builtin_objects()
|
||||||
|
return jsonify({
|
||||||
|
'modules': modules,
|
||||||
|
'builtins': builtins
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/course', methods=['GET'])
|
||||||
|
def get_course():
|
||||||
|
"""
|
||||||
|
Get Python course content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Course structure and content
|
||||||
|
"""
|
||||||
|
scraper = CourseScraper()
|
||||||
|
course_data = scraper.scrape_course_content()
|
||||||
|
return jsonify(course_data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/health', methods=['GET'])
|
||||||
|
def health():
|
||||||
|
"""Health check endpoint."""
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'service': 'pydoc-translation-service'
|
||||||
|
})
|
||||||
|
|
||||||
136
static/app.js
Normal file
136
static/app.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// Main Application
|
||||||
|
import { ThemeToggle } from './components/ThemeToggle.js';
|
||||||
|
import { Sidebar } from './components/Sidebar.js';
|
||||||
|
import { SearchForm } from './components/SearchForm.js';
|
||||||
|
import { SearchBar } from './components/SearchBar.js';
|
||||||
|
import { LanguageSelector } from './components/LanguageSelector.js';
|
||||||
|
import { ModuleList } from './components/ModuleList.js';
|
||||||
|
import { CourseList } from './components/CourseList.js';
|
||||||
|
import { Results } from './components/Results.js';
|
||||||
|
import { Tabs } from './components/Tabs.js';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
constructor() {
|
||||||
|
this.results = new Results();
|
||||||
|
this.searchBar = null;
|
||||||
|
this.languageSelector = null;
|
||||||
|
this.moduleList = null;
|
||||||
|
this.courseList = null;
|
||||||
|
this.currentObject = '';
|
||||||
|
this.currentLang = '';
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// Initialize components
|
||||||
|
new ThemeToggle();
|
||||||
|
new Sidebar();
|
||||||
|
new SearchForm();
|
||||||
|
|
||||||
|
// Initialize language selector
|
||||||
|
this.languageSelector = new LanguageSelector((langCode) => {
|
||||||
|
this.currentLang = langCode;
|
||||||
|
if (this.currentObject) {
|
||||||
|
this.fetchDocumentation(this.currentObject, langCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize search bar
|
||||||
|
this.searchBar = new SearchBar(
|
||||||
|
(query) => {
|
||||||
|
this.currentObject = query;
|
||||||
|
this.fetchDocumentation(query, this.languageSelector.getCurrentLanguage());
|
||||||
|
},
|
||||||
|
(fullName) => {
|
||||||
|
this.currentObject = fullName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.moduleList = new ModuleList((fullName) => {
|
||||||
|
this.searchBar.setValue(fullName);
|
||||||
|
this.currentObject = fullName;
|
||||||
|
this.fetchDocumentation(fullName, this.languageSelector.getCurrentLanguage());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.courseList = new CourseList((section) => {
|
||||||
|
this.results.scrollToSection(section);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Tabs((tabName) => {
|
||||||
|
if (tabName === 'course') {
|
||||||
|
this.loadCourseContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchDocumentation(objectName, targetLang) {
|
||||||
|
if (!objectName || objectName.trim().length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading with progress bar
|
||||||
|
const loadingMessage = targetLang
|
||||||
|
? `Translating documentation to ${this.getLanguageName(targetLang)}...`
|
||||||
|
: 'Loading documentation...';
|
||||||
|
this.results.showLoading(loadingMessage, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ object: objectName.trim() });
|
||||||
|
if (targetLang) {
|
||||||
|
params.append('lang', targetLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/docs?${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Complete progress animation
|
||||||
|
if (this.results.loadingProgress) {
|
||||||
|
this.results.loadingProgress.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay for smooth transition
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
this.results.showError(`Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results.displayDocumentation(data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (this.results.loadingProgress) {
|
||||||
|
this.results.loadingProgress.hide();
|
||||||
|
}
|
||||||
|
this.results.showError(`Network error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLanguageName(langCode) {
|
||||||
|
const langMap = {
|
||||||
|
'de': 'German',
|
||||||
|
'fr': 'French',
|
||||||
|
'es': 'Spanish',
|
||||||
|
'it': 'Italian',
|
||||||
|
'pt': 'Portuguese',
|
||||||
|
'ru': 'Russian'
|
||||||
|
};
|
||||||
|
return langMap[langCode] || langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCourseContent() {
|
||||||
|
await this.courseList.loadCourseContent();
|
||||||
|
const courseData = this.courseList.getCourseData();
|
||||||
|
if (courseData) {
|
||||||
|
this.results.displayCourse(courseData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize app when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => new App());
|
||||||
|
} else {
|
||||||
|
new App();
|
||||||
|
}
|
||||||
|
|
||||||
88
static/components/CourseList.js
Normal file
88
static/components/CourseList.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Course List Component
|
||||||
|
export class CourseList {
|
||||||
|
constructor(onSectionClick) {
|
||||||
|
this.container = document.getElementById('courseList');
|
||||||
|
this.onSectionClick = onSectionClick;
|
||||||
|
this.courseData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCourseContent() {
|
||||||
|
if (this.container?.querySelector('.course-loaded')) {
|
||||||
|
return; // Already loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/course');
|
||||||
|
const data = await response.json();
|
||||||
|
this.courseData = data;
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'module-items';
|
||||||
|
list.classList.add('course-loaded');
|
||||||
|
|
||||||
|
if (data.sections && data.sections.length > 0) {
|
||||||
|
data.sections.forEach(section => {
|
||||||
|
const sectionDiv = this.createSectionItem(section);
|
||||||
|
list.appendChild(sectionDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.container.appendChild(list);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = `<div class="error-message">Error loading course: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSectionItem(section) {
|
||||||
|
const sectionDiv = document.createElement('div');
|
||||||
|
sectionDiv.className = 'module-section';
|
||||||
|
|
||||||
|
const sectionTitle = document.createElement('h4');
|
||||||
|
sectionTitle.textContent = section.title;
|
||||||
|
sectionTitle.className = 'module-section-title';
|
||||||
|
sectionDiv.appendChild(sectionTitle);
|
||||||
|
|
||||||
|
const itemsList = document.createElement('div');
|
||||||
|
itemsList.className = 'module-items-list';
|
||||||
|
|
||||||
|
// Create clickable navigation item
|
||||||
|
const navBtn = document.createElement('button');
|
||||||
|
navBtn.className = 'module-item';
|
||||||
|
navBtn.textContent = section.title;
|
||||||
|
navBtn.addEventListener('click', () => {
|
||||||
|
if (this.onSectionClick) {
|
||||||
|
this.onSectionClick(section);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemsList.appendChild(navBtn);
|
||||||
|
|
||||||
|
// Add subsections if any
|
||||||
|
if (section.subsections && section.subsections.length > 0) {
|
||||||
|
section.subsections.forEach(subsection => {
|
||||||
|
const subBtn = document.createElement('button');
|
||||||
|
subBtn.className = 'module-item';
|
||||||
|
subBtn.style.paddingLeft = '1.5rem';
|
||||||
|
subBtn.textContent = subsection.title;
|
||||||
|
subBtn.addEventListener('click', () => {
|
||||||
|
if (this.onSectionClick) {
|
||||||
|
this.onSectionClick(subsection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemsList.appendChild(subBtn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionDiv.appendChild(itemsList);
|
||||||
|
return sectionDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCourseData() {
|
||||||
|
return this.courseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
134
static/components/LanguageSelector.js
Normal file
134
static/components/LanguageSelector.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Language Selector Component with Emojis
|
||||||
|
export class LanguageSelector {
|
||||||
|
constructor(onLanguageChange) {
|
||||||
|
this.onLanguageChange = onLanguageChange;
|
||||||
|
this.currentLang = '';
|
||||||
|
this.languages = [
|
||||||
|
{ code: '', name: 'English', emoji: '🇬🇧', native: 'English' },
|
||||||
|
{ code: 'de', name: 'German', emoji: '🇩🇪', native: 'Deutsch' },
|
||||||
|
{ code: 'fr', name: 'French', emoji: '🇫🇷', native: 'Français' },
|
||||||
|
{ code: 'es', name: 'Spanish', emoji: '🇪🇸', native: 'Español' },
|
||||||
|
{ code: 'it', name: 'Italian', emoji: '🇮🇹', native: 'Italiano' },
|
||||||
|
{ code: 'pt', name: 'Portuguese', emoji: '🇵🇹', native: 'Português' },
|
||||||
|
{ code: 'ru', name: 'Russian', emoji: '🇷🇺', native: 'Русский' }
|
||||||
|
];
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Load saved language preference
|
||||||
|
const savedLang = localStorage.getItem('preferredLanguage') || '';
|
||||||
|
this.currentLang = savedLang;
|
||||||
|
this.createSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSelector() {
|
||||||
|
// Create floating language selector
|
||||||
|
const selector = document.createElement('div');
|
||||||
|
selector.className = 'language-selector';
|
||||||
|
selector.id = 'languageSelector';
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'language-selector-button';
|
||||||
|
button.setAttribute('aria-label', 'Select language');
|
||||||
|
button.setAttribute('aria-haspopup', 'true');
|
||||||
|
|
||||||
|
const currentLang = this.languages.find(lang => lang.code === this.currentLang) || this.languages[0];
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="language-emoji">${currentLang.emoji}</span>
|
||||||
|
<span class="language-name">${currentLang.native}</span>
|
||||||
|
<span class="language-arrow">▼</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'language-dropdown';
|
||||||
|
dropdown.id = 'languageDropdown';
|
||||||
|
|
||||||
|
this.languages.forEach(lang => {
|
||||||
|
const option = document.createElement('button');
|
||||||
|
option.className = `language-option ${lang.code === this.currentLang ? 'active' : ''}`;
|
||||||
|
option.dataset.code = lang.code;
|
||||||
|
option.innerHTML = `
|
||||||
|
<span class="language-emoji">${lang.emoji}</span>
|
||||||
|
<span class="language-name">${lang.native}</span>
|
||||||
|
<span class="language-name-en">${lang.name}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
option.addEventListener('click', () => {
|
||||||
|
this.selectLanguage(lang.code);
|
||||||
|
dropdown.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isActive = dropdown.classList.contains('active');
|
||||||
|
dropdown.classList.toggle('active');
|
||||||
|
// Update arrow rotation
|
||||||
|
const arrow = button.querySelector('.language-arrow');
|
||||||
|
if (arrow) {
|
||||||
|
arrow.style.transform = isActive ? 'rotate(0deg)' : 'rotate(180deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
selector.appendChild(button);
|
||||||
|
selector.appendChild(dropdown);
|
||||||
|
|
||||||
|
// Store reference for arrow rotation
|
||||||
|
this.button = button;
|
||||||
|
this.dropdown = dropdown;
|
||||||
|
|
||||||
|
// Insert into content area (top right)
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
if (content) {
|
||||||
|
content.insertBefore(selector, content.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!selector.contains(e.target)) {
|
||||||
|
dropdown.classList.remove('active');
|
||||||
|
const arrow = button.querySelector('.language-arrow');
|
||||||
|
if (arrow) {
|
||||||
|
arrow.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectLanguage(code) {
|
||||||
|
this.currentLang = code;
|
||||||
|
localStorage.setItem('preferredLanguage', code);
|
||||||
|
|
||||||
|
// Update button display
|
||||||
|
const currentLang = this.languages.find(lang => lang.code === code) || this.languages[0];
|
||||||
|
const button = document.querySelector('.language-selector-button');
|
||||||
|
if (button) {
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="language-emoji">${currentLang.emoji}</span>
|
||||||
|
<span class="language-name">${currentLang.native}</span>
|
||||||
|
<span class="language-arrow">▼</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active option
|
||||||
|
document.querySelectorAll('.language-option').forEach(option => {
|
||||||
|
if (option.dataset.code === code) {
|
||||||
|
option.classList.add('active');
|
||||||
|
} else {
|
||||||
|
option.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.onLanguageChange) {
|
||||||
|
this.onLanguageChange(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentLanguage() {
|
||||||
|
return this.currentLang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
158
static/components/LoadingProgress.js
Normal file
158
static/components/LoadingProgress.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// Loading Progress Component with Qt-style progress bar
|
||||||
|
export class LoadingProgress {
|
||||||
|
constructor(container) {
|
||||||
|
this.container = container;
|
||||||
|
this.progressBar = null;
|
||||||
|
this.progressFill = null;
|
||||||
|
this.animationFrame = null;
|
||||||
|
this.progress = 0;
|
||||||
|
this.targetProgress = 0;
|
||||||
|
this.isAnimating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(message = 'Loading...', showProgress = true) {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const loadingHTML = `
|
||||||
|
<div class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner-ring"></div>
|
||||||
|
<div class="spinner-ring"></div>
|
||||||
|
<div class="spinner-ring"></div>
|
||||||
|
<div class="spinner-ring"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-text">${message}</div>
|
||||||
|
${showProgress ? `
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
<div class="progress-shine"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text" id="progressText">0%</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.container.innerHTML = loadingHTML;
|
||||||
|
|
||||||
|
if (showProgress) {
|
||||||
|
this.progressBar = this.container.querySelector('.progress-bar');
|
||||||
|
this.progressFill = document.getElementById('progressFill');
|
||||||
|
this.progressText = document.getElementById('progressText');
|
||||||
|
this.startProgressAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startProgressAnimation() {
|
||||||
|
this.progress = 0;
|
||||||
|
this.targetProgress = 0;
|
||||||
|
this.isAnimating = true;
|
||||||
|
|
||||||
|
// More stable progress simulation with smoother steps
|
||||||
|
const steps = [
|
||||||
|
{ progress: 8, delay: 150 },
|
||||||
|
{ progress: 20, delay: 250 },
|
||||||
|
{ progress: 35, delay: 400 },
|
||||||
|
{ progress: 50, delay: 350 },
|
||||||
|
{ progress: 65, delay: 500 },
|
||||||
|
{ progress: 78, delay: 450 },
|
||||||
|
{ progress: 88, delay: 600 },
|
||||||
|
{ progress: 95, delay: 700 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let stepIndex = 0;
|
||||||
|
let isCancelled = false;
|
||||||
|
this.cancelProgress = () => { isCancelled = true; };
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
if (isCancelled || !this.isAnimating) return;
|
||||||
|
|
||||||
|
if (stepIndex < steps.length) {
|
||||||
|
const step = steps[stepIndex];
|
||||||
|
this.targetProgress = step.progress;
|
||||||
|
stepIndex++;
|
||||||
|
setTimeout(updateProgress, step.delay);
|
||||||
|
} else {
|
||||||
|
// Keep at 95% until actual loading completes
|
||||||
|
this.targetProgress = 95;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProgress();
|
||||||
|
this.animateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
animateProgress() {
|
||||||
|
if (!this.isAnimating || !this.progressFill) return;
|
||||||
|
|
||||||
|
// Smooth interpolation
|
||||||
|
const diff = this.targetProgress - this.progress;
|
||||||
|
if (Math.abs(diff) > 0.1) {
|
||||||
|
this.progress += diff * 0.15; // Smooth easing
|
||||||
|
} else {
|
||||||
|
this.progress = this.targetProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progressFill) {
|
||||||
|
this.progressFill.style.width = `${this.progress}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.progressText) {
|
||||||
|
this.progressText.textContent = `${Math.round(this.progress)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add class when progress starts
|
||||||
|
if (this.progressBar) {
|
||||||
|
if (this.progress > 0) {
|
||||||
|
this.progressBar.classList.add('has-progress');
|
||||||
|
} else {
|
||||||
|
this.progressBar.classList.remove('has-progress');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAnimating) {
|
||||||
|
this.animationFrame = requestAnimationFrame(() => this.animateProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(value) {
|
||||||
|
this.targetProgress = Math.min(100, Math.max(0, value));
|
||||||
|
if (!this.isAnimating && this.progressFill) {
|
||||||
|
this.startProgressAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
this.targetProgress = 100;
|
||||||
|
// Animate to 100% and then hide
|
||||||
|
const checkComplete = () => {
|
||||||
|
if (this.progress >= 99.9) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hide();
|
||||||
|
}, 200);
|
||||||
|
} else {
|
||||||
|
setTimeout(checkComplete, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isAnimating = false;
|
||||||
|
if (this.cancelProgress) {
|
||||||
|
this.cancelProgress();
|
||||||
|
}
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame);
|
||||||
|
}
|
||||||
|
// Clear progress elements
|
||||||
|
this.progressBar = null;
|
||||||
|
this.progressFill = null;
|
||||||
|
this.progressText = null;
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
79
static/components/ModuleList.js
Normal file
79
static/components/ModuleList.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// Module List Component
|
||||||
|
export class ModuleList {
|
||||||
|
constructor(onModuleClick) {
|
||||||
|
this.container = document.getElementById('moduleList');
|
||||||
|
this.onModuleClick = onModuleClick;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadModuleList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadModuleList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/modules');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'module-items';
|
||||||
|
|
||||||
|
// Add builtins section
|
||||||
|
if (data.builtins && data.builtins.length > 0) {
|
||||||
|
const builtinsSection = this.createSection('Builtins', data.builtins.slice(0, 20));
|
||||||
|
list.appendChild(builtinsSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add modules section
|
||||||
|
if (data.modules && data.modules.length > 0) {
|
||||||
|
const modulesSection = this.createSection('Standard Library', data.modules);
|
||||||
|
list.appendChild(modulesSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.container.appendChild(list);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = `<div class="error-message">Error loading modules: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSection(title, items) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'module-section';
|
||||||
|
|
||||||
|
const sectionTitle = document.createElement('h4');
|
||||||
|
sectionTitle.textContent = title;
|
||||||
|
sectionTitle.className = 'module-section-title';
|
||||||
|
section.appendChild(sectionTitle);
|
||||||
|
|
||||||
|
const itemsList = document.createElement('div');
|
||||||
|
itemsList.className = 'module-items-list';
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const fullName = item.full_name || (title === 'Builtins' ? `builtins.${item.name}` : item.name);
|
||||||
|
const btn = this.createModuleButton(fullName, item.name);
|
||||||
|
itemsList.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
section.appendChild(itemsList);
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
createModuleButton(fullName, displayName) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'module-item';
|
||||||
|
btn.textContent = displayName;
|
||||||
|
btn.title = fullName;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if (this.onModuleClick) {
|
||||||
|
this.onModuleClick(fullName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
233
static/components/Results.js
Normal file
233
static/components/Results.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// Results Component
|
||||||
|
import { LoadingProgress } from './LoadingProgress.js';
|
||||||
|
|
||||||
|
export class Results {
|
||||||
|
constructor() {
|
||||||
|
this.container = document.getElementById('results');
|
||||||
|
this.loadingProgress = new LoadingProgress(this.container);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Show welcome message initially
|
||||||
|
this.showWelcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
showWelcome() {
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="welcome-message">
|
||||||
|
<h1>Python Documentation</h1>
|
||||||
|
<p>Search for any Python object in the sidebar to view its documentation.</p>
|
||||||
|
<p>Select a language to automatically translate the documentation.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(message = 'Loading documentation...', showProgress = true) {
|
||||||
|
if (this.container) {
|
||||||
|
this.loadingProgress.show(message, showProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
if (this.loadingProgress) {
|
||||||
|
this.loadingProgress.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = `<div class="error-message">${message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayDocumentation(data) {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
|
||||||
|
// Header with title
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'doc-header';
|
||||||
|
|
||||||
|
const title = document.createElement('h1');
|
||||||
|
title.className = 'doc-title';
|
||||||
|
title.textContent = data.object_name;
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'doc-meta';
|
||||||
|
meta.innerHTML = `
|
||||||
|
<span class="type-badge">${data.object_type || 'unknown'}</span>
|
||||||
|
${data.cached ? '<span class="doc-badge">Cached</span>' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
header.appendChild(title);
|
||||||
|
header.appendChild(meta);
|
||||||
|
wrapper.appendChild(header);
|
||||||
|
|
||||||
|
// Signature
|
||||||
|
if (data.signature) {
|
||||||
|
const signature = document.createElement('div');
|
||||||
|
signature.className = 'doc-signature';
|
||||||
|
signature.textContent = data.signature;
|
||||||
|
wrapper.appendChild(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main documentation content
|
||||||
|
const docText = data.translated || data.original;
|
||||||
|
|
||||||
|
if (docText) {
|
||||||
|
const docSection = document.createElement('section');
|
||||||
|
docSection.className = 'doc-section';
|
||||||
|
|
||||||
|
const docTextEl = document.createElement('div');
|
||||||
|
docTextEl.className = 'doc-text';
|
||||||
|
docTextEl.textContent = docText;
|
||||||
|
|
||||||
|
docSection.appendChild(docTextEl);
|
||||||
|
wrapper.appendChild(docSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show original if translation exists (collapsible)
|
||||||
|
if (data.translated && data.original) {
|
||||||
|
const originalSection = document.createElement('details');
|
||||||
|
originalSection.className = 'doc-section';
|
||||||
|
originalSection.innerHTML = `
|
||||||
|
<summary style="cursor: pointer; font-weight: 500; margin-bottom: 0.5rem; color: var(--text-secondary);">
|
||||||
|
Original Documentation (English)
|
||||||
|
</summary>
|
||||||
|
<div class="doc-text" style="margin-top: 0.5rem;">${data.original}</div>
|
||||||
|
`;
|
||||||
|
wrapper.appendChild(originalSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.original && !data.translated) {
|
||||||
|
const noDoc = document.createElement('div');
|
||||||
|
noDoc.className = 'doc-text';
|
||||||
|
noDoc.textContent = 'No documentation available for this object.';
|
||||||
|
wrapper.appendChild(noDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayCourse(courseData) {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'course-content';
|
||||||
|
|
||||||
|
const title = document.createElement('h1');
|
||||||
|
title.textContent = courseData.title || 'Python Course';
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
|
||||||
|
if (courseData.sections && courseData.sections.length > 0) {
|
||||||
|
courseData.sections.forEach(section => {
|
||||||
|
const sectionDiv = this.createCourseSection(section);
|
||||||
|
wrapper.appendChild(sectionDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCourseSection(section) {
|
||||||
|
const sectionDiv = document.createElement('section');
|
||||||
|
sectionDiv.className = 'course-section';
|
||||||
|
sectionDiv.id = `section-${section.id || section.title.toLowerCase().replace(/\s+/g, '-')}`;
|
||||||
|
|
||||||
|
// Parse and render markdown
|
||||||
|
if (section.markdown) {
|
||||||
|
// Configure marked options
|
||||||
|
if (typeof marked !== 'undefined') {
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function(code, lang) {
|
||||||
|
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Highlight error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
},
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const htmlContent = marked.parse(section.markdown);
|
||||||
|
|
||||||
|
// Create a container for the markdown content
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'markdown-content';
|
||||||
|
contentDiv.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// Add IDs to headings for navigation
|
||||||
|
contentDiv.querySelectorAll('h1, h2, h3, h4').forEach((heading) => {
|
||||||
|
const text = heading.textContent.trim();
|
||||||
|
const id = text.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.trim();
|
||||||
|
heading.id = id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight code blocks
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionDiv.appendChild(contentDiv);
|
||||||
|
}
|
||||||
|
} else if (section.content && section.content.length > 0) {
|
||||||
|
// Fallback to old format
|
||||||
|
section.content.forEach(item => {
|
||||||
|
if (item.startsWith('```')) {
|
||||||
|
const codeDiv = document.createElement('pre');
|
||||||
|
codeDiv.className = 'doc-signature';
|
||||||
|
codeDiv.textContent = item.replace(/```python\n?/g, '').replace(/```/g, '').trim();
|
||||||
|
sectionDiv.appendChild(codeDiv);
|
||||||
|
} else {
|
||||||
|
const itemDiv = document.createElement('p');
|
||||||
|
itemDiv.className = 'doc-text';
|
||||||
|
itemDiv.textContent = item;
|
||||||
|
sectionDiv.appendChild(itemDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sectionDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToSection(section) {
|
||||||
|
const sectionId = section.id || section.title.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const sectionElement = document.getElementById(`section-${sectionId}`);
|
||||||
|
if (sectionElement) {
|
||||||
|
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find by heading ID
|
||||||
|
const headingId = section.title.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.trim();
|
||||||
|
const headingElement = document.getElementById(headingId);
|
||||||
|
if (headingElement) {
|
||||||
|
headingElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
281
static/components/SearchBar.js
Normal file
281
static/components/SearchBar.js
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// Search Bar Component with Autocomplete
|
||||||
|
export class SearchBar {
|
||||||
|
constructor(onSearch, onSelect) {
|
||||||
|
this.onSearch = onSearch;
|
||||||
|
this.onSelect = onSelect;
|
||||||
|
this.searchInput = null;
|
||||||
|
this.suggestionsContainer = null;
|
||||||
|
this.allItems = [];
|
||||||
|
this.filteredItems = [];
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.debounceTimer = null;
|
||||||
|
this.isOpen = false;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadAllItems();
|
||||||
|
this.createSearchBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAllItems() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/modules');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.allItems = [];
|
||||||
|
|
||||||
|
// Add builtins
|
||||||
|
if (data.builtins && data.builtins.length > 0) {
|
||||||
|
data.builtins.forEach(item => {
|
||||||
|
this.allItems.push({
|
||||||
|
name: item.name,
|
||||||
|
fullName: item.full_name || `builtins.${item.name}`,
|
||||||
|
type: 'builtin',
|
||||||
|
display: item.name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add modules
|
||||||
|
if (data.modules && data.modules.length > 0) {
|
||||||
|
data.modules.forEach(item => {
|
||||||
|
this.allItems.push({
|
||||||
|
name: item.name,
|
||||||
|
fullName: item.name,
|
||||||
|
type: 'module',
|
||||||
|
display: item.name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading items:', error);
|
||||||
|
this.allItems = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSearchBar() {
|
||||||
|
const searchForm = document.querySelector('.search-form');
|
||||||
|
if (!searchForm) return;
|
||||||
|
|
||||||
|
// Create search input
|
||||||
|
const searchWrapper = document.createElement('div');
|
||||||
|
searchWrapper.className = 'search-bar-wrapper';
|
||||||
|
|
||||||
|
const searchIcon = document.createElement('span');
|
||||||
|
searchIcon.className = 'search-icon';
|
||||||
|
searchIcon.innerHTML = '🔍';
|
||||||
|
searchIcon.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
this.searchInput = document.createElement('input');
|
||||||
|
this.searchInput.type = 'text';
|
||||||
|
this.searchInput.className = 'search-bar-input';
|
||||||
|
this.searchInput.placeholder = 'Search Python objects, modules, builtins...';
|
||||||
|
this.searchInput.autocomplete = 'off';
|
||||||
|
this.searchInput.setAttribute('aria-label', 'Search Python documentation');
|
||||||
|
|
||||||
|
// Create suggestions dropdown
|
||||||
|
this.suggestionsContainer = document.createElement('div');
|
||||||
|
this.suggestionsContainer.className = 'search-suggestions';
|
||||||
|
this.suggestionsContainer.id = 'searchSuggestions';
|
||||||
|
|
||||||
|
searchWrapper.appendChild(searchIcon);
|
||||||
|
searchWrapper.appendChild(this.searchInput);
|
||||||
|
searchWrapper.appendChild(this.suggestionsContainer);
|
||||||
|
|
||||||
|
// Replace the old input group
|
||||||
|
const oldInputGroup = searchForm.querySelector('.input-group');
|
||||||
|
if (oldInputGroup) {
|
||||||
|
oldInputGroup.replaceWith(searchWrapper);
|
||||||
|
} else {
|
||||||
|
searchForm.insertBefore(searchWrapper, searchForm.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Debounced search on input
|
||||||
|
this.searchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
const query = e.target.value.trim();
|
||||||
|
|
||||||
|
if (query.length === 0) {
|
||||||
|
this.hideSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this.filterItems(query);
|
||||||
|
this.showSuggestions();
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle keyboard navigation
|
||||||
|
this.searchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (!this.isOpen || this.filteredItems.length === 0) {
|
||||||
|
if (e.key === 'Enter' && this.searchInput.value.trim()) {
|
||||||
|
this.performSearch(this.searchInput.value.trim());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredItems.length - 1);
|
||||||
|
this.updateSelection();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
||||||
|
this.updateSelection();
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selectedIndex >= 0 && this.selectedIndex < this.filteredItems.length) {
|
||||||
|
this.selectItem(this.filteredItems[this.selectedIndex]);
|
||||||
|
} else if (this.searchInput.value.trim()) {
|
||||||
|
this.performSearch(this.searchInput.value.trim());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
this.hideSuggestions();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close suggestions when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!this.searchInput.contains(e.target) &&
|
||||||
|
!this.suggestionsContainer.contains(e.target)) {
|
||||||
|
this.hideSuggestions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-search on blur if there's a value and user typed something
|
||||||
|
let hasTyped = false;
|
||||||
|
this.searchInput.addEventListener('input', () => {
|
||||||
|
hasTyped = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchInput.addEventListener('blur', () => {
|
||||||
|
// Delay to allow click events on suggestions
|
||||||
|
setTimeout(() => {
|
||||||
|
const query = this.searchInput.value.trim();
|
||||||
|
if (query && hasTyped && !this.isOpen) {
|
||||||
|
this.performSearch(query);
|
||||||
|
}
|
||||||
|
hasTyped = false;
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterItems(query) {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
this.filteredItems = this.allItems
|
||||||
|
.filter(item => {
|
||||||
|
const nameMatch = item.name.toLowerCase().includes(lowerQuery);
|
||||||
|
const fullNameMatch = item.fullName.toLowerCase().includes(lowerQuery);
|
||||||
|
return nameMatch || fullNameMatch;
|
||||||
|
})
|
||||||
|
.slice(0, 10); // Limit to 10 suggestions
|
||||||
|
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuggestions() {
|
||||||
|
if (this.filteredItems.length === 0) {
|
||||||
|
this.hideSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOpen = true;
|
||||||
|
this.suggestionsContainer.innerHTML = '';
|
||||||
|
this.suggestionsContainer.classList.add('active');
|
||||||
|
|
||||||
|
this.filteredItems.forEach((item, index) => {
|
||||||
|
const suggestion = document.createElement('div');
|
||||||
|
suggestion.className = 'search-suggestion';
|
||||||
|
suggestion.dataset.index = index;
|
||||||
|
|
||||||
|
const icon = item.type === 'builtin' ? '⚡' : '📦';
|
||||||
|
suggestion.innerHTML = `
|
||||||
|
<span class="suggestion-icon">${icon}</span>
|
||||||
|
<span class="suggestion-name">${this.highlightMatch(item.name, this.searchInput.value)}</span>
|
||||||
|
<span class="suggestion-type">${item.type}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
suggestion.addEventListener('click', () => {
|
||||||
|
this.selectItem(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestion.addEventListener('mouseenter', () => {
|
||||||
|
this.selectedIndex = index;
|
||||||
|
this.updateSelection();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.suggestionsContainer.appendChild(suggestion);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightMatch(text, query) {
|
||||||
|
if (!query) return text;
|
||||||
|
const lowerText = text.toLowerCase();
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const index = lowerText.indexOf(lowerQuery);
|
||||||
|
|
||||||
|
if (index === -1) return text;
|
||||||
|
|
||||||
|
const before = text.substring(0, index);
|
||||||
|
const match = text.substring(index, index + query.length);
|
||||||
|
const after = text.substring(index + query.length);
|
||||||
|
|
||||||
|
return `${before}<mark>${match}</mark>${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelection() {
|
||||||
|
const suggestions = this.suggestionsContainer.querySelectorAll('.search-suggestion');
|
||||||
|
suggestions.forEach((suggestion, index) => {
|
||||||
|
if (index === this.selectedIndex) {
|
||||||
|
suggestion.classList.add('selected');
|
||||||
|
suggestion.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
suggestion.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectItem(item) {
|
||||||
|
this.searchInput.value = item.fullName;
|
||||||
|
this.hideSuggestions();
|
||||||
|
|
||||||
|
if (this.onSelect) {
|
||||||
|
this.onSelect(item.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performSearch(item.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch(query) {
|
||||||
|
if (!query || query.trim().length === 0) return;
|
||||||
|
|
||||||
|
if (this.onSearch) {
|
||||||
|
this.onSearch(query.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSuggestions() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.suggestionsContainer.classList.remove('active');
|
||||||
|
this.suggestionsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
if (this.searchInput) {
|
||||||
|
this.searchInput.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
8
static/components/SearchForm.js
Normal file
8
static/components/SearchForm.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Search Form Component (simplified - now just a container)
|
||||||
|
export class SearchForm {
|
||||||
|
constructor() {
|
||||||
|
// This component is now just a container
|
||||||
|
// Actual search is handled by SearchBar component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
123
static/components/Sidebar.js
Normal file
123
static/components/Sidebar.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Sidebar Component
|
||||||
|
export class Sidebar {
|
||||||
|
constructor() {
|
||||||
|
this.sidebar = document.getElementById('sidebar');
|
||||||
|
this.resizeHandle = document.getElementById('resizeHandle');
|
||||||
|
this.menuToggle = null;
|
||||||
|
this.isResizing = false;
|
||||||
|
this.startX = 0;
|
||||||
|
this.startWidth = 0;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createMenuToggle();
|
||||||
|
this.setupResize();
|
||||||
|
this.setupMobileBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
createMenuToggle() {
|
||||||
|
// Create mobile menu toggle button
|
||||||
|
this.menuToggle = document.createElement('button');
|
||||||
|
this.menuToggle.className = 'mobile-menu-toggle';
|
||||||
|
this.menuToggle.innerHTML = '☰';
|
||||||
|
this.menuToggle.setAttribute('aria-label', 'Toggle menu');
|
||||||
|
this.menuToggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleMobile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create overlay for mobile
|
||||||
|
this.overlay = document.createElement('div');
|
||||||
|
this.overlay.className = 'sidebar-overlay';
|
||||||
|
this.overlay.addEventListener('click', () => this.closeMobile());
|
||||||
|
|
||||||
|
// Insert at the beginning of body
|
||||||
|
document.body.insertBefore(this.menuToggle, document.body.firstChild);
|
||||||
|
document.body.appendChild(this.overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupResize() {
|
||||||
|
if (!this.resizeHandle || !this.sidebar) return;
|
||||||
|
|
||||||
|
this.resizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
if (window.innerWidth <= 768) return; // Disable resize on mobile
|
||||||
|
|
||||||
|
this.isResizing = true;
|
||||||
|
this.startX = e.clientX;
|
||||||
|
this.startWidth = parseInt(window.getComputedStyle(this.sidebar).width, 10);
|
||||||
|
document.addEventListener('mousemove', this.handleResize.bind(this));
|
||||||
|
document.addEventListener('mouseup', this.stopResize.bind(this));
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize(e) {
|
||||||
|
if (!this.isResizing) return;
|
||||||
|
const width = this.startWidth + e.clientX - this.startX;
|
||||||
|
const minWidth = 200;
|
||||||
|
const maxWidth = 600;
|
||||||
|
if (width >= minWidth && width <= maxWidth) {
|
||||||
|
this.sidebar.style.width = `${width}px`;
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', `${width}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopResize() {
|
||||||
|
this.isResizing = false;
|
||||||
|
document.removeEventListener('mousemove', this.handleResize);
|
||||||
|
document.removeEventListener('mouseup', this.stopResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMobileBehavior() {
|
||||||
|
// Close sidebar on window resize if switching to desktop
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (window.innerWidth > 768) {
|
||||||
|
this.closeMobile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close sidebar when clicking on module items or search button on mobile
|
||||||
|
if (this.sidebar) {
|
||||||
|
this.sidebar.addEventListener('click', (e) => {
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
// Close if clicking on interactive elements (but not the toggle itself)
|
||||||
|
if (e.target.closest('.module-item') ||
|
||||||
|
e.target.closest('.search-btn') ||
|
||||||
|
e.target.closest('.nav-tab')) {
|
||||||
|
// Small delay to allow the click to register
|
||||||
|
setTimeout(() => this.closeMobile(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMobile() {
|
||||||
|
const isOpen = this.sidebar?.classList.contains('open');
|
||||||
|
if (isOpen) {
|
||||||
|
this.closeMobile();
|
||||||
|
} else {
|
||||||
|
this.openMobile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openMobile() {
|
||||||
|
this.sidebar?.classList.add('open');
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.classList.add('active');
|
||||||
|
}
|
||||||
|
// Prevent body scroll when sidebar is open
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeMobile() {
|
||||||
|
this.sidebar?.classList.remove('open');
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
// Restore body scroll
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
static/components/Tabs.js
Normal file
54
static/components/Tabs.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Tabs Component
|
||||||
|
export class Tabs {
|
||||||
|
constructor(onTabChange) {
|
||||||
|
this.navTabs = document.querySelectorAll('.nav-tab');
|
||||||
|
this.docsTab = document.getElementById('docsTab');
|
||||||
|
this.courseTab = document.getElementById('courseTab');
|
||||||
|
this.onTabChange = onTabChange;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.navTabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const tabName = tab.dataset.tab;
|
||||||
|
this.switchTab(tabName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTab(tabName) {
|
||||||
|
// Update active tab
|
||||||
|
this.navTabs.forEach(t => t.classList.remove('active'));
|
||||||
|
const activeTab = Array.from(this.navTabs).find(t => t.dataset.tab === tabName);
|
||||||
|
if (activeTab) {
|
||||||
|
activeTab.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide tab content
|
||||||
|
if (tabName === 'docs') {
|
||||||
|
if (this.docsTab) {
|
||||||
|
this.docsTab.style.display = 'flex';
|
||||||
|
this.docsTab.classList.add('active');
|
||||||
|
}
|
||||||
|
if (this.courseTab) {
|
||||||
|
this.courseTab.style.display = 'none';
|
||||||
|
this.courseTab.classList.remove('active');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.docsTab) {
|
||||||
|
this.docsTab.style.display = 'none';
|
||||||
|
this.docsTab.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (this.courseTab) {
|
||||||
|
this.courseTab.style.display = 'flex';
|
||||||
|
this.courseTab.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onTabChange) {
|
||||||
|
this.onTabChange(tabName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
26
static/components/ThemeToggle.js
Normal file
26
static/components/ThemeToggle.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Theme Toggle Component
|
||||||
|
export class ThemeToggle {
|
||||||
|
constructor() {
|
||||||
|
this.html = document.documentElement;
|
||||||
|
this.toggle = document.getElementById('themeToggle');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Load saved theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
this.html.setAttribute('data-theme', savedTheme);
|
||||||
|
|
||||||
|
if (this.toggle) {
|
||||||
|
this.toggle.addEventListener('click', () => this.toggleTheme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTheme() {
|
||||||
|
const currentTheme = this.html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
this.html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1436
static/index.css
Normal file
1436
static/index.css
Normal file
File diff suppressed because it is too large
Load Diff
84
static/index.html
Normal file
84
static/index.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Python Documentation - Auto Translator</title>
|
||||||
|
<link rel="stylesheet" href="/static/index.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2 class="sidebar-title">Python Docs</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-content" id="sidebarContent">
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
<div class="nav-tabs">
|
||||||
|
<button class="nav-tab active" data-tab="docs">Docs</button>
|
||||||
|
<button class="nav-tab" data-tab="course">Course</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Docs Tab Content -->
|
||||||
|
<div id="docsTab" class="tab-content active">
|
||||||
|
<!-- Search form -->
|
||||||
|
<div class="search-form">
|
||||||
|
<!-- SearchBar component will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Module list -->
|
||||||
|
<div class="module-list-container">
|
||||||
|
<h3 class="module-list-title">Available Modules</h3>
|
||||||
|
<div id="moduleList" class="module-list">
|
||||||
|
<div class="loading-modules">Loading modules...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Course Tab Content -->
|
||||||
|
<div id="courseTab" class="tab-content" style="display: none;">
|
||||||
|
<div class="module-list-container">
|
||||||
|
<h3 class="module-list-title">Python Course</h3>
|
||||||
|
<div id="courseList" class="module-list">
|
||||||
|
<div class="loading-modules">Loading course...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-toggle-container">
|
||||||
|
<span class="theme-toggle-label">Toggle Theme</span>
|
||||||
|
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
|
||||||
|
<span class="theme-toggle-sr">Toggle theme</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
class="theme-toggle__inner-moon"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
>
|
||||||
|
<path d="M27.5 11.5v-7h-7L16 0l-4.5 4.5h-7v7L0 16l4.5 4.5v7h7L16 32l4.5-4.5h7v-7L32 16l-4.5-4.5zM16 25.4a9.39 9.39 0 1 1 0-18.8 9.39 9.39 0 1 1 0 18.8z" />
|
||||||
|
<circle cx="16" cy="16" r="8.1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resize-handle" id="resizeHandle"></div>
|
||||||
|
</aside>
|
||||||
|
<main class="content" id="content">
|
||||||
|
<div id="results" class="results-container">
|
||||||
|
<div class="welcome-message">
|
||||||
|
<h1>Python Documentation</h1>
|
||||||
|
<p>Search for any Python object in the sidebar to view its documentation.</p>
|
||||||
|
<p>Select a language to automatically translate the documentation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="/static/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
461
static/index.js
Normal file
461
static/index.js
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
// Theme management
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
// Load saved theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
html.setAttribute('data-theme', savedTheme);
|
||||||
|
|
||||||
|
themeToggle.addEventListener('click', () => {
|
||||||
|
const currentTheme = html.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
html.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sidebar reference (no collapse functionality)
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
|
||||||
|
// Sidebar resizing
|
||||||
|
const resizeHandle = document.getElementById('resizeHandle');
|
||||||
|
let isResizing = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startWidth = 0;
|
||||||
|
|
||||||
|
resizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
isResizing = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startWidth = parseInt(window.getComputedStyle(sidebar).width, 10);
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', stopResize);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResize(e) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
const width = startWidth + e.clientX - startX;
|
||||||
|
const minWidth = 200;
|
||||||
|
const maxWidth = 600;
|
||||||
|
if (width >= minWidth && width <= maxWidth) {
|
||||||
|
sidebar.style.width = `${width}px`;
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', `${width}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
isResizing = false;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
const navTabs = document.querySelectorAll('.nav-tab');
|
||||||
|
const docsTab = document.getElementById('docsTab');
|
||||||
|
const courseTab = document.getElementById('courseTab');
|
||||||
|
|
||||||
|
navTabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const tabName = tab.dataset.tab;
|
||||||
|
|
||||||
|
// Update active tab
|
||||||
|
navTabs.forEach(t => t.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
|
||||||
|
// Show/hide tab content
|
||||||
|
if (tabName === 'docs') {
|
||||||
|
docsTab.style.display = 'block';
|
||||||
|
courseTab.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
docsTab.style.display = 'none';
|
||||||
|
courseTab.style.display = 'block';
|
||||||
|
loadCourseContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API interaction
|
||||||
|
const objectInput = document.getElementById('objectInput');
|
||||||
|
const langSelect = document.getElementById('langSelect');
|
||||||
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
|
const resultsContainer = document.getElementById('results');
|
||||||
|
const moduleListContainer = document.getElementById('moduleList');
|
||||||
|
const courseListContainer = document.getElementById('courseList');
|
||||||
|
|
||||||
|
// Current state
|
||||||
|
let currentObject = '';
|
||||||
|
let currentLang = '';
|
||||||
|
|
||||||
|
// Load module list
|
||||||
|
async function loadModuleList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/modules');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'module-items';
|
||||||
|
|
||||||
|
// Add builtins section
|
||||||
|
if (data.builtins && data.builtins.length > 0) {
|
||||||
|
const builtinsSection = document.createElement('div');
|
||||||
|
builtinsSection.className = 'module-section';
|
||||||
|
const builtinsTitle = document.createElement('h4');
|
||||||
|
builtinsTitle.textContent = 'Builtins';
|
||||||
|
builtinsTitle.className = 'module-section-title';
|
||||||
|
builtinsSection.appendChild(builtinsTitle);
|
||||||
|
|
||||||
|
const builtinsList = document.createElement('div');
|
||||||
|
builtinsList.className = 'module-items-list';
|
||||||
|
data.builtins.slice(0, 20).forEach(item => {
|
||||||
|
const btn = createModuleButton(item.full_name || `builtins.${item.name}`, item.name);
|
||||||
|
builtinsList.appendChild(btn);
|
||||||
|
});
|
||||||
|
builtinsSection.appendChild(builtinsList);
|
||||||
|
list.appendChild(builtinsSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add modules section
|
||||||
|
if (data.modules && data.modules.length > 0) {
|
||||||
|
const modulesSection = document.createElement('div');
|
||||||
|
modulesSection.className = 'module-section';
|
||||||
|
const modulesTitle = document.createElement('h4');
|
||||||
|
modulesTitle.textContent = 'Standard Library';
|
||||||
|
modulesTitle.className = 'module-section-title';
|
||||||
|
modulesSection.appendChild(modulesTitle);
|
||||||
|
|
||||||
|
const modulesList = document.createElement('div');
|
||||||
|
modulesList.className = 'module-items-list';
|
||||||
|
data.modules.forEach(item => {
|
||||||
|
const btn = createModuleButton(item.name, item.name);
|
||||||
|
modulesList.appendChild(btn);
|
||||||
|
});
|
||||||
|
modulesSection.appendChild(modulesList);
|
||||||
|
list.appendChild(modulesSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleListContainer.innerHTML = '';
|
||||||
|
moduleListContainer.appendChild(list);
|
||||||
|
} catch (error) {
|
||||||
|
moduleListContainer.innerHTML = `<div class="error-message">Error loading modules: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModuleButton(fullName, displayName) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'module-item';
|
||||||
|
btn.textContent = displayName;
|
||||||
|
btn.title = fullName;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
objectInput.value = fullName;
|
||||||
|
currentObject = fullName;
|
||||||
|
fetchDocumentation();
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load modules on page load
|
||||||
|
loadModuleList();
|
||||||
|
|
||||||
|
// Auto-translate when language changes
|
||||||
|
langSelect.addEventListener('change', () => {
|
||||||
|
if (currentObject) {
|
||||||
|
currentLang = langSelect.value;
|
||||||
|
fetchDocumentation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchDocumentation() {
|
||||||
|
const objectName = objectInput.value.trim();
|
||||||
|
const targetLang = langSelect.value;
|
||||||
|
|
||||||
|
if (!objectName) {
|
||||||
|
resultsContainer.innerHTML = '<div class="error-message">Please enter a Python object name.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentObject = objectName;
|
||||||
|
currentLang = targetLang;
|
||||||
|
|
||||||
|
searchBtn.disabled = true;
|
||||||
|
searchBtn.textContent = 'Loading...';
|
||||||
|
resultsContainer.innerHTML = '<div class="loading">Loading documentation...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ object: objectName });
|
||||||
|
if (targetLang) {
|
||||||
|
params.append('lang', targetLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/docs?${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
resultsContainer.innerHTML = `<div class="error-message">Error: ${data.error}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(data, targetLang);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
resultsContainer.innerHTML = `<div class="error-message">Network error: ${error.message}</div>`;
|
||||||
|
} finally {
|
||||||
|
searchBtn.disabled = false;
|
||||||
|
searchBtn.textContent = 'Get Documentation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(data, targetLang) {
|
||||||
|
// Python docs style - no card, clean layout
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
|
||||||
|
// Header with title
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'doc-header';
|
||||||
|
|
||||||
|
const title = document.createElement('h1');
|
||||||
|
title.className = 'doc-title';
|
||||||
|
title.textContent = data.object_name;
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'doc-meta';
|
||||||
|
meta.innerHTML = `
|
||||||
|
<span class="type-badge">${data.object_type || 'unknown'}</span>
|
||||||
|
${data.cached ? '<span class="doc-badge">Cached</span>' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
header.appendChild(title);
|
||||||
|
header.appendChild(meta);
|
||||||
|
wrapper.appendChild(header);
|
||||||
|
|
||||||
|
// Signature
|
||||||
|
if (data.signature) {
|
||||||
|
const signature = document.createElement('div');
|
||||||
|
signature.className = 'doc-signature';
|
||||||
|
signature.textContent = data.signature;
|
||||||
|
wrapper.appendChild(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main documentation content
|
||||||
|
const docText = data.translated || data.original;
|
||||||
|
|
||||||
|
if (docText) {
|
||||||
|
const docSection = document.createElement('section');
|
||||||
|
docSection.className = 'doc-section';
|
||||||
|
|
||||||
|
const docTextEl = document.createElement('div');
|
||||||
|
docTextEl.className = 'doc-text';
|
||||||
|
docTextEl.textContent = docText;
|
||||||
|
|
||||||
|
docSection.appendChild(docTextEl);
|
||||||
|
wrapper.appendChild(docSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show original if translation exists (collapsible)
|
||||||
|
if (data.translated && data.original) {
|
||||||
|
const originalSection = document.createElement('details');
|
||||||
|
originalSection.className = 'doc-section';
|
||||||
|
originalSection.innerHTML = `
|
||||||
|
<summary style="cursor: pointer; font-weight: 500; margin-bottom: 0.5rem; color: var(--text-secondary);">
|
||||||
|
Original Documentation (English)
|
||||||
|
</summary>
|
||||||
|
<div class="doc-text" style="margin-top: 0.5rem;">${data.original}</div>
|
||||||
|
`;
|
||||||
|
wrapper.appendChild(originalSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.original && !data.translated) {
|
||||||
|
const noDoc = document.createElement('div');
|
||||||
|
noDoc.className = 'doc-text';
|
||||||
|
noDoc.textContent = 'No documentation available for this object.';
|
||||||
|
wrapper.appendChild(noDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
resultsContainer.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load course content
|
||||||
|
async function loadCourseContent() {
|
||||||
|
if (courseListContainer.querySelector('.course-loaded')) {
|
||||||
|
return; // Already loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/course');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'module-items';
|
||||||
|
list.classList.add('course-loaded');
|
||||||
|
|
||||||
|
if (data.sections && data.sections.length > 0) {
|
||||||
|
data.sections.forEach(section => {
|
||||||
|
const sectionDiv = document.createElement('div');
|
||||||
|
sectionDiv.className = 'module-section';
|
||||||
|
|
||||||
|
const sectionTitle = document.createElement('h4');
|
||||||
|
sectionTitle.textContent = section.title;
|
||||||
|
sectionTitle.className = 'module-section-title';
|
||||||
|
sectionDiv.appendChild(sectionTitle);
|
||||||
|
|
||||||
|
const itemsList = document.createElement('div');
|
||||||
|
itemsList.className = 'module-items-list';
|
||||||
|
|
||||||
|
// Create clickable navigation item
|
||||||
|
const navBtn = document.createElement('button');
|
||||||
|
navBtn.className = 'module-item';
|
||||||
|
navBtn.textContent = section.title;
|
||||||
|
navBtn.addEventListener('click', () => {
|
||||||
|
// Scroll to section in main content
|
||||||
|
const sectionId = section.id || section.title.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const sectionElement = document.getElementById(`section-${sectionId}`);
|
||||||
|
if (sectionElement) {
|
||||||
|
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else {
|
||||||
|
// Try to find by heading ID
|
||||||
|
const headingId = section.title.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.trim();
|
||||||
|
const headingElement = document.getElementById(headingId);
|
||||||
|
if (headingElement) {
|
||||||
|
headingElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemsList.appendChild(navBtn);
|
||||||
|
|
||||||
|
// Add subsections if any
|
||||||
|
if (section.subsections && section.subsections.length > 0) {
|
||||||
|
section.subsections.forEach(subsection => {
|
||||||
|
const subBtn = document.createElement('button');
|
||||||
|
subBtn.className = 'module-item';
|
||||||
|
subBtn.style.paddingLeft = '1.5rem';
|
||||||
|
subBtn.textContent = subsection.title;
|
||||||
|
subBtn.addEventListener('click', () => {
|
||||||
|
const subId = subsection.id || subsection.title.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
const subElement = document.getElementById(`subsection-${subId}`);
|
||||||
|
if (subElement) {
|
||||||
|
subElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemsList.appendChild(subBtn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionDiv.appendChild(itemsList);
|
||||||
|
list.appendChild(sectionDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
courseListContainer.innerHTML = '';
|
||||||
|
courseListContainer.appendChild(list);
|
||||||
|
|
||||||
|
// Also display course content in main area
|
||||||
|
displayCourseContent(data);
|
||||||
|
} catch (error) {
|
||||||
|
courseListContainer.innerHTML = `<div class="error-message">Error loading course: ${error.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayCourseContent(courseData) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'course-content';
|
||||||
|
|
||||||
|
const title = document.createElement('h1');
|
||||||
|
title.textContent = courseData.title || 'Python Course';
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
|
||||||
|
if (courseData.sections && courseData.sections.length > 0) {
|
||||||
|
courseData.sections.forEach(section => {
|
||||||
|
const sectionDiv = document.createElement('section');
|
||||||
|
sectionDiv.className = 'course-section';
|
||||||
|
sectionDiv.id = `section-${section.id || section.title.toLowerCase().replace(/\s+/g, '-')}`;
|
||||||
|
|
||||||
|
// Parse and render markdown
|
||||||
|
if (section.markdown) {
|
||||||
|
// Configure marked options
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function(code, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Highlight error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
},
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const htmlContent = marked.parse(section.markdown);
|
||||||
|
|
||||||
|
// Create a container for the markdown content
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'markdown-content';
|
||||||
|
contentDiv.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// Add IDs to headings for navigation
|
||||||
|
contentDiv.querySelectorAll('h1, h2, h3, h4').forEach((heading) => {
|
||||||
|
const text = heading.textContent.trim();
|
||||||
|
const id = text.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.trim();
|
||||||
|
heading.id = id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight code blocks
|
||||||
|
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
sectionDiv.appendChild(contentDiv);
|
||||||
|
} else if (section.content && section.content.length > 0) {
|
||||||
|
// Fallback to old format
|
||||||
|
section.content.forEach(item => {
|
||||||
|
if (item.startsWith('```')) {
|
||||||
|
const codeDiv = document.createElement('pre');
|
||||||
|
codeDiv.className = 'doc-signature';
|
||||||
|
codeDiv.textContent = item.replace(/```python\n?/g, '').replace(/```/g, '').trim();
|
||||||
|
sectionDiv.appendChild(codeDiv);
|
||||||
|
} else {
|
||||||
|
const itemDiv = document.createElement('p');
|
||||||
|
itemDiv.className = 'doc-text';
|
||||||
|
itemDiv.textContent = item;
|
||||||
|
sectionDiv.appendChild(itemDiv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(sectionDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
resultsContainer.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLanguageName(langCode) {
|
||||||
|
const langMap = {
|
||||||
|
'de': 'German',
|
||||||
|
'fr': 'French',
|
||||||
|
'es': 'Spanish',
|
||||||
|
'it': 'Italian',
|
||||||
|
'pt': 'Portuguese',
|
||||||
|
'ru': 'Russian'
|
||||||
|
};
|
||||||
|
return langMap[langCode] || langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBtn.addEventListener('click', fetchDocumentation);
|
||||||
|
|
||||||
|
objectInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
fetchDocumentation();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user