Files
rocky-man/src/rocky_man/web/generator.py
Stephen Simpson ec32c72363 CUSP-1256 (#1)
* Complete refactor

Signed-off-by: Stephen Simpson <ssimpson89@users.noreply.github.com>

* Complete refactor

Signed-off-by: Stephen Simpson <ssimpson89@users.noreply.github.com>

---------

Signed-off-by: Stephen Simpson <ssimpson89@users.noreply.github.com>
2025-11-20 11:16:33 -06:00

298 lines
9.2 KiB
Python

"""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