#!/usr/bin/env python3 """ Simple HTTP server for serving the documentation hub locally. This allows proper CORS handling and file serving for the markdown files. """ import http.server import socketserver import json import os import mimetypes from urllib.parse import urlparse, parse_qs from pathlib import Path class DocumentationHandler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(Path(__file__).parent), **kwargs) def end_headers(self): # Add CORS headers self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') super().end_headers() def do_GET(self): # Handle the blogs directory listing API if self.path.startswith('/api/blogs'): self.handle_blogs_api() elif self.path.startswith('/api/search'): self.handle_search_api() else: # Serve static files normally super().do_GET() def handle_blogs_api(self): """API endpoint to list blog files and their metadata""" try: blogs_dir = Path(__file__).parent / 'blogs' if not blogs_dir.exists(): self.send_error(404, "Blogs directory not found") return blogs = [] for md_file in blogs_dir.glob('*.md'): # Get file stats stat = md_file.stat() size_kb = round(stat.st_size / 1024, 1) # Read first few lines to extract title and excerpt try: with open(md_file, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') # Extract title (first # header) title = md_file.stem.replace('-', ' ').title() for line in lines: if line.startswith('# '): title = line[2:].strip() break # Extract excerpt (first paragraph after title) excerpt = "No description available." for i, line in enumerate(lines): if line.startswith('# '): # Look for first non-empty paragraph after title for j in range(i + 1, min(i + 10, len(lines))): if lines[j].strip() and not lines[j].startswith('#'): excerpt = lines[j].strip()[:150] if len(lines[j].strip()) > 150: excerpt += "..." break break blogs.append({ 'filename': md_file.name, 'title': title, 'excerpt': excerpt, 'size': f"{size_kb} KB", 'lastModified': stat.st_mtime }) except Exception as e: print(f"Error reading {md_file}: {e}") continue # Sort by last modified (newest first) blogs.sort(key=lambda x: x['lastModified'], reverse=True) # Format dates import datetime for blog in blogs: blog['lastModified'] = datetime.datetime.fromtimestamp( blog['lastModified'] ).strftime('%Y-%m-%d') # Send JSON response response = json.dumps(blogs, indent=2) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(response.encode('utf-8')) except Exception as e: print(f"Error in blogs API: {e}") self.send_error(500, f"Internal server error: {e}") def handle_search_api(self): """API endpoint to search within blog content""" try: # Parse query parameters parsed_url = urlparse(self.path) query_params = parse_qs(parsed_url.query) search_query = query_params.get('q', [''])[0].lower().strip() if not search_query: self.send_error(400, "Missing search query parameter 'q'") return blogs_dir = Path(__file__).parent / 'blogs' if not blogs_dir.exists(): self.send_error(404, "Blogs directory not found") return search_results = [] for md_file in blogs_dir.glob('*.md'): try: with open(md_file, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') # Extract title title = md_file.stem.replace('-', ' ').title() for line in lines: if line.startswith('# '): title = line[2:].strip() break # Check if search term is in title, filename, or content filename_match = search_query in md_file.name.lower() title_match = search_query in title.lower() content_match = search_query in content.lower() if filename_match or title_match or content_match: # Get file stats stat = md_file.stat() size_kb = round(stat.st_size / 1024, 1) # Extract excerpt (preferably containing search term) excerpt = self.extract_search_excerpt(content, search_query) # Calculate relevance score relevance = 0 if filename_match: relevance += 3 if title_match: relevance += 2 if content_match: relevance += 1 search_results.append({ 'filename': md_file.name, 'title': title, 'excerpt': excerpt, 'size': f"{size_kb} KB", 'lastModified': stat.st_mtime, 'relevance': relevance, 'matches': { 'filename': filename_match, 'title': title_match, 'content': content_match } }) except Exception as e: print(f"Error searching {md_file}: {e}") continue # Sort by relevance, then by last modified search_results.sort(key=lambda x: (x['relevance'], x['lastModified']), reverse=True) # Format dates import datetime for result in search_results: result['lastModified'] = datetime.datetime.fromtimestamp( result['lastModified'] ).strftime('%Y-%m-%d') # Remove relevance from final response del result['relevance'] # Send JSON response response = json.dumps({ 'query': search_query, 'results': search_results, 'total': len(search_results) }, indent=2) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(response.encode('utf-8')) except Exception as e: print(f"Error in search API: {e}") self.send_error(500, f"Internal server error: {e}") def extract_search_excerpt(self, content, search_term, context_chars=150): """Extract excerpt around search term or fallback to beginning""" content_lower = content.lower() search_pos = content_lower.find(search_term) if search_pos == -1: # If search term not found, return beginning of content lines = content.split('\n') for line in lines: if line.strip() and not line.startswith('#'): excerpt = line.strip()[:context_chars] if len(line.strip()) > context_chars: excerpt += "..." return excerpt return "No description available." # Extract context around the search term start = max(0, search_pos - context_chars // 2) end = min(len(content), search_pos + len(search_term) + context_chars // 2) excerpt = content[start:end].strip() # Clean up excerpt (remove incomplete words at edges) if start > 0: space_pos = excerpt.find(' ') if space_pos > 0: excerpt = excerpt[space_pos:].strip() excerpt = "..." + excerpt if end < len(content): last_space = excerpt.rfind(' ') if last_space > 0: excerpt = excerpt[:last_space].strip() excerpt += "..." return excerpt def run_server(port=8000): """Start the documentation server""" try: with socketserver.TCPServer(("", port), DocumentationHandler) as httpd: print(f"🌐 Server running at: http://localhost:{port}") print(f"šŸ“ Serving from: {Path(__file__).parent}") print(f"šŸ” Open http://localhost:{port} in your browser") print() httpd.serve_forever() except KeyboardInterrupt: print("\nšŸ‘‹ Server stopped by user") except OSError as e: if e.errno == 10048: # Windows: Address already in use print(f"āŒ Port {port} is already in use. Try a different port:") print(f" python server.py --port 8001") else: print(f"āŒ Error starting server: {e}") if __name__ == "__main__": import sys port = 8000 # Simple command line argument parsing if len(sys.argv) > 1: for i, arg in enumerate(sys.argv): if arg in ['--port', '-p'] and i + 1 < len(sys.argv): try: port = int(sys.argv[i + 1]) except ValueError: print("āŒ Invalid port number") sys.exit(1) elif arg in ['--help', '-h']: print("Usage: python server.py [--port PORT]") print("Options:") print(" --port, -p PORT Port to run server on (default: 8000)") print(" --help, -h Show this help message") sys.exit(0) run_server(port)