"""Web page generator for Rocky Man.""" import gzip import json import logging from pathlib import Path from typing import List, Dict, Any from jinja2 import Environment, FileSystemLoader, select_autoescape from ..models import ManFile logger = logging.getLogger(__name__) class WebGenerator: """Generates web pages and search index for Rocky Man. Handles: - Generating index/search page - Wrapping man page HTML in templates - Creating search index JSON """ def __init__(self, template_dir: Path, output_dir: Path): """Initialize web generator. Args: template_dir: Directory containing Jinja2 templates output_dir: Directory for HTML output """ self.template_dir = Path(template_dir) self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) # Setup Jinja2 environment self.env = Environment( loader=FileSystemLoader(str(self.template_dir)), autoescape=select_autoescape(['html', 'xml']) ) def generate_manpage_html(self, man_file: ManFile, version: str) -> bool: """Generate complete HTML page for a man page. Args: man_file: ManFile with html_content already set version: Rocky Linux version Returns: True if successful """ if not man_file.html_content: logger.warning(f"No HTML content for {man_file.display_name}") return False try: template = self.env.get_template('manpage.html') html = template.render( title=f"{man_file.display_name} - {man_file.package_name} - Rocky Linux {version}", header_title=man_file.display_name, package_name=man_file.package_name, version=version, section=man_file.section, language=man_file.language or 'en', content=man_file.html_content ) # Ensure output path is set if not man_file.html_path: man_file.html_path = self._get_manpage_path(man_file, version) man_file.html_path.parent.mkdir(parents=True, exist_ok=True) with open(man_file.html_path, 'w', encoding='utf-8') as f: f.write(html) return True except Exception as e: logger.error(f"Error generating HTML for {man_file.display_name}: {e}") return False def generate_index(self, version: str, search_data: Dict[str, Any]) -> bool: """Generate search/index page for a version. Args: version: Rocky Linux version search_data: Search index data Returns: True if successful """ try: template = self.env.get_template('index.html') html = template.render( title=f"Rocky Linux {version} Man Pages", version=version, total_pages=len(search_data), packages=sorted(search_data.keys()) ) index_path = self.output_dir / version / 'index.html' index_path.parent.mkdir(parents=True, exist_ok=True) with open(index_path, 'w', encoding='utf-8') as f: f.write(html) logger.info(f"Generated index for version {version}") return True except Exception as e: logger.error(f"Error generating index for {version}: {e}") return False def generate_packages_index(self, version: str, search_data: Dict[str, Any]) -> bool: """Generate full packages index page. Args: version: Rocky Linux version search_data: Search index data Returns: True if successful """ try: # Group packages by first letter packages_by_letter = {} for pkg_name, pages in search_data.items(): first_char = pkg_name[0].upper() if not first_char.isalpha(): first_char = 'other' if first_char not in packages_by_letter: packages_by_letter[first_char] = [] packages_by_letter[first_char].append({ 'name': pkg_name, 'count': len(pages) }) # Sort packages within each letter for letter in packages_by_letter: packages_by_letter[letter].sort(key=lambda x: x['name']) template = self.env.get_template('packages.html') html = template.render( title=f"All Packages - Rocky Linux {version}", version=version, total_packages=len(search_data), packages_by_letter=packages_by_letter ) output_path = self.output_dir / version / 'packages.html' output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(html) logger.info(f"Generated packages index for version {version}") return True except Exception as e: logger.error(f"Error generating packages index for {version}: {e}") return False def generate_search_index( self, man_files: List[ManFile], version: str ) -> Dict[str, Any]: """Generate search index from man files. Args: man_files: List of ManFile objects version: Rocky Linux version Returns: Search index dictionary """ index = {} for man_file in man_files: pkg_name = man_file.package_name if pkg_name not in index: index[pkg_name] = {} # Create entry for this man page entry = { 'name': man_file.name, 'section': man_file.section, 'display_name': man_file.display_name, 'language': man_file.language or 'en', 'url': man_file.uri_path, 'full_name': f"{man_file.package_name} - {man_file.display_name}" } # Use display name as key (handles duplicates with different sections) key = man_file.display_name if man_file.language: key = f"{key}.{man_file.language}" index[pkg_name][key] = entry return index def save_search_index(self, index: Dict[str, Any], version: str) -> bool: """Save search index as JSON (both plain and gzipped). Args: index: Search index dictionary version: Rocky Linux version Returns: True if successful """ try: version_dir = self.output_dir / version version_dir.mkdir(parents=True, exist_ok=True) json_path = version_dir / 'search.json' gz_path = version_dir / 'search.json.gz' # Sort for consistency sorted_index = {k: index[k] for k in sorted(index)} # Save plain JSON with open(json_path, 'w', encoding='utf-8') as f: json.dump(sorted_index, f, indent=2) # Save gzipped JSON with gzip.open(gz_path, 'wt', encoding='utf-8') as f: json.dump(sorted_index, f) logger.info(f"Saved search index for {version} ({len(index)} packages)") return True except Exception as e: logger.error(f"Error saving search index: {e}") return False def _get_manpage_path(self, man_file: ManFile, version: str) -> Path: """Get output path for a man page HTML file. Args: man_file: ManFile object version: Rocky Linux version Returns: Path for HTML file """ version_dir = self.output_dir / version pkg_dir = version_dir / man_file.package_name section_dir = pkg_dir / f"man{man_file.section}" return section_dir / man_file.html_filename def generate_root_index(self, versions: List[str]) -> bool: """Generate root index page linking to all versions. Args: versions: List of Rocky Linux versions Returns: True if successful """ try: template = self.env.get_template('root.html') # Sort versions numerically (e.g., 8.10, 9.6, 10.0) def version_key(v): try: parts = v.split('.') return tuple(int(p) for p in parts) except (ValueError, AttributeError): return (0, 0) html = template.render( title="Rocky Linux Man Pages", versions=sorted(versions, key=version_key) ) index_path = self.output_dir / 'index.html' with open(index_path, 'w', encoding='utf-8') as f: f.write(html) logger.info("Generated root index page") return True except Exception as e: logger.error(f"Error generating root index: {e}") return False