This commit is contained in:
2025-11-16 18:01:30 +01:00
commit 858003cb0b
26 changed files with 4712 additions and 0 deletions

48
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,4 @@
# PyPages
A _currently_ experimental PyDoc WebPage, which gets documentation from Python itself!

23
app.py Normal file
View 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
View File

@@ -0,0 +1,2 @@
"""Modules package for pydoc translation service."""

252
modules/cache.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
"""Routes package for Flask API."""

157
routes/api.py Normal file
View 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
View 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();
}

View 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;
}
}

View 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;
}
}

View 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 = '';
}
}
}

View 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;
}
}

View 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' });
}
}
}

View 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;
}
}
}

View 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
}
}

View 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
View 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);
}
}
}
}

View 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

File diff suppressed because it is too large Load Diff

84
static/index.html Normal file
View 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
View 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();
}
});