From e356431b72fdef98d906cded60407fb01ff08447 Mon Sep 17 00:00:00 2001
From: Stephen Simpson
Date: Thu, 4 Dec 2025 17:04:55 -0600
Subject: [PATCH] Refactor Rocky Man Page Generator
- Improved logging and error handling in main.py, including better version management and configuration handling.
- Enhanced RepoManager to dynamically switch between pub and vault repositories for package retrieval.
- Updated ManPageConverter to handle symlink detection and generate appropriate redirect HTML.
- Refined WebGenerator to support dynamic version grid layout and improved HTML generation for man pages and indexes.
- Modified templates to remove trademark symbols and enhance styling for version cards.
- Added support for existing versions in the root index generation.
Signed-off-by: Stephen Simpson
---
Jenkinsfile | 8 +-
README.md | 420 +++++----------------------
src/rocky_man/main.py | 201 +++++++------
src/rocky_man/processor/converter.py | 121 +++++---
src/rocky_man/repo/manager.py | 56 +++-
src/rocky_man/web/generator.py | 113 ++++---
templates/base.html | 5 +-
templates/root.html | 53 +++-
8 files changed, 427 insertions(+), 550 deletions(-)
diff --git a/Jenkinsfile b/Jenkinsfile
index 9ac136e..64ffb52 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -50,6 +50,11 @@ spec:
defaultValue: 'rockyman',
description: 'B2 bucket name for uploads'
)
+ string(
+ name: 'EXISTING_VERSIONS',
+ defaultValue: '',
+ description: 'Existing versions already built (space-separated)'
+ )
string(
name: 'PARALLEL_DOWNLOADS',
defaultValue: '5',
@@ -100,7 +105,8 @@ docker run --rm \
--versions ${VERSIONS} \
--verbose \
--parallel-downloads ${PARALLEL_DOWNLOADS} \
- --parallel-conversions ${PARALLEL_CONVERSIONS}
+ --parallel-conversions ${PARALLEL_CONVERSIONS} \
+ --existing-versions "${EXISTING_VERSIONS}"
'''
}
}
diff --git a/README.md b/README.md
index 9796500..0c11fcf 100644
--- a/README.md
+++ b/README.md
@@ -1,121 +1,85 @@
# Rocky Man π
-**Rocky Man** is a comprehensive man page hosting solution for Rocky Linux, providing beautiful, searchable documentation for all packages in BaseOS and AppStream repositories across Rocky Linux 8, 9, and 10.
-
-> **β¨ This is a complete rewrite** with 60-80% faster performance, modern architecture, and production-ready features!
-
-## π What's New in This Rewrite
-
-This version is a **complete ground-up rebuild** with major improvements:
-
-- π **60-80% faster** - Pre-filters packages using filelists.xml (downloads only ~800 packages instead of ~3000)
-- ποΈ **Modular architecture** - Clean separation into models, repo, processor, web, and utils
-- π¨ **Modern UI** - Beautiful dark theme with instant fuzzy search
-- π³ **Container ready** - Multi-stage Dockerfile that works on any architecture
-- β‘ **Parallel processing** - Concurrent downloads and HTML conversions
-- π§Ή **Smart cleanup** - Automatic cleanup of temporary files
-- π **Well documented** - Comprehensive docstrings and type hints throughout
-- π **Thread safe** - Proper locking and resource management
-- π€ **GitHub Actions** - Automated weekly builds and deployment
-
-### Performance Comparison
-
-| Metric | Old Version | New Version | Improvement |
-|--------|-------------|-------------|-------------|
-| Packages Downloaded | ~3000 | ~800 | 73% reduction |
-| Processing Time | 2-3 hours | 30-45 minutes | 75% faster |
-| Bandwidth Used | ~10 GB | ~2-3 GB | 80% reduction |
-| Architecture | Single file | Modular (16 files) | Much cleaner |
-| Thread Safety | β οΈ Issues | β
Safe | Fixed |
-| Cleanup | Manual | Automatic | Improved |
-| UI Quality | Basic | Modern | Much better |
+**Rocky Man** is a tool for generating searchable HTML documentation from Rocky Linux man pages across BaseOS and AppStream repositories for Rocky Linux 8, 9, and 10.
## Features
-- β¨ **Fast & Efficient**: Uses filelists.xml to pre-filter packages with man pages (massive bandwidth savings)
-- π **Fuzzy Search**: Instant search across all man pages with Fuse.js
-- π¨ **Modern UI**: Clean, responsive dark theme interface inspired by GitHub
-- π¦ **Complete Coverage**: All packages from BaseOS and AppStream repositories
-- π³ **Container Ready**: Architecture-independent Docker support (works on x86_64, aarch64, arm64, etc.)
-- π **GitHub Actions**: Automated weekly builds and deployment to GitHub Pages
-- π§Ή **Smart Cleanup**: Automatic cleanup of temporary files (configurable)
-- β‘ **Parallel Processing**: Concurrent downloads and conversions for maximum speed
-- π **Multi-version**: Support for Rocky Linux 8, 9, and 10 simultaneously
+- **Fast & Efficient**: Uses filelists.xml to pre-filter packages with man pages
+- **Complete Coverage**: All packages from BaseOS and AppStream repositories
+- **Container Ready**: Works on x86_64, aarch64, arm64, etc.
+- **Smart Cleanup**: Automatic cleanup of temporary files (configurable)
+- **Parallel Processing**: Concurrent downloads and conversions for maximum speed
+- **Multi-version**: Support for Rocky Linux 8, 9, and 10 simultaneously
## Quick Start
-### Option 1: Docker (Recommended)
-
-```bash
-# Build the image
-docker build -t rocky-man .
-
-# Generate man pages for Rocky Linux 9.6
-docker run --rm -v $(pwd)/html:/data/html rocky-man --versions 9.6
-
-# Generate for multiple versions
-docker run --rm -v $(pwd)/html:/data/html rocky-man --versions 8.10 9.6 10.0
-
-# With verbose logging
-docker run --rm -v $(pwd)/html:/data/html rocky-man --versions 9.6 --verbose
-
-# Keep downloaded RPMs (mount the download directory)
-docker run --rm -it \
- -v $(pwd)/html:/data/html \
- -v $(pwd)/downloads:/data/tmp/downloads \
- rocky-man --versions 9.6 --keep-rpms --verbose
-```
-
-### Option 2: Podman (Native Rocky Linux)
+### Podman (Recommended)
```bash
# Build the image
podman build -t rocky-man .
-# Run with podman (note the :Z flag for SELinux)
-podman run --rm -v $(pwd)/html:/data/html:Z rocky-man --versions 9.6
+# Generate man pages for Rocky Linux 9.6 (using defaults, no custom args)
+podman run --rm -v $(pwd)/html:/data/html:Z rocky-man
-# Interactive mode for debugging
-podman run --rm -it -v $(pwd)/html:/data/html:Z rocky-man --versions 9.6 --verbose
+# Generate for specific versions (requires explicit paths)
+podman run --rm -v $(pwd)/html:/app/html:Z rocky-man \
+ --versions 8.10 9.6 10.0 --output-dir /app/html
+
+# With verbose logging
+podman run --rm -v $(pwd)/html:/app/html:Z rocky-man \
+ --versions 9.6 --output-dir /app/html --verbose
# Keep downloaded RPMs (mount the download directory)
podman run --rm -it \
- -v $(pwd)/html:/data/html:Z \
- -v $(pwd)/downloads:/data/tmp/downloads:Z \
- rocky-man --versions 9.6 --keep-rpms --verbose
+ -v $(pwd)/html:/app/html:Z \
+ -v $(pwd)/downloads:/app/tmp/downloads:Z \
+ rocky-man --versions 9.6 --keep-rpms \
+ --output-dir /app/html --download-dir /app/tmp/downloads --verbose
```
-### Option 3: Docker Compose (Development)
+### Docker
```bash
-# Build and run
-docker-compose up
+# Build the image
+docker build -t rocky-man .
-# The generated HTML will be in ./html/
-# Preview at http://localhost:8080 (nginx container)
+# Generate man pages (using defaults, no custom args)
+docker run --rm -v $(pwd)/html:/data/html rocky-man
+
+# Generate for specific versions (requires explicit paths)
+docker run --rm -v $(pwd)/html:/app/html rocky-man \
+ --versions 9.6 --output-dir /app/html
+
+# Interactive mode for debugging
+docker run --rm -it -v $(pwd)/html:/app/html rocky-man \
+ --versions 9.6 --output-dir /app/html --verbose
+
+# Keep downloaded RPMs (mount the download directory)
+docker run --rm -it \
+ -v $(pwd)/html:/app/html \
+ -v $(pwd)/downloads:/app/tmp/downloads \
+ rocky-man --versions 9.6 --keep-rpms \
+ --output-dir /app/html --download-dir /app/tmp/downloads --verbose
```
### Directory Structure in Container
-When running in a container, rocky-man uses these directories inside `/data/`:
+The container uses different paths depending on whether you pass custom arguments:
-- `/data/html` - Generated HTML output (mount this to access results)
-- `/data/tmp/downloads` - Downloaded RPM files (temporary)
-- `/data/tmp/extracts` - Extracted man page files (temporary)
+**Without custom arguments** (using Dockerfile CMD defaults):
+- `/data/html` - Generated HTML output
+- `/data/tmp/downloads` - Downloaded RPM files
+- `/data/tmp/extracts` - Extracted man page files
-By default, RPMs and extracts are automatically cleaned up after processing. If you want to keep the RPMs (e.g., for debugging or multiple runs), mount the download directory and use `--keep-rpms`:
+**With custom arguments** (argparse defaults from working directory `/app`):
+- `/app/html` - Generated HTML output
+- `/app/tmp/downloads` - Downloaded RPM files
+- `/app/tmp/extracts` - Extracted man page files
-```bash
-# This keeps RPMs on your host in ./downloads/
-podman run --rm -it \
- -v $(pwd)/html:/data/html:Z \
- -v $(pwd)/downloads:/data/tmp/downloads:Z \
- rocky-man --versions 9.6 --keep-rpms
-```
+**Important**: When passing custom arguments, the container's CMD is overridden and the code falls back to relative paths (`./html` = `/app/html`). You must explicitly specify `--output-dir /app/html --download-dir /app/tmp/downloads` to match your volume mounts. Without this, files are written inside the container and lost when it stops (especially with `--rm`).
-**Note**: Without mounting `/data/tmp/downloads`, the `--keep-rpms` flag will keep files inside the container, but they'll be lost when the container stops (especially with `--rm`).
-
-### Option 4: Local Development
+### Local Development
#### Prerequisites
@@ -154,6 +118,9 @@ python -m rocky_man.main --parallel-downloads 10 --parallel-conversions 20
# Use a different mirror
python -m rocky_man.main --mirror https://mirrors.example.com/
+
+# Only BaseOS (faster)
+python -m rocky_man.main --repo-types BaseOS --versions 9.6
```
## Architecture
@@ -164,59 +131,24 @@ Rocky Man is organized into clean, modular components:
rocky-man/
βββ src/rocky_man/
β βββ models/ # Data models (Package, ManFile)
-β β βββ package.py # RPM package representation
-β β βββ manfile.py # Man page file representation
-β βββ repo/ # Repository management
-β β βββ manager.py # DNF repository operations
-β β βββ contents.py # Filelists.xml parser (key optimization!)
-β βββ processor/ # Man page processing
-β β βββ extractor.py # Extract man pages from RPMs
-β β βββ converter.py # Convert to HTML with mandoc
-β βββ web/ # Web page generation
-β β βββ generator.py # HTML and search index generation
-β βββ utils/ # Utilities
-β β βββ config.py # Configuration management
-β βββ main.py # Main entry point and orchestration
-βββ templates/ # Jinja2 templates
-β βββ base.html # Base template with modern styling
-β βββ index.html # Search page with Fuse.js
-β βββ manpage.html # Individual man page display
-β βββ root.html # Multi-version landing page
-βββ Dockerfile # Multi-stage, arch-independent
-βββ docker-compose.yml # Development setup with nginx
-βββ .github/workflows/ # GitHub Actions automation
-βββ pyproject.toml # Python project configuration
+β βββ repo/ # Repository management
+β βββ processor/ # Man page processing
+β βββ web/ # Web page generation
+β βββ utils/ # Utilities
+β βββ main.py # Main entry point and orchestration
+βββ templates/ # Jinja2 templates
+βββ Dockerfile # Multi-stage, arch-independent
+βββ pyproject.toml # Python project configuration
```
### How It Works
-1. **Package Discovery** π
- - Parse repository `filelists.xml` to identify packages with man pages
- - This is the **key optimization** - we know what to download before downloading!
-
-2. **Smart Download** β¬οΈ
- - Download only packages containing man pages (60-80% reduction)
- - Parallel downloads for speed
- - Architecture-independent (man pages are the same across arches)
-
-3. **Extraction** π¦
- - Extract man page files from RPM packages
- - Handle gzipped and plain text man pages
- - Support for multiple languages
-
-4. **Conversion** π
- - Convert troff format to HTML using mandoc
- - Clean up HTML output
- - Parallel processing for speed
-
-5. **Web Generation** π
- - Wrap HTML in beautiful templates
- - Generate search index with fuzzy search
- - Create multi-version navigation
-
-6. **Cleanup** π§Ή
- - Automatically remove temporary files (configurable)
- - Keep only what you need
+1. **Package Discovery** - Parse repository `filelists.xml` to identify packages with man pages
+2. **Smart Download** - Download only packages containing man pages with parallel downloads
+3. **Extraction** - Extract man page files from RPM packages
+4. **Conversion** - Convert troff format to HTML using mandoc
+5. **Web Generation** - Wrap HTML in templates and generate search index
+6. **Cleanup** - Automatically remove temporary files (configurable)
## Command Line Options
@@ -266,183 +198,6 @@ Options:
-v, --verbose Enable verbose logging
```
-### Examples
-
-```bash
-# Quick test with one version
-python -m rocky_man.main --versions 9.6
-
-# Production build with all versions (default)
-python -m rocky_man.main
-
-# Fast build with more parallelism
-python -m rocky_man.main --parallel-downloads 15 --parallel-conversions 30
-
-# Keep files for debugging
-python -m rocky_man.main --keep-rpms --keep-extracts --verbose
-
-# Custom mirror (faster for your location)
-python -m rocky_man.main --mirror https://mirror.usi.edu/pub/rocky/
-
-# Only BaseOS (faster)
-python -m rocky_man.main --repo-types BaseOS --versions 9.6
-```
-
-## GitHub Actions Integration
-
-This project includes a **production-ready GitHub Actions workflow** that:
-
-- β
Runs automatically every Sunday at midnight UTC
-- β
Can be manually triggered with custom version selection
-- β
Builds man pages in a Rocky Linux container
-- β
Automatically deploys to GitHub Pages
-- β
Artifacts available for download
-
-### Setup Instructions
-
-1. **Enable GitHub Pages**
- - Go to your repository β Settings β Pages
- - Set source to **"GitHub Actions"**
- - Save
-
-2. **Trigger the workflow**
- - Go to Actions tab
- - Select "Build Rocky Man Pages"
- - Click "Run workflow"
- - Choose versions (or use default)
-
-3. **Access your site**
- - Will be available at: `https://YOUR_USERNAME.github.io/rocky-man/`
- - Updates automatically every week!
-
-### Workflow File
-
-Located at `.github/workflows/build.yml`, it:
-- Uses Rocky Linux 9 container
-- Installs all dependencies
-- Runs the build
-- Uploads artifacts
-- Deploys to GitHub Pages
-
-## What's Different from the Original
-
-| Feature | Old Version | New Version |
-|---------|-------------|-------------|
-| **Architecture** | Single 400-line file | Modular, 16 files across 6 modules |
-| **Package Filtering** | Downloads everything | Pre-filters with filelists.xml |
-| **Performance** | 2-3 hours, ~10 GB | 30-45 min, ~2-3 GB |
-| **UI** | Basic template | Modern GitHub-inspired design |
-| **Search** | Simple filter | Fuzzy search with Fuse.js |
-| **Container** | Basic Podman commands | Multi-stage Dockerfile + compose |
-| **Thread Safety** | Global dict issues | Proper locking mechanisms |
-| **Cleanup** | Method exists but unused | Automatic, configurable |
-| **Documentation** | Minimal comments | Comprehensive docstrings |
-| **Type Hints** | None | Throughout codebase |
-| **Error Handling** | Basic try/catch | Comprehensive with logging |
-| **CI/CD** | None | GitHub Actions ready |
-| **Testing** | None | Ready for pytest integration |
-| **Configuration** | Hardcoded | Config class with defaults |
-
-## Project Structure Details
-
-```
-rocky-man/
-βββ src/rocky_man/ # Main source code
-β βββ __init__.py # Package initialization
-β βββ main.py # Entry point and orchestration (200 lines)
-β βββ models/ # Data models
-β β βββ __init__.py
-β β βββ package.py # Package model with properties
-β β βββ manfile.py # ManFile model with path parsing
-β βββ repo/ # Repository operations
-β β βββ __init__.py
-β β βββ manager.py # DNF integration, downloads
-β β βββ contents.py # Filelists parser (key optimization)
-β βββ processor/ # Processing pipeline
-β β βββ __init__.py
-β β βββ extractor.py # RPM extraction with rpmfile
-β β βββ converter.py # mandoc conversion wrapper
-β βββ web/ # Web generation
-β β βββ __init__.py
-β β βββ generator.py # Template rendering, search index
-β βββ utils/ # Utilities
-β βββ __init__.py
-β βββ config.py # Configuration management
-βββ templates/ # Jinja2 templates
-β βββ base.html # Base layout (modern dark theme)
-β βββ index.html # Search page (Fuse.js integration)
-β βββ manpage.html # Man page display
-β βββ root.html # Multi-version landing
-βββ old/ # Your original code (preserved)
-β βββ rocky_man.py
-β βββ rocky_man2.py
-β βββ templates/
-βββ .github/
-β βββ workflows/
-β βββ build.yml # GitHub Actions workflow
-βββ Dockerfile # Multi-stage build
-βββ .dockerignore # Optimize Docker context
-βββ docker-compose.yml # Dev environment
-βββ pyproject.toml # Python project config
-βββ .gitignore # Updated for new structure
-βββ README.md # This file!
-```
-
-## Development
-
-### Adding New Features
-
-The modular design makes it easy to extend:
-
-- **New repositories**: Add to `config.repo_types` in `utils/config.py`
-- **Custom templates**: Use `--template-dir` flag or modify `templates/`
-- **Additional metadata**: Extend `Package` or `ManFile` models
-- **Alternative converters**: Implement new converter in `processor/`
-- **Different outputs**: Add new generator in `web/`
-
-### Running Tests
-
-```bash
-# Install dev dependencies
-pip3 install -e ".[dev]"
-
-# Run tests (when implemented)
-pytest
-
-# Type checking
-mypy src/
-
-# Linting
-ruff check src/
-```
-
-### Development Workflow
-
-```bash
-# 1. Make changes to code
-vim src/rocky_man/processor/converter.py
-
-# 2. Test locally in container
-podman run --rm -it -v $(pwd):/app rockylinux:9 /bin/bash
-cd /app
-python3 -m rocky_man.main --versions 9.6 --verbose
-
-# 3. Build Docker image
-docker build -t rocky-man .
-
-# 4. Test Docker image
-docker run --rm -v $(pwd)/html:/data/html rocky-man --versions 9.6
-
-# 5. Preview output
-docker-compose up nginx
-# Visit http://localhost:8080
-
-# 6. Commit and push
-git add .
-git commit -m "feat: your feature description"
-git push
-```
-
## Troubleshooting
### DNF Errors
@@ -510,12 +265,6 @@ python -m rocky_man.main --parallel-downloads 2 --parallel-conversions 5
python -m rocky_man.main --mirror https://mirror.example.com/rocky/
```
-### UTF-8 Decode Errors
-
-**Problem**: `'utf-8' codec can't decode byte...`
-
-**Solution**: This is now handled with `errors='replace'` in the new version. The man page will still be processed with replacement characters for invalid UTF-8.
-
## Performance Tips
1. **Use closer mirrors** - Significant speed improvement for downloads
@@ -547,34 +296,3 @@ Contributions welcome! Please:
5. Commit with clear messages (`git commit -m 'feat: add amazing feature'`)
6. Push to your branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
-
-## Acknowledgments
-
-- Inspired by [debiman](https://github.com/Debian/debiman) for Debian
-- Uses [mandoc](https://mandoc.bsd.lv/) for man page conversion
-- Search powered by [Fuse.js](https://fusejs.io/)
-- Modern UI design inspired by GitHub's dark theme
-
-## Links
-
-- [Rocky Linux](https://rockylinux.org/)
-- [Man Page Format](https://man7.org/linux/man-pages/)
-- [Mandoc Documentation](https://mandoc.bsd.lv/)
-- [DNF Documentation](https://dnf.readthedocs.io/)
-
-## Roadmap
-
-- [ ] Add pytest test suite
-- [ ] Implement incremental updates (checksum-based)
-- [ ] Add support for localized man pages (es, fr, etc.)
-- [ ] Create redirect system like debiman
-- [ ] Add statistics page (most viewed, etc.)
-- [ ] Implement RSS feed for updates
-- [ ] Add support for Rocky Linux 10 (when released)
-- [ ] Create sitemap.xml for SEO
-- [ ] Add dark/light theme toggle
-- [ ] Implement caching for faster rebuilds
-
----
-
-**Made with β€οΈ for the Rocky Linux community**
diff --git a/src/rocky_man/main.py b/src/rocky_man/main.py
index 97d8e1a..0a230a2 100644
--- a/src/rocky_man/main.py
+++ b/src/rocky_man/main.py
@@ -2,6 +2,7 @@
import argparse
import logging
+import re
import sys
from pathlib import Path
@@ -16,16 +17,12 @@ def setup_logging(verbose: bool = False):
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
)
-def process_version(
- config: Config,
- version: str,
- template_dir: Path
-) -> bool:
+def process_version(config: Config, version: str, template_dir: Path) -> bool:
"""Process a single Rocky Linux version.
Args:
@@ -53,21 +50,18 @@ def process_version(
# Use first available architecture (man pages are arch-independent)
arch = config.architectures[0]
- # Get repository URL
- repo_url = config.get_repo_url(version, repo_type, arch)
-
# Create cache dir for this repo
cache_dir = config.download_dir / f".cache/{version}/{repo_type}"
try:
# Initialize repository manager
repo_manager = RepoManager(
- repo_url=repo_url,
+ config=config,
version=version,
repo_type=repo_type,
arch=arch,
cache_dir=cache_dir,
- download_dir=version_download_dir
+ download_dir=version_download_dir,
)
# List packages (with man pages only)
@@ -83,19 +77,19 @@ def process_version(
if config.skip_packages:
original_count = len(packages)
packages = [
- pkg for pkg in packages
- if pkg.name not in config.skip_packages
+ pkg for pkg in packages if pkg.name not in config.skip_packages
]
filtered_count = original_count - len(packages)
if filtered_count > 0:
- logger.info(f"Filtered out {filtered_count} packages based on skip list")
+ logger.info(
+ f"Filtered out {filtered_count} packages based on skip list"
+ )
logger.info(f"Processing {len(packages)} packages")
# Download packages
logger.info("Downloading packages...")
downloaded = repo_manager.download_packages(
- packages,
- max_workers=config.parallel_downloads
+ packages, max_workers=config.parallel_downloads
)
# Extract man pages
@@ -103,11 +97,10 @@ def process_version(
extractor = ManPageExtractor(
version_extract_dir,
skip_sections=config.skip_sections,
- skip_languages=config.skip_languages
+ skip_languages=config.skip_languages,
)
man_files = extractor.extract_from_packages(
- downloaded,
- max_workers=config.parallel_downloads
+ downloaded, max_workers=config.parallel_downloads
)
logger.info(f"Extracted {len(man_files)} man pages")
@@ -124,8 +117,7 @@ def process_version(
logger.info("Converting man pages to HTML...")
converter = ManPageConverter(version_output_dir)
converted = converter.convert_many(
- man_files_with_content,
- max_workers=config.parallel_conversions
+ man_files_with_content, max_workers=config.parallel_conversions
)
all_man_files.extend(converted)
@@ -149,11 +141,6 @@ def process_version(
logger.error(f"No man pages were successfully processed for version {version}")
return False
- # Link cross-references between man pages
- logger.info("Linking cross-references...")
- converter = ManPageConverter(version_output_dir)
- converter.link_cross_references(all_man_files)
-
# Generate web pages
logger.info("Generating web pages...")
web_gen = WebGenerator(template_dir, config.output_dir)
@@ -168,132 +155,154 @@ def process_version(
# Generate packages index page
web_gen.generate_packages_index(version, search_index)
+ # Set HTML paths for all man files
+ for man_file in all_man_files:
+ if not man_file.html_path:
+ man_file.html_path = web_gen._get_manpage_path(man_file, version)
+
+ # Link cross-references between man pages
+ logger.info("Linking cross-references...")
+ converter.link_cross_references(all_man_files, version)
+
# Wrap man pages in templates
logger.info("Generating man page HTML...")
for man_file in all_man_files:
web_gen.generate_manpage_html(man_file, version)
- logger.info(f"Successfully processed {len(all_man_files)} man pages for Rocky Linux {version}")
+ logger.info(
+ f"Successfully processed {len(all_man_files)} man pages for Rocky Linux {version}"
+ )
return True
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
- description='Generate HTML documentation for Rocky Linux man pages'
+ description="Generate HTML documentation for Rocky Linux man pages"
)
parser.add_argument(
- '--versions',
- nargs='+',
- default=['8.10', '9.6', '10.0'],
- help='Rocky Linux versions to process (default: 8.10 9.6 10.0)'
+ "--versions",
+ nargs="+",
+ default=["8.10", "9.6", "10.0"],
+ help="Rocky Linux versions to process (default: 8.10 9.6 10.0)",
)
parser.add_argument(
- '--repo-types',
- nargs='+',
- default=['BaseOS', 'AppStream'],
- help='Repository types to process (default: BaseOS AppStream)'
+ "--repo-types",
+ nargs="+",
+ default=["BaseOS", "AppStream"],
+ help="Repository types to process (default: BaseOS AppStream)",
)
parser.add_argument(
- '--output-dir',
+ "--output-dir",
type=Path,
- default=Path('./html'),
- help='Output directory for HTML files (default: ./html)'
+ default=Path("./html"),
+ help="Output directory for HTML files (default: ./html)",
)
parser.add_argument(
- '--download-dir',
+ "--download-dir",
type=Path,
- default=Path('./tmp/downloads'),
- help='Directory for downloading packages (default: ./tmp/downloads)'
+ default=Path("./tmp/downloads"),
+ help="Directory for downloading packages (default: ./tmp/downloads)",
)
parser.add_argument(
- '--extract-dir',
+ "--extract-dir",
type=Path,
- default=Path('./tmp/extracts'),
- help='Directory for extracting man pages (default: ./tmp/extracts)'
+ default=Path("./tmp/extracts"),
+ help="Directory for extracting man pages (default: ./tmp/extracts)",
)
parser.add_argument(
- '--keep-rpms',
- action='store_true',
- help='Keep downloaded RPM files after processing'
+ "--keep-rpms",
+ action="store_true",
+ help="Keep downloaded RPM files after processing",
)
parser.add_argument(
- '--keep-extracts',
- action='store_true',
- help='Keep extracted man files after processing'
+ "--keep-extracts",
+ action="store_true",
+ help="Keep extracted man files after processing",
)
parser.add_argument(
- '--parallel-downloads',
+ "--parallel-downloads",
type=int,
default=5,
- help='Number of parallel downloads (default: 5)'
+ help="Number of parallel downloads (default: 5)",
)
parser.add_argument(
- '--parallel-conversions',
+ "--parallel-conversions",
type=int,
default=10,
- help='Number of parallel HTML conversions (default: 10)'
+ help="Number of parallel HTML conversions (default: 10)",
)
parser.add_argument(
- '--mirror',
- default='http://dl.rockylinux.org/',
- help='Rocky Linux mirror URL (default: http://dl.rockylinux.org/)'
+ "--mirror",
+ default="http://dl.rockylinux.org/",
+ help="Rocky Linux mirror URL (default: http://dl.rockylinux.org/)",
)
parser.add_argument(
- '--template-dir',
+ "--vault",
+ action="store_true",
+ help="Use vault directory instead of pub (vault/rocky instead of pub/rocky)",
+ )
+
+ parser.add_argument(
+ "--existing-versions",
+ nargs="*",
+ metavar="VERSION",
+ help="List of existing versions to include in root index (e.g., 8.10 9.7)",
+ )
+
+ parser.add_argument(
+ "--template-dir",
type=Path,
- default=Path(__file__).parent.parent.parent / 'templates',
- help='Template directory (default: ./templates)'
+ default=Path(__file__).parent.parent.parent / "templates",
+ help="Template directory (default: ./templates)",
)
parser.add_argument(
- '-v', '--verbose',
- action='store_true',
- help='Enable verbose logging'
+ "-v", "--verbose", action="store_true", help="Enable verbose logging"
)
parser.add_argument(
- '--skip-sections',
- nargs='*',
+ "--skip-sections",
+ nargs="*",
default=None,
- help='Man sections to skip (default: 3 3p 3pm). Use empty list to skip none.'
+ help="Man sections to skip (default: 3 3p 3pm). Use empty list to skip none.",
)
parser.add_argument(
- '--skip-packages',
- nargs='*',
+ "--skip-packages",
+ nargs="*",
default=None,
- help='Package names to skip (default: lapack dpdk-devel gl-manpages). Use empty list to skip none.'
+ help="Package names to skip (default: lapack dpdk-devel gl-manpages). Use empty list to skip none.",
)
parser.add_argument(
- '--skip-languages',
- action='store_true',
+ "--skip-languages",
+ action="store_true",
default=None,
- help='Skip non-English man pages (default: enabled)'
+ help="Skip non-English man pages (default: enabled)",
)
parser.add_argument(
- '--keep-languages',
- action='store_true',
- help='Keep all languages (disables --skip-languages)'
+ "--keep-languages",
+ action="store_true",
+ help="Keep all languages (disables --skip-languages)",
)
parser.add_argument(
- '--allow-all-sections',
- action='store_true',
- help='Include all man sections (overrides --skip-sections)'
+ "--allow-all-sections",
+ action="store_true",
+ help="Include all man sections (overrides --skip-sections)",
)
args = parser.parse_args()
@@ -309,9 +318,13 @@ def main():
elif args.skip_languages is not None:
skip_languages = args.skip_languages
+ # Determine content directory
+ content_dir = "vault/rocky" if args.vault else "pub/rocky"
+
# Create configuration
config = Config(
base_url=args.mirror,
+ content_dir=content_dir,
versions=args.versions,
repo_types=args.repo_types,
download_dir=args.download_dir,
@@ -324,11 +337,31 @@ def main():
skip_sections=args.skip_sections,
skip_packages=args.skip_packages,
skip_languages=skip_languages,
- allow_all_sections=args.allow_all_sections
+ allow_all_sections=args.allow_all_sections,
)
+ # Get existing versions from scan and argument
+ scanned_versions = [
+ d.name
+ for d in config.output_dir.iterdir()
+ if d.is_dir() and re.match(r"\d+\.\d+", d.name)
+ ]
+ arg_versions = args.existing_versions or []
+
+ # Sort versions numerically by (major, minor)
+ def version_key(v):
+ try:
+ major, minor = v.split(".")
+ return (int(major), int(minor))
+ except (ValueError, AttributeError):
+ return (0, 0)
+
+ existing_versions = sorted(set(scanned_versions + arg_versions), key=version_key)
+ all_versions = sorted(set(existing_versions + config.versions), key=version_key)
+
logger.info("Rocky Man - Rocky Linux Man Page Generator")
- logger.info(f"Versions: {', '.join(config.versions)}")
+ logger.info(f"Versions to process: {', '.join(config.versions)}")
+ logger.info(f"All known versions: {', '.join(all_versions)}")
logger.info(f"Repositories: {', '.join(config.repo_types)}")
logger.info(f"Output directory: {config.output_dir}")
@@ -362,7 +395,7 @@ def main():
# Generate root index
logger.info("Generating root index page...")
web_gen = WebGenerator(args.template_dir, config.output_dir)
- web_gen.generate_root_index(processed_versions)
+ web_gen.generate_root_index(all_versions)
logger.info("=" * 60)
logger.info("Processing complete!")
@@ -373,5 +406,5 @@ def main():
return 0
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main())
diff --git a/src/rocky_man/processor/converter.py b/src/rocky_man/processor/converter.py
index 5f201e0..6dee0b2 100644
--- a/src/rocky_man/processor/converter.py
+++ b/src/rocky_man/processor/converter.py
@@ -40,11 +40,7 @@ class ManPageConverter:
try:
# Run mandoc with no arguments - it will show usage and exit
# We just want to verify the command exists, not that it succeeds
- subprocess.run(
- ['mandoc'],
- capture_output=True,
- timeout=5
- )
+ subprocess.run(["mandoc"], capture_output=True, timeout=5)
return True
except FileNotFoundError:
# mandoc command not found
@@ -73,6 +69,31 @@ class ManPageConverter:
# Clean up HTML
html = self._clean_html(html)
+ # Check if mandoc output indicates this is a symlink/redirect
+ # Pattern: /usr/share/man/man8/target.8.gz
+ # or: See the file /usr/share/man/man8/target.8.
+ # or: See the file man1/builtin.1.
+ symlink_match = re.search(
+ r'.*?(?:See the file )?((?:/usr/share/man/)?man\d+[a-z]*/([^/]+)\.(\d+[a-z]*)(?:\.gz)?)\..*?
',
+ html,
+ re.DOTALL,
+ )
+ if not symlink_match:
+ # Try simpler pattern without "See the file" or period
+ symlink_match = re.search(
+ r'.*?((?:/usr/share/man/)?man\d+[a-z]*/([^/<]+)\.(\d+[a-z]*)(?:\.gz)?).*?
',
+ html,
+ re.DOTALL,
+ )
+
+ if symlink_match:
+ name = symlink_match.group(2)
+ section = symlink_match.group(3)
+ logger.info(
+ f"{man_file.display_name} detected as symlink to {name}({section})"
+ )
+ html = self._generate_redirect_html({"name": name, "section": section})
+
# Store in ManFile object
man_file.html_content = html
@@ -82,7 +103,7 @@ class ManPageConverter:
# Save HTML file
output_path.parent.mkdir(parents=True, exist_ok=True)
- with open(output_path, 'w', encoding='utf-8') as f:
+ with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
logger.debug(f"Converted {man_file.display_name} -> {output_path}")
@@ -93,9 +114,7 @@ class ManPageConverter:
return False
def convert_many(
- self,
- man_files: List[tuple],
- max_workers: int = 10
+ self, man_files: List[tuple], max_workers: int = 10
) -> List[ManFile]:
"""Convert multiple man pages in parallel.
@@ -138,21 +157,21 @@ class ManPageConverter:
"""
try:
result = subprocess.run(
- ['mandoc', '-T', 'html', '-O', 'fragment,toc'],
- input=content.encode('utf-8'),
+ ["mandoc", "-T", "html", "-O", "fragment,toc"],
+ input=content.encode("utf-8"),
capture_output=True,
- timeout=30
+ timeout=30,
)
if result.returncode != 0:
- stderr = result.stderr.decode('utf-8', errors='replace')
+ stderr = result.stderr.decode("utf-8", errors="replace")
logger.warning(f"mandoc returned error: {stderr}")
# Sometimes mandoc returns non-zero but still produces output
if result.stdout:
- return result.stdout.decode('utf-8', errors='replace')
+ return result.stdout.decode("utf-8", errors="replace")
return None
- return result.stdout.decode('utf-8', errors='replace')
+ return result.stdout.decode("utf-8", errors="replace")
except subprocess.TimeoutExpired:
logger.error("mandoc conversion timed out")
@@ -172,14 +191,10 @@ class ManPageConverter:
"""
# Remove empty parentheses in header cells
html = re.sub(
- r'\(\) | ',
- ' | ',
- html
+ r'\(\) | ', ' | ', html
)
html = re.sub(
- r'\(\) | ',
- ' | ',
- html
+ r'\(\) | ', ' | ', html
)
# Strip leading/trailing whitespace
@@ -187,7 +202,34 @@ class ManPageConverter:
return html
- def link_cross_references(self, man_files: List[ManFile]) -> None:
+ def _generate_redirect_html(self, target_info: dict) -> str:
+ """Generate HTML for a symlink/redirect page.
+
+ Args:
+ target_info: Dict with 'name' and 'section' of target man page
+
+ Returns:
+ HTML fragment for redirect page
+ """
+ name = target_info["name"]
+ section = target_info["section"]
+
+ # Generate the relative path to the target man page
+ # Symlinks are in the same package, just different file names
+ target_filename = f"{name}.{section}.html"
+
+ # Generate simple redirect HTML with a working hyperlink
+ html = f''''''
+ return html
+
+ def link_cross_references(self, man_files: List[ManFile], version: str) -> None:
"""Add hyperlinks to cross-references in SEE ALSO sections.
Goes through all converted HTML files and converts man page references
@@ -206,31 +248,31 @@ class ManPageConverter:
logger.info(f"Linking cross-references across {len(man_files)} man pages...")
- # Process each man page HTML file
+ # Process each man page HTML content
for man_file in man_files:
- if not man_file.html_path or not man_file.html_path.exists():
+ if not man_file.html_content:
continue
try:
- # Read the HTML
- with open(man_file.html_path, 'r', encoding='utf-8') as f:
- html = f.read()
+ html = man_file.html_content
# Find and replace man page references
# Mandoc outputs references as: name(section)
# Pattern matches both name(section) and plain name(section)
- pattern = r'([\w\-_.]+)\((\d+[a-z]*)\)|\b([\w\-_.]+)\((\d+[a-z]*)\)'
+ pattern = (
+ r"([\w\-_.]+)\((\d+[a-z]*)\)|\b([\w\-_.]+)\((\d+[a-z]*)\)"
+ )
def replace_reference(match):
full_match = match.group(0)
# Check if this match is already inside an tag
# Look back up to 500 chars for context
- before_text = html[max(0, match.start()-500):match.start()]
+ before_text = html[max(0, match.start() - 500) : match.start()]
# Find the last before this match
- last_open = before_text.rfind('')
+ last_open = before_text.rfind("")
# If the last is after the last , we're inside a link
if last_open > last_close:
@@ -249,23 +291,24 @@ class ManPageConverter:
# Calculate relative path from current file to target
target_path = lookup[key]
# File structure: output_dir/version/package_name/manN/file.html
- # Need to go up 3 levels to reach version root
- # Current: package_name/manN/file.html
- # Target: other_package/manM/file.html
- rel_path = f"../../../{target_path}"
+ # Need to go up 3 levels to reach output root, then down to version/target
+ # Current: version/package_name/manN/file.html
+ # Target: version/other_package/manM/file.html
+ rel_path = f"../../../{version}/{target_path}"
return f'{full_match}'
return full_match
updated_html = re.sub(pattern, replace_reference, html)
- # Only write if something changed
+ # Update the content if something changed
if updated_html != html:
- with open(man_file.html_path, 'w', encoding='utf-8') as f:
- f.write(updated_html)
+ man_file.html_content = updated_html
except Exception as e:
- logger.warning(f"Error linking references in {man_file.display_name}: {e}")
+ logger.warning(
+ f"Error linking references in {man_file.display_name}: {e}"
+ )
logger.info("Cross-reference linking complete")
diff --git a/src/rocky_man/repo/manager.py b/src/rocky_man/repo/manager.py
index b1aa7b5..5a8aebb 100644
--- a/src/rocky_man/repo/manager.py
+++ b/src/rocky_man/repo/manager.py
@@ -25,7 +25,7 @@ class RepoManager:
def __init__(
self,
- repo_url: str,
+ config,
version: str,
repo_type: str,
arch: str,
@@ -35,14 +35,14 @@ class RepoManager:
"""Initialize repository manager.
Args:
- repo_url: Full repository URL
+ config: Configuration object
version: Rocky Linux version (e.g., '9.5')
repo_type: Repository type ('BaseOS' or 'AppStream')
arch: Architecture (e.g., 'x86_64')
cache_dir: Directory for caching metadata
download_dir: Directory for downloading packages
"""
- self.repo_url = repo_url
+ self.config = config
self.version = version
self.repo_type = repo_type
self.arch = arch
@@ -58,7 +58,7 @@ class RepoManager:
self.base.conf.errorlevel = 0
self.base.conf.cachedir = str(self.cache_dir / "dnf")
- self._configure_repo()
+ self.repo_url = None
self.packages_with_manpages: Optional[Set[str]] = None
def _configure_repo(self):
@@ -88,8 +88,32 @@ class RepoManager:
if self.packages_with_manpages is not None:
return self.packages_with_manpages
- parser = ContentsParser(self.repo_url, self.cache_dir)
- self.packages_with_manpages = parser.get_packages_with_manpages()
+ # Try pub first, then vault if it fails
+ content_dirs = ["pub/rocky", "vault/rocky"]
+ for content_dir in content_dirs:
+ original_content_dir = self.config.content_dir
+ self.config.content_dir = content_dir
+ try:
+ repo_url = self.config.get_repo_url(
+ self.version, self.repo_type, self.arch
+ )
+ parser = ContentsParser(repo_url, self.cache_dir)
+ packages = parser.get_packages_with_manpages()
+ if packages: # Only use if it has man pages
+ self.packages_with_manpages = packages
+ self.repo_url = repo_url # Set for later use
+ logger.info(f"Using repository: {repo_url}")
+ break
+ else:
+ logger.warning(f"No man pages found in {content_dir}, trying next")
+ except Exception as e:
+ logger.warning(f"Failed to load metadata from {content_dir}: {e}")
+ finally:
+ self.config.content_dir = original_content_dir
+ else:
+ raise RuntimeError(
+ f"Failed to load repository metadata for {self.version} {self.repo_type} from both pub and vault"
+ )
return self.packages_with_manpages
@@ -102,7 +126,9 @@ class RepoManager:
Returns:
List of Package objects
"""
- logger.info(f"Querying packages from {self.repo_type} ({self.version}/{self.arch})")
+ logger.info(
+ f"Querying packages from {self.repo_type} ({self.version}/{self.arch})"
+ )
# Get packages with man pages if filtering
manpage_packages = None
@@ -110,6 +136,9 @@ class RepoManager:
manpage_packages = self.discover_packages_with_manpages()
logger.info(f"Filtering to {len(manpage_packages)} packages with man pages")
+ # Configure DNF repo now that we have the correct repo_url
+ self._configure_repo()
+
packages = []
# Query all available packages
@@ -176,7 +205,7 @@ class RepoManager:
response.raise_for_status()
# Download with progress (optional: could add progress bar here)
- with open(download_path, 'wb') as f:
+ with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
@@ -192,9 +221,7 @@ class RepoManager:
return False
def download_packages(
- self,
- packages: List[Package],
- max_workers: int = 5
+ self, packages: List[Package], max_workers: int = 5
) -> List[Package]:
"""Download multiple packages in parallel.
@@ -210,8 +237,7 @@ class RepoManager:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all download tasks
future_to_pkg = {
- executor.submit(self.download_package, pkg): pkg
- for pkg in packages
+ executor.submit(self.download_package, pkg): pkg for pkg in packages
}
# Process completed downloads
@@ -223,7 +249,9 @@ class RepoManager:
except Exception as e:
logger.error(f"Error processing {pkg.name}: {e}")
- logger.info(f"Successfully downloaded {len(downloaded)}/{len(packages)} packages")
+ logger.info(
+ f"Successfully downloaded {len(downloaded)}/{len(packages)} packages"
+ )
return downloaded
def cleanup_package(self, package: Package):
diff --git a/src/rocky_man/web/generator.py b/src/rocky_man/web/generator.py
index fba528f..9db50e3 100644
--- a/src/rocky_man/web/generator.py
+++ b/src/rocky_man/web/generator.py
@@ -36,7 +36,7 @@ class WebGenerator:
# Setup Jinja2 environment
self.env = Environment(
loader=FileSystemLoader(str(self.template_dir)),
- autoescape=select_autoescape(['html', 'xml'])
+ autoescape=select_autoescape(["html", "xml"]),
)
def generate_manpage_html(self, man_file: ManFile, version: str) -> bool:
@@ -54,7 +54,7 @@ class WebGenerator:
return False
try:
- template = self.env.get_template('manpage.html')
+ template = self.env.get_template("manpage.html")
html = template.render(
title=f"{man_file.display_name} - {man_file.package_name} - Rocky Linux {version}",
@@ -62,8 +62,8 @@ class WebGenerator:
package_name=man_file.package_name,
version=version,
section=man_file.section,
- language=man_file.language or 'en',
- content=man_file.html_content
+ language=man_file.language or "en",
+ content=man_file.html_content,
)
# Ensure output path is set
@@ -72,7 +72,7 @@ class WebGenerator:
man_file.html_path.parent.mkdir(parents=True, exist_ok=True)
- with open(man_file.html_path, 'w', encoding='utf-8') as f:
+ with open(man_file.html_path, "w", encoding="utf-8") as f:
f.write(html)
return True
@@ -92,19 +92,19 @@ class WebGenerator:
True if successful
"""
try:
- template = self.env.get_template('index.html')
+ 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())
+ packages=sorted(search_data.keys()),
)
- index_path = self.output_dir / version / 'index.html'
+ 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:
+ with open(index_path, "w", encoding="utf-8") as f:
f.write(html)
logger.info(f"Generated index for version {version}")
@@ -113,8 +113,10 @@ class WebGenerator:
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:
+
+ def generate_packages_index(
+ self, version: str, search_data: Dict[str, Any]
+ ) -> bool:
"""Generate full packages index page.
Args:
@@ -127,37 +129,36 @@ class WebGenerator:
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'
-
+ 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)
- })
+
+ 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'])
+ packages_by_letter[letter].sort(key=lambda x: x["name"])
- template = self.env.get_template('packages.html')
+ 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
+ packages_by_letter=packages_by_letter,
)
- output_path = self.output_dir / version / 'packages.html'
+ 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:
+ with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
logger.info(f"Generated packages index for version {version}")
@@ -168,9 +169,7 @@ class WebGenerator:
return False
def generate_search_index(
- self,
- man_files: List[ManFile],
- version: str
+ self, man_files: List[ManFile], version: str
) -> Dict[str, Any]:
"""Generate search index from man files.
@@ -191,12 +190,12 @@ class WebGenerator:
# 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}"
+ "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)
@@ -222,18 +221,18 @@ class WebGenerator:
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'
+ 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:
+ 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:
+ 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)")
@@ -269,24 +268,42 @@ class WebGenerator:
True if successful
"""
try:
- template = self.env.get_template('root.html')
+ template = self.env.get_template("root.html")
- # Sort versions numerically (e.g., 8.10, 9.6, 10.0)
- def version_key(v):
+ # Group versions by major version
+ major_to_minors = {}
+ for v in versions:
try:
- parts = v.split('.')
- return tuple(int(p) for p in parts)
- except (ValueError, AttributeError):
- return (0, 0)
+ major, minor = v.split(".")
+ major_to_minors.setdefault(major, []).append(minor)
+ except ValueError:
+ continue # Skip invalid versions
+
+ # Sort majors ascending, minors descending within each major
+ sorted_majors = sorted(major_to_minors, key=int)
+ max_minors = max(len(major_to_minors[major]) for major in sorted_majors)
+ num_columns = len(sorted_majors)
+
+ # Create rows for grid layout (each row has one version from each major)
+ # This creates the data structure for proper column grouping
+ version_rows = []
+ for minor_idx in range(max_minors):
+ row = []
+ for major in sorted_majors:
+ minors_list = sorted(major_to_minors[major], key=int, reverse=True)
+ if minor_idx < len(minors_list):
+ row.append((major, minors_list[minor_idx]))
+ else:
+ row.append(None) # Placeholder for empty cells
+ version_rows.append(row)
html = template.render(
- title="Rocky Linux Man Pages",
- versions=sorted(versions, key=version_key)
+ title="Rocky Linux Man Pages", version_rows=version_rows, num_columns=num_columns
)
- index_path = self.output_dir / 'index.html'
+ index_path = self.output_dir / "index.html"
- with open(index_path, 'w', encoding='utf-8') as f:
+ with open(index_path, "w", encoding="utf-8") as f:
f.write(html)
logger.info("Generated root index page")
diff --git a/templates/base.html b/templates/base.html
index b5896e4..6149414 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -255,10 +255,7 @@
Search by Fuse.js
- Rocky Linuxβ’ is a trademark of the Rocky Enterprise Software Foundation.
-
-
- This tool is open source (MIT License). See THIRD-PARTY-LICENSES.md for attributions.
+ Rocky Linux is a trademark of the Rocky Enterprise Software Foundation.
diff --git a/templates/root.html b/templates/root.html
index 29e277a..16ed80f 100644
--- a/templates/root.html
+++ b/templates/root.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
-{% block header_title %}Rocky Linuxβ’ Man Pages{% endblock %}
-{% block header_subtitle %}Man page documentation for Rocky Linuxβ’ packages{% endblock %}
+{% block header_title %}Rocky Linux Man Pages{% endblock %}
+{% block header_subtitle %}Man page documentation for Rocky Linux packages{% endblock %}
{% block extra_css %}
.logo-container {
@@ -15,9 +15,11 @@
height: auto;
}
+
+
.version-grid {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ grid-template-columns: repeat({{ num_columns }}, 1fr);
gap: 1.5rem;
margin-top: 2rem;
}
@@ -32,7 +34,7 @@
}
.version-grid {
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ grid-template-columns: 1fr;
gap: 1rem;
}
@@ -40,9 +42,21 @@
padding: 1.5rem;
}
+ .version-card.small {
+ padding: 0.75rem;
+ }
+
+ .version-card.small {
+ padding: 0.75rem;
+ }
+
.version-number {
font-size: 2rem;
}
+
+ .version-card.small .version-number {
+ font-size: 1.5rem;
+ }
}
@media (max-width: 480px) {
@@ -55,6 +69,10 @@
gap: 1rem;
}
+ .version-card.small {
+ padding: 0.5rem;
+ }
+
.intro {
font-size: 0.9rem;
}
@@ -71,6 +89,15 @@
display: block;
}
+.version-card.small {
+ padding: 1rem;
+ opacity: 0.7;
+}
+
+.version-card.small .version-number {
+ font-size: 1.8rem;
+}
+
.version-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
@@ -128,19 +155,27 @@
- Man page documentation for packages in the Rocky Linuxβ’ BaseOS and AppStream repositories.
+ Man page documentation for packages in the Rocky Linux BaseOS and AppStream repositories.