diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 1678ced..0000000 --- a/.dockerignore +++ /dev/null @@ -1,37 +0,0 @@ -# Git -.git -.gitignore - -# Python -__pycache__ -*.py[cod] -*$py.class -*.so -.Python -*.egg-info -dist/ -build/ -*.egg - -# Virtual environments -venv/ -env/ -ENV/ -.venv - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -.DS_Store - -# Project specific -old/ -old_scripts/ -tmp/ -html/ -.cache/ - -# UV cache -.uv_cache/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index b40a5c2..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,57 +0,0 @@ -# GitHub Actions workflow for building Rocky Man pages -name: Build Rocky Man Pages - -on: - - # Allow manual trigger - workflow_dispatch: - inputs: - versions: - description: 'Rocky Linux versions to build (space-separated)' - required: false - default: '8.10 9.6 10.0' - - - # # Run on push to main (for testing) - # push: - # branches: - # - main - # paths: - # - 'src/**' - # - 'templates/**' - # - 'pyproject.toml' - # - '.github/workflows/build.yml' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build Docker image - run: | - docker build -t rocky-man:latest . - - - name: Create output directories - run: | - mkdir -p ./html ./tmp - - - name: Build man pages in container - run: | - docker run --rm \ - -v "$(pwd)/html:/data/html" \ - -v "$(pwd)/tmp:/data/tmp" \ - rocky-man:latest \ - --versions ${{ github.event.inputs.versions || '8.10 9.6 10.0' }} \ - --verbose - env: - PYTHONUNBUFFERED: 1 - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: rocky-man-pages - path: html/ - retention-days: 30 diff --git a/.gitignore b/.gitignore index bb47d26..4b993bc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,8 +39,14 @@ html/ tmp/ .cache/ +# Application source cloned by the pipeline at build time +/rocky-man/ + # UV cache .uv_cache/ +# Ruff cache +.ruff_cache/ + # Logs *.log diff --git a/.ruff_cache/0.12.5/7088262797167738659 b/.ruff_cache/0.12.5/7088262797167738659 deleted file mode 100644 index 3c105be..0000000 Binary files a/.ruff_cache/0.12.5/7088262797167738659 and /dev/null differ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5114417..0000000 --- a/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -# Multi-stage Dockerfile for Rocky Man -# This creates an architecture-independent image that can run on x86_64, aarch64, etc. - -FROM rockylinux/rockylinux:9 AS builder - -# Install system dependencies -RUN dnf install -y epel-release \ - && dnf install -y \ - python3 \ - python3-pip \ - python3-dnf \ - mandoc \ - rpm-build \ - dnf-plugins-core \ - && dnf clean all - -# Set working directory -WORKDIR /app - -# Copy project files -COPY pyproject.toml README.md LICENSE ./ -COPY src ./src -COPY templates ./templates - -# Install Python dependencies using pip -RUN python3 -m pip install --no-cache-dir -e . - -# Runtime stage -FROM rockylinux/rockylinux:9 - -# Install runtime dependencies -RUN dnf install -y epel-release \ - && dnf install -y \ - python3 \ - python3-dnf \ - mandoc \ - rpm-build \ - dnf-plugins-core \ - && dnf clean all - -# Copy Python packages and app from builder -COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages -COPY --from=builder /usr/local/lib64/python3.9/site-packages /usr/local/lib64/python3.9/site-packages -COPY --from=builder /app /app - -WORKDIR /app - -# Create directories for data -RUN mkdir -p /data/html /data/tmp - -# Set environment variables -ENV PYTHONUNBUFFERED=1 - -# Volume for output -VOLUME ["/data/html", "/data/tmp"] - -# Default command -ENTRYPOINT ["python3", "-m", "rocky_man.main"] -CMD ["--output-dir", "/data/html", "--download-dir", "/data/tmp/downloads", "--extract-dir", "/data/tmp/extracts"] diff --git a/Jenkinsfile b/Jenkinsfile index a99aafa..dd3a359 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,4 +1,8 @@ // Jenkinsfile for Rocky Man +// +// This repo holds only the deploy pipeline. The application source is pulled +// from the upstream repository at build time (see SOURCE_REPO / SOURCE_REF), +// so the code is not duplicated here. pipeline { agent { kubernetes { @@ -35,6 +39,16 @@ spec: } parameters { + string( + name: 'SOURCE_REPO', + defaultValue: 'https://git.resf.org/documentation/rocky-man.git', + description: 'Upstream application repository to build' + ) + string( + name: 'SOURCE_REF', + defaultValue: 'main', + description: 'Branch or tag of the application to build' + ) string( name: 'VERSIONS', defaultValue: '8.10 9.8 10.2', @@ -73,9 +87,11 @@ spec: } stages { - stage('Checkout') { + stage('Checkout Source') { steps { - checkout scm + dir('rocky-man') { + git url: "${params.SOURCE_REPO}", branch: "${params.SOURCE_REF}" + } } } @@ -87,7 +103,7 @@ until docker info > /dev/null 2>&1; do echo "Waiting for Docker daemon..." sleep 2 done -docker build -t rocky-man:${BUILD_NUMBER} . +docker build -t rocky-man:${BUILD_NUMBER} rocky-man docker tag rocky-man:${BUILD_NUMBER} rocky-man:latest ''' } @@ -156,4 +172,4 @@ docker rmi rocky-man:latest || true } } } -} \ No newline at end of file +} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1b2ab88..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Ctrl IQ, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 728c82c..0000000 --- a/README.md +++ /dev/null @@ -1,204 +0,0 @@ -# πŸš€ Rocky Man πŸš€ - -**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 - -- Uses filelists.xml to pre-filter packages with man pages -- Processes packages from BaseOS and AppStream repositories -- Runs in containers on x86_64, aarch64, and arm64 architectures -- Configurable cleanup of temporary files -- Concurrent downloads and conversions -- Supports Rocky Linux 8, 9, and 10 - -## Quick Start - -### Podman - -```bash -# Build the image -docker build -t rocky-man . - -# Generate for specific versions -podman run --rm -v $(pwd)/html:/data/html:Z rocky-man \ - --versions 8.10 9.6 10.0 - -# Keep downloaded RPMs for multiple builds -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 -``` - -### View the HTML Locally - -Start a local web server to browse the generated documentation: - -```bash -python3 -m http.server -d ./html -``` - -Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser. - -To use a different port: - -```bash -python3 -m http.server 8080 -d ./html -``` - -### Directory Structure in Container - -The container uses the following paths: - -- `/data/html` - Generated HTML output -- `/data/tmp/downloads` - Downloaded RPM files -- `/data/tmp/extracts` - Extracted man page files - -These paths are used by default and can be overridden with command-line arguments if needed. - -### Local Development - -**Important**: Rocky Man requires Rocky Linux because it uses the system's native `python3-dnf` module to interact with DNF repositories. This module cannot be installed via pip and must come from the Rocky Linux system packages. - -#### Option 1: Run in a Rocky Linux Container (Recommended) - -```bash -# Start a Rocky Linux container with your project mounted -podman run --rm -it -v $(pwd):/workspace:Z rockylinux/rockylinux:9 /bin/bash - -# Inside the container, navigate to the project -cd /workspace - -# Install epel-release for mandoc -dnf install -y epel-release - -# Install system dependencies -dnf install -y python3 python3-pip python3-dnf mandoc rpm-build dnf-plugins-core - -# Install Python dependencies -pip3 install -e . - -# Run the tool -python3 -m rocky_man.main --versions 9.6 --output-dir ./html/ -``` - -#### Option 2: On a Native Rocky Linux System - -```bash -# Install epel-release for mandoc -dnf install -y epel-release - -# Install system dependencies -dnf install -y python3 python3-pip python3-dnf mandoc rpm-build dnf-plugins-core - -# Install Python dependencies -pip3 install -e . - -# Run the tool -python3 -m rocky_man.main --versions 9.6 --output-dir ./html/ -``` - -## Architecture - -Rocky Man is organized into components: - -```text -rocky-man/ -β”œβ”€β”€ src/rocky_man/ -β”‚ β”œβ”€β”€ models/ # Data models (Package, ManFile) -β”‚ β”œβ”€β”€ 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** - Parses repository metadata (`repodata/repomd.xml` and `filelists.xml.gz`) to identify packages containing files in `/usr/share/man/` directories -2. **Package Download** - Downloads identified RPM packages using DNF, with configurable parallel downloads (default: 5) -3. **Man Page Extraction** - Extracts man page files from RPMs using `rpm2cpio`, filtering by section and language based on configuration -4. **HTML Conversion** - Converts troff-formatted man pages to HTML using mandoc, with parallel processing (default: 10 workers) -5. **Cross-Reference Linking** - Parses converted HTML to add hyperlinks between man page references (e.g., `bash(1)` becomes clickable) -6. **Index Generation** - Creates search indexes (JSON/gzipped) and navigation pages using Jinja2 templates -7. **Cleanup** - Removes temporary files (RPMs and extracted content) unless `--keep-rpms` or `--keep-extracts` is specified - -## Command Line Options - -```bash -usage: main.py [-h] [--versions VERSIONS [VERSIONS ...]] - [--repo-types REPO_TYPES [REPO_TYPES ...]] - [--output-dir OUTPUT_DIR] [--download-dir DOWNLOAD_DIR] - [--extract-dir EXTRACT_DIR] [--keep-rpms] [--keep-extracts] - [--parallel-downloads PARALLEL_DOWNLOADS] - [--parallel-conversions PARALLEL_CONVERSIONS] [--mirror MIRROR] - [--vault] [--existing-versions [VERSION ...]] - [--template-dir TEMPLATE_DIR] [-v] - [--skip-sections [SKIP_SECTIONS ...]] - [--skip-packages [SKIP_PACKAGES ...]] [--skip-languages] - [--keep-languages] [--allow-all-sections] - -Generate HTML documentation for Rocky Linux man pages - -optional arguments: - -h, --help show this help message and exit - --versions VERSIONS [VERSIONS ...] - Rocky Linux versions to process (default: 8.10 9.6 10.0) - --repo-types REPO_TYPES [REPO_TYPES ...] - Repository types to process (default: BaseOS AppStream) - --output-dir OUTPUT_DIR - Output directory for HTML files (default: /data/html) - --download-dir DOWNLOAD_DIR - Directory for downloading packages (default: /data/tmp/downloads) - --extract-dir EXTRACT_DIR - Directory for extracting man pages (default: /data/tmp/extracts) - --keep-rpms Keep downloaded RPM files after processing - --keep-extracts Keep extracted man files after processing - --parallel-downloads PARALLEL_DOWNLOADS - Number of parallel downloads (default: 5) - --parallel-conversions PARALLEL_CONVERSIONS - Number of parallel HTML conversions (default: 10) - --mirror MIRROR Rocky Linux mirror URL (default: http://dl.rockylinux.org/) - --vault Use vault directory instead of pub (vault/rocky instead of pub/rocky) - --existing-versions [VERSION ...] - List of existing versions to include in root index (e.g., 8.10 9.7) - --template-dir TEMPLATE_DIR - Template directory (default: ./templates) - -v, --verbose Enable verbose logging - --skip-sections [SKIP_SECTIONS ...] - Man sections to skip (default: 3 3p 3pm). Use empty list to skip none. - --skip-packages [SKIP_PACKAGES ...] - Package names to skip (default: lapack dpdk-devel gl-manpages). Use empty list to skip none. - --skip-languages Skip non-English man pages (default: enabled) - --keep-languages Keep all languages (disables --skip-languages) - --allow-all-sections Include all man sections (overrides --skip-sections) -``` - -## Attribution - -The man pages displayed in this documentation are sourced from Rocky Linux distribution packages. All man page content is copyrighted by their respective authors and distributed under the licenses specified within each man page. - -This tool generates HTML documentation from man pages contained in Rocky Linux packages but does not modify the content of the man pages themselves. - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -### Third-Party Software - -This project uses several open source components. - -Key dependencies include: - -- **mandoc** - Man page converter (ISC License) -- **python3-dnf** - DNF package manager Python bindings (GPL-2.0-or-later) -- **Fuse.js** - Client-side search (Apache 2.0) -- **Python packages**: requests, rpmfile, Jinja2, lxml, zstandard -- **Fonts**: Red Hat Display, Red Hat Text, JetBrains Mono (SIL OFL) - -### Trademark Notice - -Rocky Linux is a trademark of the Rocky Enterprise Software Foundation (RESF). This project is not officially affiliated with or endorsed by RESF. All trademarks are the property of their respective owners. This project complies with RESF's trademark usage guidelines. diff --git a/THIRD-PARTY-LICENSES.md b/THIRD-PARTY-LICENSES.md deleted file mode 100644 index 932332a..0000000 --- a/THIRD-PARTY-LICENSES.md +++ /dev/null @@ -1,59 +0,0 @@ -# Third-Party Licenses and Attributions - -This project uses the following third-party software and resources: - -## Software Components - -### mandoc -- **Description**: Man page converter (troff to HTML) -- **Website**: https://mandoc.bsd.lv/ -- **License**: ISC License -- **Usage**: Core conversion engine for transforming man pages to HTML - -### Fuse.js -- **Description**: Lightweight fuzzy-search library -- **Website**: https://fusejs.io/ -- **License**: Apache License 2.0 -- **Usage**: Client-side search functionality (loaded via CDN) - -### Python Dependencies - -#### requests -- **License**: Apache License 2.0 -- **Website**: https://requests.readthedocs.io/ - -#### rpmfile -- **License**: MIT License -- **Website**: https://github.com/srossross/rpmfile - -#### Jinja2 -- **License**: BSD License -- **Website**: https://palletsprojects.com/p/jinja/ - -#### lxml -- **License**: BSD License -- **Website**: https://lxml.de/ - -#### zstandard -- **License**: BSD License -- **Website**: https://github.com/indygreg/python-zstandard - -## Trademarks - -### Rocky Linux -- **Rocky Linuxβ„’** is a trademark of the Rocky Enterprise Software Foundation (RESF) -- This project is not officially affiliated with or endorsed by RESF -- Rocky Linux trademark usage complies with RESF's trademark guidelines -- Brand assets used with permission under RESF trademark policy - -## Content - -### Man Pages -- Man pages are extracted from Rocky Linux package repositories -- Man page content is copyright of their respective authors and maintainers -- Man pages are distributed under various open source licenses as part of their respective packages -- This tool does not modify man page content, only converts format for web display - -## Disclaimer - -This project aggregates and displays documentation from Rocky Linux packages. All original content remains under the copyright and license of the respective package authors. This tool is provided as-is for community benefit and convenience. diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5ddf4ab..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,27 +0,0 @@ -[project] -name = "rocky-man" -version = "0.1.0" -description = "Rocky Linux Man Pages - A comprehensive man page hosting solution for Rocky Linux 8, 9, and 10" -readme = "README.md" -license = {text = "MIT"} -authors = [ - { name = "Stephen Simpson", email = "ssimpson89@users.noreply.github.com" } -] -requires-python = ">=3.9" -dependencies = [ - "requests>=2.32.0", - "rpmfile>=2.1.0", - "jinja2>=3.1.0", - "lxml>=6.0.0", - "zstandard>=0.25.0", -] - -[project.scripts] -rocky-man = "rocky_man.main:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [] diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/rocky_man/__init__.py b/src/rocky_man/__init__.py deleted file mode 100644 index 2293f69..0000000 --- a/src/rocky_man/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .utils.config import Config - -__version__ = "0.1.0" - -__all__ = ["Config"] diff --git a/src/rocky_man/main.py b/src/rocky_man/main.py deleted file mode 100644 index 920fec1..0000000 --- a/src/rocky_man/main.py +++ /dev/null @@ -1,383 +0,0 @@ -"""Main entry point for Rocky Man.""" - -import argparse -import logging -import re -import sys -from pathlib import Path - -from .utils.config import Config -from .repo import RepoManager -from .processor import ManPageExtractor, ManPageConverter -from .web import WebGenerator - - -def setup_logging(verbose: bool = False): - """Configure logging.""" - 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", - ) - - -def process_version(config: Config, version: str, template_dir: Path) -> bool: - """Process a single Rocky Linux version. - - Args: - config: Configuration object - version: Rocky Linux version to process - template_dir: Path to templates directory - - Returns: - True if successful - """ - logger = logging.getLogger(__name__) - logger.info(f"Processing Rocky Linux {version}") - - # Setup directories for this version - version_download_dir = config.get_version_download_dir(version) - version_extract_dir = config.get_version_extract_dir(version) - version_output_dir = config.get_version_output_dir(version) - - all_man_files = [] - - for repo_type in config.repo_types: - logger.info(f"Processing {repo_type} repository") - - arch = config.architectures[0] - cache_dir = config.download_dir / f".cache/{version}/{repo_type}" - - try: - repo_manager = RepoManager( - config=config, - version=version, - repo_type=repo_type, - arch=arch, - cache_dir=cache_dir, - download_dir=version_download_dir, - ) - - packages = repo_manager.list_packages(with_manpages_only=True) - - if not packages: - logger.warning(f"No packages found in {repo_type}") - continue - - logger.info(f"Found {len(packages)} packages with man pages in {repo_type}") - - if config.skip_packages: - original_count = len(packages) - 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"Processing {len(packages)} packages") - - logger.info("Downloading packages...") - downloaded = repo_manager.download_packages( - packages, max_workers=config.parallel_downloads - ) - - logger.info("Extracting man pages...") - extractor = ManPageExtractor( - version_extract_dir, - skip_sections=config.skip_sections, - skip_languages=config.skip_languages, - ) - man_files = extractor.extract_from_packages( - downloaded, max_workers=config.parallel_downloads - ) - - logger.info(f"Extracted {len(man_files)} man pages") - - logger.info("Reading man page content...") - man_files_with_content = [] - for man_file in man_files: - content = extractor.read_manpage_content(man_file) - if content: - man_files_with_content.append((man_file, content)) - - 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 - ) - - all_man_files.extend(converted) - - if not config.keep_rpms: - logger.info("Cleaning up downloaded packages...") - for package in downloaded: - repo_manager.cleanup_package(package) - - if not config.keep_extracts: - logger.info("Cleaning up extracted files...") - for package in downloaded: - extractor.cleanup_extracts(package) - - except Exception as e: - logger.error(f"Error processing {repo_type}: {e}", exc_info=True) - continue - - if not all_man_files: - logger.error(f"No man pages were successfully processed for version {version}") - return False - - logger.info("Generating web pages...") - web_gen = WebGenerator(template_dir, config.output_dir) - - search_index = web_gen.generate_search_index(all_man_files, version) - web_gen.save_search_index(search_index, version) - web_gen.generate_index(version, search_index) - web_gen.generate_packages_index(version, search_index) - - 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) - - logger.info("Linking cross-references...") - converter.link_cross_references(all_man_files, version) - - 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}" - ) - return True - - -def main(): - """Main entry point.""" - parser = argparse.ArgumentParser( - 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)", - ) - - parser.add_argument( - "--repo-types", - nargs="+", - default=["BaseOS", "AppStream"], - help="Repository types to process (default: BaseOS AppStream)", - ) - - parser.add_argument( - "--output-dir", - type=Path, - default=Path("/data/html"), - help="Output directory for HTML files (default: /data/html)", - ) - - parser.add_argument( - "--download-dir", - type=Path, - default=Path("/data/tmp/downloads"), - help="Directory for downloading packages (default: /data/tmp/downloads)", - ) - - parser.add_argument( - "--extract-dir", - type=Path, - default=Path("/data/tmp/extracts"), - help="Directory for extracting man pages (default: /data/tmp/extracts)", - ) - - parser.add_argument( - "--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", - ) - - parser.add_argument( - "--parallel-downloads", - type=int, - default=5, - help="Number of parallel downloads (default: 5)", - ) - - parser.add_argument( - "--parallel-conversions", - type=int, - 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/)", - ) - - parser.add_argument( - "--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)", - ) - - parser.add_argument( - "-v", "--verbose", action="store_true", help="Enable verbose logging" - ) - - parser.add_argument( - "--skip-sections", - nargs="*", - default=None, - help="Man sections to skip (default: 3 3p 3pm). Use empty list to skip none.", - ) - - parser.add_argument( - "--skip-packages", - nargs="*", - default=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", - default=None, - help="Skip non-English man pages (default: enabled)", - ) - - parser.add_argument( - "--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)", - ) - - args = parser.parse_args() - - setup_logging(args.verbose) - logger = logging.getLogger(__name__) - - skip_languages = True - if args.keep_languages: - skip_languages = False - elif args.skip_languages is not None: - skip_languages = args.skip_languages - - content_dir = "vault/rocky" if args.vault else "pub/rocky" - - config = Config( - base_url=args.mirror, - content_dir=content_dir, - versions=args.versions, - repo_types=args.repo_types, - download_dir=args.download_dir, - extract_dir=args.extract_dir, - output_dir=args.output_dir, - keep_rpms=args.keep_rpms, - keep_extracts=args.keep_extracts, - parallel_downloads=args.parallel_downloads, - parallel_conversions=args.parallel_conversions, - skip_sections=args.skip_sections, - skip_packages=args.skip_packages, - skip_languages=skip_languages, - allow_all_sections=args.allow_all_sections, - ) - - 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 [] - - 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 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}") - - if config.skip_sections: - logger.info(f"Skipping man sections: {', '.join(config.skip_sections)}") - else: - logger.info("Including all man sections") - - if config.skip_packages: - logger.info(f"Skipping packages: {', '.join(config.skip_packages)}") - - if config.skip_languages: - logger.info("Skipping non-English languages") - else: - logger.info("Including all languages") - - processed_versions = [] - for version in config.versions: - try: - if process_version(config, version, args.template_dir): - processed_versions.append(version) - except Exception as e: - logger.error(f"Failed to process version {version}: {e}", exc_info=True) - - if not processed_versions: - logger.error("No versions were successfully processed") - return 1 - - logger.info("Generating root index page...") - web_gen = WebGenerator(args.template_dir, config.output_dir) - web_gen.generate_root_index(all_versions) - - logger.info("Generating 404 page...") - web_gen.generate_404_page() - - logger.info("=" * 60) - logger.info("Processing complete!") - logger.info(f"Generated documentation for: {', '.join(processed_versions)}") - logger.info(f"Output directory: {config.output_dir.absolute()}") - logger.info("=" * 60) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/rocky_man/models/__init__.py b/src/rocky_man/models/__init__.py deleted file mode 100644 index 2d14aeb..0000000 --- a/src/rocky_man/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Data models for Rocky Man.""" - -from .package import Package -from .manfile import ManFile - -__all__ = ["Package", "ManFile"] diff --git a/src/rocky_man/models/manfile.py b/src/rocky_man/models/manfile.py deleted file mode 100644 index 1e3d4d9..0000000 --- a/src/rocky_man/models/manfile.py +++ /dev/null @@ -1,105 +0,0 @@ -"""ManFile model representing a man page file.""" - -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional -import re - - -@dataclass -class ManFile: - """Represents a man page file extracted from an RPM package. - - Attributes: - file_path: Path to the extracted man page file - package_name: Name of the package this man page belongs to - section: Man page section (1-9) - name: Man page name without extension - language: Language code (e.g., 'en', 'es', None for default) - content: Raw man page content (gzipped or plain text) - html_content: Converted HTML content - html_path: Path where HTML file is saved - """ - - file_path: Path - package_name: str - section: Optional[str] = None - name: Optional[str] = None - language: Optional[str] = None - content: Optional[bytes] = None - html_content: Optional[str] = None - html_path: Optional[Path] = None - - def __post_init__(self): - """Parse file information from the path.""" - self._parse_path() - - def _parse_path(self): - """Extract section, name, and language from the file path.""" - parts = self.file_path.parts - filename = self.file_path.name - - if filename.endswith('.gz'): - filename = filename[:-3] - - for part in reversed(parts): - if part.startswith('man') and len(part) > 3: - if part[3].isdigit(): - self.section = part[3:] - break - - name_parts = filename.split('.') - if len(name_parts) >= 2: - potential_section = name_parts[-1] - if potential_section and potential_section[0].isdigit(): - if not self.section: - self.section = potential_section - self.name = '.'.join(name_parts[:-1]) - else: - self.name = name_parts[0] - else: - self.name = name_parts[0] - - for i, part in enumerate(parts): - if part == 'man' and i + 1 < len(parts): - next_part = parts[i + 1] - if not (next_part.startswith('man') and next_part[3:].isdigit()): - if len(next_part) <= 5: - self.language = next_part - break - - @property - def display_name(self) -> str: - """Get display name for the man page (e.g., 'bash(1)').""" - return f"{self.name}({self.section})" if self.section else self.name - - @property - def html_filename(self) -> str: - """Get the HTML filename for this man page.""" - safe_name = self._clean_filename(self.name) - suffix = f".{self.language}" if self.language else "" - return f"{safe_name}.{self.section}{suffix}.html" - - def _clean_filename(self, name: str) -> str: - """Clean filename for filesystem safety.""" - name = name.replace('/', '_') - name = name.replace(':', '_') - name = re.sub(r'\.\.', '__', name) - return name - - @property - def uri_path(self) -> str: - """Get the URI path for this man page (relative to version root).""" - if not self.html_path: - return "" - parts = self.html_path.parts - try: - for i, part in enumerate(parts): - if re.match(r'\d+\.\d+', part): - return '/'.join(parts[i+1:]) - except (ValueError, IndexError): - pass - return str(self.html_path) - - def __str__(self): - return f"{self.package_name}: {self.display_name}" diff --git a/src/rocky_man/models/package.py b/src/rocky_man/models/package.py deleted file mode 100644 index a30481f..0000000 --- a/src/rocky_man/models/package.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Package model representing an RPM package.""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Optional - - -@dataclass -class Package: - """Represents an RPM package from a Rocky Linux repository. - - Attributes: - name: Package name (e.g., 'bash') - version: Package version - release: Package release - arch: Architecture (e.g., 'x86_64', 'noarch') - repo_type: Repository type ('BaseOS' or 'AppStream') - location: Relative path in repo (e.g., 'Packages/b/bash-5.1.8-6.el9.x86_64.rpm') - baseurl: Base URL of the repository - checksum: Package checksum for verification - checksum_type: Type of checksum (e.g., 'sha256') - download_path: Local path where package is downloaded - has_manpages: Whether this package contains man pages - """ - - name: str - version: str - release: str - arch: str - repo_type: str - location: str - baseurl: str - checksum: str - checksum_type: str - has_manpages: bool = False - download_path: Optional[Path] = None - - @property - def filename(self) -> str: - """Get the RPM filename from the location.""" - return self.location.split("/")[-1] - - @property - def download_url(self) -> str: - """Get the full download URL for this package.""" - return f"{self.baseurl.rstrip('/')}/{self.location.lstrip('/')}" - - @property - def nvra(self) -> str: - """Get the Name-Version-Release-Arch identifier.""" - return f"{self.name}-{self.version}-{self.release}.{self.arch}" - - def __lt__(self, other): - """Enable sorting packages by name.""" - return self.name < other.name - - def __str__(self): - return f"{self.nvra} ({self.repo_type})" diff --git a/src/rocky_man/processor/__init__.py b/src/rocky_man/processor/__init__.py deleted file mode 100644 index fd8a2e5..0000000 --- a/src/rocky_man/processor/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .extractor import ManPageExtractor -from .converter import ManPageConverter - -__all__ = ["ManPageExtractor", "ManPageConverter"] diff --git a/src/rocky_man/processor/converter.py b/src/rocky_man/processor/converter.py deleted file mode 100644 index c868055..0000000 --- a/src/rocky_man/processor/converter.py +++ /dev/null @@ -1,283 +0,0 @@ -"""Convert man pages to HTML using mandoc.""" - -import logging -import re -import subprocess -from pathlib import Path -from typing import List, Optional -from concurrent.futures import ThreadPoolExecutor, as_completed - -from ..models import ManFile - -logger = logging.getLogger(__name__) - - -class ManPageConverter: - """Converts man pages to HTML using mandoc. - - Handles: - - Converting troff to HTML using mandoc - - Cleaning up HTML output - - Parallel conversion of multiple man pages - """ - - def __init__(self, output_dir: Path): - """Initialize converter. - - Args: - output_dir: Base directory for HTML output - """ - self.output_dir = Path(output_dir) - self.output_dir.mkdir(parents=True, exist_ok=True) - - # Check if mandoc is available - if not self._check_mandoc(): - raise RuntimeError("mandoc is not installed or not in PATH") - - @staticmethod - def _check_mandoc() -> bool: - """Check if mandoc is available.""" - try: - subprocess.run(["mandoc"], capture_output=True, timeout=5) - return True - except FileNotFoundError: - return False - except Exception: - return True - - def convert(self, man_file: ManFile, content: str) -> bool: - """Convert a single man page to HTML. - - Args: - man_file: ManFile object to convert - content: Raw man page content (troff format) - - Returns: - True if conversion successful - """ - try: - html = self._run_mandoc(content) - if not html: - logger.warning(f"mandoc produced no output for {man_file.display_name}") - return False - - html = self._clean_html(html) - - # Check if output indicates this is a symlink/redirect - 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: - 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}) - - man_file.html_content = html - output_path = self._get_output_path(man_file) - man_file.html_path = output_path - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, "w", encoding="utf-8") as f: - f.write(html) - - logger.debug(f"Converted {man_file.display_name} -> {output_path}") - return True - - except Exception as e: - logger.error(f"Error converting {man_file.display_name}: {e}") - return False - - def convert_many( - self, man_files: List[tuple], max_workers: int = 10 - ) -> List[ManFile]: - """Convert multiple man pages in parallel. - - Args: - man_files: List of (ManFile, content) tuples - max_workers: Maximum number of parallel conversions - - Returns: - List of successfully converted ManFile objects - """ - converted = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_manfile = { - executor.submit(self.convert, man_file, content): man_file - for man_file, content in man_files - } - - for future in as_completed(future_to_manfile): - man_file = future_to_manfile[future] - try: - if future.result(): - converted.append(man_file) - except Exception as e: - logger.error(f"Error converting {man_file.display_name}: {e}") - - logger.info(f"Converted {len(converted)}/{len(man_files)} man pages to HTML") - return converted - - def _run_mandoc(self, content: str) -> Optional[str]: - """Run mandoc to convert man page to HTML. - - Args: - content: Raw man page content - - Returns: - HTML output from mandoc, or None on error - """ - try: - result = subprocess.run( - ["mandoc", "-T", "html", "-O", "fragment,toc"], - input=content.encode("utf-8"), - capture_output=True, - timeout=30, - ) - - if result.returncode != 0: - stderr = result.stderr.decode("utf-8", errors="replace") - logger.warning(f"mandoc returned error: {stderr}") - if result.stdout: - return result.stdout.decode("utf-8", errors="replace") - return None - - return result.stdout.decode("utf-8", errors="replace") - - except subprocess.TimeoutExpired: - logger.error("mandoc conversion timed out") - return None - except Exception as e: - logger.error(f"Error running mandoc: {e}") - return None - - def _clean_html(self, html: str) -> str: - """Clean up mandoc HTML output. - - Args: - html: Raw HTML from mandoc - - Returns: - Cleaned HTML - """ - # Fix empty header cells - html = re.sub( - r'\(\)', - r'', - html, - ) - - # Remove empty

tags (from .sp directives in troff) - html = re.sub(r'

\s*

', '', html) - - # Clean up trailing whitespace and br tags in pre blocks - # Match:
...
and clean trailing
followed by whitespace - def clean_pre_block(match): - content = match.group(1) - # Remove trailing
tags and whitespace before closing - content = re.sub(r'\s*$', '', content) - content = re.sub(r'\s+$', '', content) - return f'
{content}
' - - html = re.sub(r'
(.*?)
', clean_pre_block, html, flags=re.DOTALL) - - html = html.strip() - - return html - - 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"] - target_filename = f"{name}.{section}.html" - - html = f'''''' - return html - - def link_cross_references(self, man_files: List[ManFile], version: str) -> None: - """Add hyperlinks to cross-references in man pages. - - Args: - man_files: List of all converted ManFile objects - version: Rocky Linux version - """ - lookup = {} - for mf in man_files: - key = (mf.name.lower(), str(mf.section)) - if key not in lookup: - lookup[key] = f"{mf.package_name}/man{mf.section}/{mf.html_filename}" - - logger.info(f"Linking cross-references across {len(man_files)} man pages...") - - for man_file in man_files: - if not man_file.html_content: - continue - - try: - html = man_file.html_content - pattern = ( - r"([\w\-_.]+)\((\d+[a-z]*)\)|\b([\w\-_.]+)\((\d+[a-z]*)\)" - ) - - def replace_reference(match): - full_match = match.group(0) - - # Skip if already inside an tag - before_text = html[max(0, match.start() - 500) : match.start()] - last_open = before_text.rfind("") - if last_open > last_close: - return full_match - - name = (match.group(1) or match.group(3)).lower() - section = match.group(2) or match.group(4) - - key = (name, section) - if key in lookup: - target_path = lookup[key] - rel_path = f"../../../{version}/{target_path}" - return f'{full_match}' - - return full_match - - updated_html = re.sub(pattern, replace_reference, html) - if updated_html != html: - man_file.html_content = updated_html - - except Exception as e: - logger.warning( - f"Error linking references in {man_file.display_name}: {e}" - ) - - logger.info("Cross-reference linking complete") - - def _get_output_path(self, man_file: ManFile) -> Path: - """Determine output path for HTML file.""" - pkg_dir = self.output_dir / man_file.package_name - section_dir = pkg_dir / f"man{man_file.section}" - return section_dir / man_file.html_filename diff --git a/src/rocky_man/processor/extractor.py b/src/rocky_man/processor/extractor.py deleted file mode 100644 index ab7aa4e..0000000 --- a/src/rocky_man/processor/extractor.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Extract man pages from RPM packages.""" - -import gzip -import logging -from pathlib import Path -from typing import List -from concurrent.futures import ThreadPoolExecutor, as_completed - -import rpmfile - -from ..models import Package, ManFile - -logger = logging.getLogger(__name__) - - -class ManPageExtractor: - """Extracts man pages from RPM packages. - - Handles: - - Extracting man pages from RPMs - - Reading gzipped man page content - - Organizing extracted files by package - """ - - def __init__(self, extract_dir: Path, skip_sections: List[str] = None, skip_languages: bool = True): - """Initialize extractor. - - Args: - extract_dir: Base directory for extracting man pages - skip_sections: List of man sections to skip (e.g., ['3', '3p', '3pm']) - skip_languages: If True, skip non-English man pages - """ - self.extract_dir = Path(extract_dir) - self.extract_dir.mkdir(parents=True, exist_ok=True) - self.skip_sections = skip_sections or [] - self.skip_languages = skip_languages - - def extract_from_package(self, package: Package) -> List[ManFile]: - """Extract all man pages from a package. - - Args: - package: Package to extract from - - Returns: - List of ManFile objects for extracted man pages - """ - if not package.download_path or not package.download_path.exists(): - logger.warning(f"Package file not found: {package.name}") - return [] - - pkg_extract_dir = self.extract_dir / package.name - pkg_extract_dir.mkdir(parents=True, exist_ok=True) - - man_files = [] - - try: - logger.info(f"Extracting man pages from {package.filename}") - - with rpmfile.open(package.download_path) as rpm: - for member in rpm.getmembers(): - if not self._is_manpage(member.name): - continue - - # Sanitize path to prevent path traversal attacks - safe_name = member.name.lstrip('/') - extract_path = pkg_extract_dir / safe_name - - # Resolve to absolute path and verify it's within the extraction directory - real_extract_path = extract_path.resolve() - real_pkg_extract_dir = pkg_extract_dir.resolve() - - if not real_extract_path.is_relative_to(real_pkg_extract_dir): - logger.warning(f"Skipping file with path traversal attempt: {member.name}") - continue - - man_file = ManFile( - file_path=real_extract_path, - package_name=package.name - ) - - if self.skip_sections and man_file.section in self.skip_sections: - logger.debug(f"Skipping {man_file.display_name} (section {man_file.section})") - continue - - if self.skip_languages and man_file.language and man_file.language != 'en': - logger.debug(f"Skipping {man_file.display_name} (language {man_file.language})") - continue - - real_extract_path.parent.mkdir(parents=True, exist_ok=True) - - try: - content = rpm.extractfile(member).read() - with open(real_extract_path, 'wb') as f: - f.write(content) - - man_file.content = content - man_files.append(man_file) - - except Exception as e: - logger.warning(f"Failed to extract {member.name}: {e}") - - logger.info(f"Extracted {len(man_files)} man pages from {package.name}") - - except Exception as e: - logger.error(f"Error extracting from {package.filename}: {e}") - - return man_files - - def extract_from_packages( - self, - packages: List[Package], - max_workers: int = 5 - ) -> List[ManFile]: - """Extract man pages from multiple packages in parallel. - - Args: - packages: List of packages to process - max_workers: Maximum number of parallel extractions - - Returns: - List of all extracted ManFile objects - """ - all_man_files = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_pkg = { - executor.submit(self.extract_from_package, pkg): pkg - for pkg in packages - } - - for future in as_completed(future_to_pkg): - pkg = future_to_pkg[future] - try: - man_files = future.result() - all_man_files.extend(man_files) - except Exception as e: - logger.error(f"Error processing {pkg.name}: {e}") - - logger.info(f"Extracted total of {len(all_man_files)} man pages from {len(packages)} packages") - return all_man_files - - def read_manpage_content(self, man_file: ManFile) -> str: - """Read and decompress man page content. - - Args: - man_file: ManFile to read - - Returns: - Decompressed man page content as string - """ - if not man_file.file_path.exists(): - logger.warning(f"Man page file not found: {man_file.file_path}") - return "" - - try: - if man_file.file_path.suffix == '.gz': - try: - with gzip.open(man_file.file_path, 'rb') as f: - return f.read().decode('utf-8', errors='replace') - except gzip.BadGzipFile: - pass - - with open(man_file.file_path, 'rb') as f: - return f.read().decode('utf-8', errors='replace') - - except Exception as e: - logger.error(f"Error reading {man_file.file_path}: {e}") - return "" - - @staticmethod - def _is_manpage(path: str) -> bool: - """Check if a file path is a man page.""" - if '/man/' not in path: - return False - - if not ('/share/man/' in path or path.startswith('/usr/man/')): - return False - - parts = path.split('/') - return any( - part.startswith('man') and len(part) > 3 and part[3].isdigit() - for part in parts - ) - - def cleanup_extracts(self, package: Package): - """Clean up extracted files for a package. - - Args: - package: Package whose extracts to clean up - """ - pkg_extract_dir = self.extract_dir / package.name - if pkg_extract_dir.exists(): - import shutil - shutil.rmtree(pkg_extract_dir) - logger.debug(f"Cleaned up extracts for {package.name}") diff --git a/src/rocky_man/repo/__init__.py b/src/rocky_man/repo/__init__.py deleted file mode 100644 index a3d5bc3..0000000 --- a/src/rocky_man/repo/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .manager import RepoManager -from .contents import ContentsParser - -__all__ = ["RepoManager", "ContentsParser"] diff --git a/src/rocky_man/repo/contents.py b/src/rocky_man/repo/contents.py deleted file mode 100644 index a32f098..0000000 --- a/src/rocky_man/repo/contents.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Contents file parser for identifying packages with man pages.""" - -import gzip -import logging -import xml.etree.ElementTree as ET -from pathlib import Path -from typing import Set -from urllib.parse import urljoin - -import requests - -logger = logging.getLogger(__name__) - - -class ContentsParser: - """Parse repository metadata to identify packages containing man pages. - - This is a key optimization - instead of downloading all packages, - we parse the filelists.xml to find only packages with man pages. - """ - - def __init__(self, repo_url: str, cache_dir: Path): - """Initialize the contents parser. - - Args: - repo_url: Base URL of the repository (e.g., .../BaseOS/x86_64/os/) - cache_dir: Directory to cache downloaded metadata - """ - self.repo_url = repo_url.rstrip('/') + '/' - self.cache_dir = Path(cache_dir) - self.cache_dir.mkdir(parents=True, exist_ok=True) - - def get_packages_with_manpages(self) -> Set[str]: - """Get set of package names that contain man pages. - - Returns: - Set of package names (e.g., {'bash', 'coreutils', ...}) - """ - logger.info(f"Fetching filelists for {self.repo_url}") - - filelists_path = self._get_filelists_path() - if not filelists_path: - logger.warning("Could not find filelists in repository metadata") - return set() - - filelists_file = self._download_filelists(filelists_path) - if not filelists_file: - logger.warning("Could not download filelists") - return set() - - packages = self._parse_filelists(filelists_file) - logger.info(f"Found {len(packages)} packages with man pages") - - return packages - - def _get_filelists_path(self) -> str: - """Parse repomd.xml to get the filelists.xml location. - - Returns: - Relative path to filelists.xml.gz - """ - repomd_url = urljoin(self.repo_url, 'repodata/repomd.xml') - - try: - response = requests.get(repomd_url, timeout=30) - response.raise_for_status() - - root = ET.fromstring(response.content) - ns = {'repo': 'http://linux.duke.edu/metadata/repo'} - - for data in root.findall('repo:data', ns): - if data.get('type') == 'filelists': - location = data.find('repo:location', ns) - if location is not None: - return location.get('href') - - # Fallback without namespace - for data in root.findall('data'): - if data.get('type') == 'filelists': - location = data.find('location') - if location is not None: - return location.get('href') - - except Exception as e: - logger.error(f"Error parsing repomd.xml: {e}") - - return None - - def _download_filelists(self, relative_path: str) -> Path: - """Download filelists.xml.gz file. - - Args: - relative_path: Relative path from repo root (e.g., 'repodata/...-filelists.xml.gz') - - Returns: - Path to downloaded file - """ - url = urljoin(self.repo_url, relative_path) - cache_file = self.cache_dir / relative_path.split('/')[-1] - - if cache_file.exists(): - logger.debug(f"Using cached filelists: {cache_file}") - return cache_file - - try: - logger.info(f"Downloading {url}") - response = requests.get(url, timeout=60, stream=True) - response.raise_for_status() - - cache_file.parent.mkdir(parents=True, exist_ok=True) - with open(cache_file, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - return cache_file - - except Exception as e: - logger.error(f"Error downloading filelists: {e}") - return None - - def _parse_filelists(self, filelists_path: Path) -> Set[str]: - """Parse filelists.xml.gz to find packages with man pages. - - Args: - filelists_path: Path to filelists.xml.gz file - - Returns: - Set of package names containing man pages - """ - packages = set() - - try: - with gzip.open(filelists_path, 'rb') as f: - context = ET.iterparse(f, events=('start', 'end')) - - current_package = None - has_manpage = False - - for event, elem in context: - if event == 'start' and elem.tag.endswith('package'): - current_package = elem.get('name') - has_manpage = False - - elif event == 'end': - if elem.tag.endswith('file'): - file_path = elem.text - if file_path and self._is_manpage_path(file_path): - has_manpage = True - - elif elem.tag.endswith('package'): - if has_manpage and current_package: - packages.add(current_package) - elem.clear() - current_package = None - has_manpage = False - - except Exception as e: - logger.error(f"Error parsing filelists: {e}") - - return packages - - @staticmethod - def _is_manpage_path(file_path: str) -> bool: - """Check if a file path is a man page location. - - Args: - file_path: File path to check - - Returns: - True if path is in a standard man page directory - """ - return '/man/' in file_path and ( - '/share/man/' in file_path or file_path.startswith('/usr/man/') - ) diff --git a/src/rocky_man/repo/manager.py b/src/rocky_man/repo/manager.py deleted file mode 100644 index d1e25c5..0000000 --- a/src/rocky_man/repo/manager.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Repository manager for querying and downloading RPM packages.""" - -import logging -from pathlib import Path -from typing import List, Set, Optional -from concurrent.futures import ThreadPoolExecutor, as_completed - -import dnf -import requests - -from ..models import Package -from .contents import ContentsParser - -logger = logging.getLogger(__name__) - - -class RepoManager: - """Manages Rocky Linux repository operations. - - Handles: - - Repository configuration with DNF - - Package discovery and filtering - - Package downloads with progress tracking - """ - - def __init__( - self, - config, - version: str, - repo_type: str, - arch: str, - cache_dir: Path, - download_dir: Path, - ): - """Initialize repository manager. - - Args: - 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.config = config - self.version = version - self.repo_type = repo_type - self.arch = arch - self.cache_dir = Path(cache_dir) - self.download_dir = Path(download_dir) - - self.cache_dir.mkdir(parents=True, exist_ok=True) - self.download_dir.mkdir(parents=True, exist_ok=True) - - self.base = dnf.Base() - self.base.conf.debuglevel = 0 - self.base.conf.errorlevel = 0 - self.base.conf.cachedir = str(self.cache_dir / "dnf") - - self.repo_url = None - self.packages_with_manpages: Optional[Set[str]] = None - - def _configure_repo(self): - """Configure DNF repository.""" - repo_id = f"rocky-{self.repo_type.lower()}-{self.version}-{self.arch}" - repo = dnf.repo.Repo(repo_id, self.base.conf) - repo.baseurl = [self.repo_url] - repo.enabled = True - repo.gpgcheck = False - - self.base.repos.add(repo) - logger.info(f"Configured repository: {repo_id} at {self.repo_url}") - - self.base.fill_sack(load_system_repo=False, load_available_repos=True) - logger.info("Repository metadata loaded") - - def discover_packages_with_manpages(self) -> Set[str]: - """Discover which packages contain man pages using filelists. - - Returns: - Set of package names that contain man pages - """ - if self.packages_with_manpages is not None: - return self.packages_with_manpages - - 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: - self.packages_with_manpages = packages - self.repo_url = repo_url - 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 - - def list_packages(self, with_manpages_only: bool = True) -> List[Package]: - """List all packages in the repository. - - Args: - with_manpages_only: If True, only return packages with man pages - - Returns: - List of Package objects - """ - logger.info( - f"Querying packages from {self.repo_type} ({self.version}/{self.arch})" - ) - - manpage_packages = None - if with_manpages_only: - manpage_packages = self.discover_packages_with_manpages() - logger.info(f"Filtering to {len(manpage_packages)} packages with man pages") - - self._configure_repo() - - packages = [] - query = self.base.sack.query().available() - seen_names = set() - - for pkg in query: - pkg_name = pkg.name - - if pkg_name in seen_names: - continue - - if manpage_packages and pkg_name not in manpage_packages: - continue - - repo = pkg.repo - baseurl = repo.baseurl[0] if repo and repo.baseurl else self.repo_url - chksum_type, chksum_value = pkg.chksum if pkg.chksum else ("sha256", "") - package = Package( - name=pkg_name, - version=pkg.version, - release=pkg.release, - arch=pkg.arch, - repo_type=self.repo_type, - location=pkg.location, - baseurl=baseurl, - checksum=chksum_value, - checksum_type=chksum_type, - has_manpages=bool(manpage_packages), - ) - - packages.append(package) - seen_names.add(pkg_name) - - logger.info(f"Found {len(packages)} packages to process") - return sorted(packages) - - def download_package(self, package: Package) -> bool: - """Download a single package. - - Args: - package: Package to download - - Returns: - True if download successful, False otherwise - """ - download_path = self.download_dir / package.filename - package.download_path = download_path - - if download_path.exists(): - logger.debug(f"Package already downloaded: {package.filename}") - return True - - try: - logger.info(f"Downloading {package.filename}") - response = requests.get(package.download_url, timeout=300, stream=True) - response.raise_for_status() - - with open(download_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - - logger.debug(f"Downloaded: {package.filename}") - return True - - except Exception as e: - logger.error(f"Error downloading {package.filename}: {e}") - if download_path.exists(): - download_path.unlink() - return False - - def download_packages( - self, packages: List[Package], max_workers: int = 5 - ) -> List[Package]: - """Download multiple packages in parallel. - - Args: - packages: List of packages to download - max_workers: Maximum number of parallel downloads - - Returns: - List of successfully downloaded packages - """ - downloaded = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_pkg = { - executor.submit(self.download_package, pkg): pkg for pkg in packages - } - - for future in as_completed(future_to_pkg): - pkg = future_to_pkg[future] - try: - if future.result(): - downloaded.append(pkg) - except Exception as e: - logger.error(f"Error processing {pkg.name}: {e}") - - logger.info( - f"Successfully downloaded {len(downloaded)}/{len(packages)} packages" - ) - return downloaded - - def cleanup_package(self, package: Package): - """Delete a downloaded package file. - - Args: - package: Package to clean up - """ - if package.download_path and package.download_path.exists(): - package.download_path.unlink() - logger.debug(f"Deleted: {package.filename}") diff --git a/src/rocky_man/utils/__init__.py b/src/rocky_man/utils/__init__.py deleted file mode 100644 index 786c82d..0000000 --- a/src/rocky_man/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .config import Config - -__all__ = ["Config"] diff --git a/src/rocky_man/utils/config.py b/src/rocky_man/utils/config.py deleted file mode 100644 index 32d36f4..0000000 --- a/src/rocky_man/utils/config.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Configuration management for Rocky Man.""" - -from dataclasses import dataclass -from pathlib import Path -from typing import List - - -@dataclass -class Config: - """Configuration for Rocky Man page generation. - - Attributes: - base_url: Base URL for Rocky Linux mirror - content_dir: Content directory path (usually 'pub/rocky') - versions: List of Rocky Linux versions to process (e.g., ['8.10', '9.5']) - architectures: List of architectures to consider (we'll pick one) - repo_types: Repository types to process (e.g., ['BaseOS', 'AppStream']) - download_dir: Directory for downloading RPM packages - extract_dir: Directory for extracting man pages - output_dir: Directory for generated HTML files - keep_rpms: Whether to keep downloaded RPM files after processing - keep_extracts: Whether to keep extracted man files after processing - parallel_downloads: Number of parallel downloads - parallel_conversions: Number of parallel HTML conversions - """ - - base_url: str = "http://dl.rockylinux.org/" - content_dir: str = "pub/rocky" - versions: List[str] = None - architectures: List[str] = None - repo_types: List[str] = None - - download_dir: Path = Path("/data/tmp/downloads") - extract_dir: Path = Path("/data/tmp/extracts") - output_dir: Path = Path("/data/html") - - keep_rpms: bool = False - keep_extracts: bool = False - - parallel_downloads: int = 5 - parallel_conversions: int = 10 - - skip_sections: List[str] = None - skip_packages: List[str] = None - skip_languages: bool = True - allow_all_sections: bool = False - - def __post_init__(self): - """Set defaults and ensure directories exist.""" - if self.versions is None: - self.versions = ["8.10", "9.6", "10.0"] - - if self.architectures is None: - self.architectures = ["x86_64", "aarch64", "ppc64le", "s390x"] - - if self.repo_types is None: - self.repo_types = ["BaseOS", "AppStream"] - - if self.skip_sections is None and not self.allow_all_sections: - self.skip_sections = ["3", "3p", "3pm"] - elif self.allow_all_sections: - self.skip_sections = [] - - if self.skip_packages is None: - self.skip_packages = [ - "lapack", - "dpdk-devel", - "gl-manpages", - ] - - self.download_dir = Path(self.download_dir) - self.extract_dir = Path(self.extract_dir) - self.output_dir = Path(self.output_dir) - - def get_repo_url(self, version: str, repo_type: str, arch: str) -> str: - """Construct repository URL for given parameters. - - Args: - version: Rocky Linux version (e.g., '9.5') - repo_type: Repository type ('BaseOS' or 'AppStream') - arch: Architecture (e.g., 'x86_64') - - Returns: - Full repository URL - """ - url = self.base_url.rstrip('/') - path = f"{self.content_dir}/{version}/{repo_type}/{arch}/os" - return f"{url}/{path}/" - - def get_version_output_dir(self, version: str) -> Path: - """Get output directory for a specific version.""" - return self.output_dir / version - - def get_version_download_dir(self, version: str) -> Path: - """Get download directory for a specific version.""" - return self.download_dir / version - - def get_version_extract_dir(self, version: str) -> Path: - """Get extract directory for a specific version.""" - return self.extract_dir / version diff --git a/src/rocky_man/web/__init__.py b/src/rocky_man/web/__init__.py deleted file mode 100644 index e151fe4..0000000 --- a/src/rocky_man/web/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .generator import WebGenerator - -__all__ = ["WebGenerator"] diff --git a/src/rocky_man/web/generator.py b/src/rocky_man/web/generator.py deleted file mode 100644 index 30eb914..0000000 --- a/src/rocky_man/web/generator.py +++ /dev/null @@ -1,322 +0,0 @@ -"""Web page generator for Rocky Man.""" - -import gzip -import json -import logging -from collections import defaultdict -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) - - 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, - ) - - 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: - packages_by_letter = defaultdict(list) - - for pkg_name, pages in search_data.items(): - first_char = pkg_name[0].upper() - if not first_char.isalpha(): - first_char = "other" - packages_by_letter[first_char].append( - {"name": pkg_name, "count": len(pages)} - ) - - for packages in packages_by_letter.values(): - packages.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] = {} - - 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}", - } - - 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" - sorted_index = {k: index[k] for k in sorted(index)} - - with open(json_path, "w", encoding="utf-8") as f: - json.dump(sorted_index, f, indent=2) - - 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") - - major_to_minors = defaultdict(list) - for v in versions: - try: - major, minor = v.split(".") - major_to_minors[major].append(minor) - except ValueError: - continue - - sorted_majors = sorted(major_to_minors, key=int) - max_minors = max((len(major_to_minors[m]) for m in sorted_majors), default=0) - num_columns = len(sorted_majors) - - 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) - version_rows.append(row) - - html = template.render( - title="Rocky Linux Man Pages", version_rows=version_rows, num_columns=num_columns - ) - - 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 - - def generate_404_page(self) -> bool: - """Generate 404 error page. - - Returns: - True if successful - """ - try: - template = self.env.get_template("404.html") - - html = template.render( - title="404 - Page Not Found" - ) - - error_path = self.output_dir / "404.html" - - with open(error_path, "w", encoding="utf-8") as f: - f.write(html) - - logger.info("Generated 404 page") - return True - - except Exception as e: - logger.error(f"Error generating 404 page: {e}") - return False diff --git a/templates/404.html b/templates/404.html deleted file mode 100644 index a59e047..0000000 --- a/templates/404.html +++ /dev/null @@ -1,137 +0,0 @@ -{% extends "base.html" %} - -{% block header_title %}Rocky Linux Man Pages{% endblock %} -{% block header_subtitle %}Man page documentation for Rocky Linux packages{% endblock %} - -{% block extra_css %} -.error-container { - text-align: center; - padding: 4rem 2rem; -} - -.error-code { - font-size: 8rem; - font-weight: 700; - color: var(--accent-primary); - line-height: 1; - margin-bottom: 1rem; - font-family: "JetBrains Mono", monospace; -} - -.error-message { - font-size: 1.5rem; - color: var(--text-primary); - margin-bottom: 1rem; -} - -.error-description { - color: var(--text-secondary); - margin-bottom: 2rem; - max-width: 600px; - margin-left: auto; - margin-right: auto; -} - -.suggestions { - max-width: 600px; - margin: 2rem auto; - text-align: left; -} - -.suggestions h3 { - color: var(--text-primary); - margin-bottom: 1rem; -} - -.suggestions ul { - list-style: none; - padding: 0; -} - -.suggestions li { - margin-bottom: 0.75rem; - padding-left: 1.5rem; - position: relative; -} - -.suggestions li::before { - content: "β†’"; - position: absolute; - left: 0; - color: var(--accent-primary); -} - -.back-button { - display: inline-block; - padding: 0.75rem 1.5rem; - background: var(--accent-primary); - color: white; - text-decoration: none; - border-radius: 6px; - font-weight: 500; - transition: all 0.2s; - margin-top: 2rem; -} - -.back-button:hover { - background: var(--accent-secondary); - transform: translateY(-2px); - text-decoration: none; -} - -@media (max-width: 768px) { - .error-code { - font-size: 5rem; - } - - .error-message { - font-size: 1.25rem; - } - - .error-container { - padding: 3rem 1rem; - } -} - -@media (max-width: 480px) { - .error-code { - font-size: 4rem; - } - - .error-message { - font-size: 1.1rem; - } - - .error-container { - padding: 2rem 1rem; - } - - .suggestions { - padding: 0 1rem; - } -} -{% endblock %} - -{% block content %} -
-
-
404
-
Page Not Found
-
- The page you're looking for doesn't exist or may have been moved. -
- -
-

Suggestions:

-
    -
  • Check the URL for typos
  • -
  • Return to the home page and navigate from there
  • -
  • Use the search feature on the version index page
  • -
  • The man page may be in a different version of Rocky Linux
  • -
-
- - Go to Home Page -
-
-{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 6149414..0000000 --- a/templates/base.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - {% block title %}{{ title }}{% endblock %} - - - - - - - -
-
- - - - - - -
-

{% block header_title %}Rocky Linux Man Pages{% endblock %}

-

{% block header_subtitle %}Comprehensive man page documentation{% endblock %}

-
-
-
- -
- {% block content %}{% endblock %} -
- - - - {% block scripts %}{% endblock %} - - diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index e3f1dc9..0000000 --- a/templates/index.html +++ /dev/null @@ -1,359 +0,0 @@ -{% extends "base.html" %} - -{% block header_title %}Rocky Linux {{ version }} Man Pages{% endblock %} -{% block header_subtitle %}Search and browse {{ total_pages }} man pages{% endblock %} - -{% block extra_css %} -.search-box { -margin-bottom: 2rem; -} - -.search-input { -width: 100%; -padding: 0.75rem 1rem; -font-size: 1rem; -background-color: var(--bg-tertiary); -border: 1px solid var(--border-color); -border-radius: 6px; -color: var(--text-primary); -transition: border-color 0.2s, box-shadow 0.2s; -} - -.search-input:focus { -outline: none; -border-color: var(--accent-primary); -box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2); -} - -.search-input:disabled { -opacity: 0.5; -cursor: not-allowed; -} - -.search-stats { -margin-top: 1rem; -color: var(--text-secondary); -font-size: 0.9rem; -} - -.results-list { -list-style: none; -padding: 0; -} - -.result-item { -padding: 0.75rem 0; -border-bottom: 1px solid var(--border-color); -} - -.result-item:last-child { -border-bottom: none; -} - -.result-link { -font-size: 1.1rem; -display: flex; -align-items: baseline; -gap: 0.5rem; -} - -.result-section { -color: var(--text-secondary); -font-size: 0.9rem; -} - -.result-package { -color: var(--text-secondary); -font-size: 0.85rem; -margin-left: auto; -} - -.loading { -text-align: center; -padding: 2rem; -color: var(--text-secondary); -} - -.spinner { -display: inline-block; -width: 20px; -height: 20px; -border: 3px solid var(--border-color); -border-top-color: var(--accent-primary); -border-radius: 50%; -animation: spin 0.8s linear infinite; -} - -@keyframes spin { -to { transform: rotate(360deg); } -} - -.no-results { -text-align: center; -padding: 3rem 1rem; -color: var(--text-secondary); -} - -.quick-links { -margin-top: 2rem; -padding-top: 1.5rem; -border-top: 1px solid var(--border-color); -} - -.quick-links h3 { -margin-bottom: 1rem; -color: var(--text-primary); -} - -.package-grid { -display: grid; -grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); -gap: 0.5rem; -} - -.package-link { -padding: 0.5rem; -background-color: var(--bg-tertiary); -border: 1px solid var(--border-color); -border-radius: 4px; -text-align: center; -transition: background-color 0.2s, border-color 0.2s; -min-height: 44px; -display: flex; -align-items: center; -justify-content: center; -} - -.package-link:hover { -background-color: var(--bg-primary); -border-color: var(--accent-primary); -text-decoration: none; -} - -.view-all-container { -text-align: center; -margin-top: 1.5rem; -} - -.view-all-button { -display: inline-block; -padding: 0.75rem 1.5rem; -background-color: var(--bg-tertiary); -border: 1px solid var(--border-color); -border-radius: 6px; -color: var(--accent-primary); -text-decoration: none; -font-weight: 600; -transition: all 0.2s; -min-height: 44px; -} - -.view-all-button:hover { -background-color: var(--bg-primary); -border-color: var(--accent-primary); -transform: translateY(-2px); -box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -text-decoration: none; -} - -@media (max-width: 768px) { - .search-input { - font-size: 16px; - } - - .package-grid { - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - } - - .result-link { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - .result-package { - margin-left: 0; - } -} - -@media (max-width: 480px) { - .package-grid { - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - } - - .quick-links h3 { - font-size: 1.2rem; - } -} -{% endblock %} - -{% block content %} -
- - -
-
    -
    - - - -
    - -{% endblock %} - -{% block scripts %} - - -{% endblock %} \ No newline at end of file diff --git a/templates/manpage.html b/templates/manpage.html deleted file mode 100644 index 157ad9a..0000000 --- a/templates/manpage.html +++ /dev/null @@ -1,318 +0,0 @@ -{% extends "base.html" %} - -{% block header_title %}{{ header_title }}{% endblock %} -{% block header_subtitle %}{{ package_name }} - Rocky Linux {{ version }}{% endblock %} - -{% block extra_css %} -/* Man page specific styles */ -.man-header { -margin-bottom: 2rem; -padding-bottom: 1.5rem; -border-bottom: 1px solid var(--border-color); -} - -.header-left { -display: flex; -flex-direction: column; -gap: 1rem; -} - -.back-button { -display: inline-flex; -align-items: center; -gap: 0.5rem; -color: var(--text-secondary); -font-size: 0.9rem; -font-weight: 500; -text-decoration: none; -transition: color 0.2s; -align-self: flex-start; -} - -.back-button:hover { -color: var(--accent-primary); -text-decoration: none; -} - -.title-group { -display: flex; -flex-direction: column; -gap: 0.5rem; -} - -.man-meta { -display: flex; -flex-wrap: wrap; -gap: 1.5rem; -color: var(--text-secondary); -font-size: 0.9rem; -} - -.meta-item { -display: inline-flex; -align-items: center; -gap: 0.5rem; -} - -/* Style the mandoc output */ -.man-content { -line-height: 1.8; -} - -.man-content table { -width: 100%; -margin-bottom: 1rem; -border-collapse: collapse; -} - -.man-content table.head, -.man-content table.foot { -background-color: var(--bg-tertiary); -} - -.man-content td { -padding: 0.5rem; -} - -.man-content .head-ltitle, -.man-content .head-vol, -.man-content .head-rtitle { -color: var(--text-primary); -font-weight: 600; -} - -.man-content .head-vol { -text-align: center; -} - -.man-content .head-rtitle { -text-align: right; -} - -.man-content h1, .man-content h2 { -color: var(--accent-primary); -margin-top: 2rem; -margin-bottom: 1rem; -font-size: 1.5rem; -} - -.man-content h2 { -font-size: 1.3rem; -} - -.man-content code, -.man-content .Nm, -.man-content .Cm, -.man-content .Fl { -background-color: var(--bg-tertiary); -padding: 0.2rem 0.4rem; -border-radius: 3px; -font-family: 'Monaco', 'Courier New', monospace; -font-size: 0.9em; -color: var(--success); -} - -/* OPTIONS section specific styling */ -/* Style paragraphs that contain option flags (b tags followed by i tags or immediately followed by Bd-indent) */ -.man-content section.Sh p.Pp:has(+ .Bd-indent) { -font-weight: 600; -font-size: 1.05em; -margin-top: 1.5rem; -margin-bottom: 0.5rem; -padding: 0.5rem 0.75rem; -background: linear-gradient(90deg, var(--bg-tertiary) 0%, transparent 100%); -border-left: 3px solid var(--accent-primary); -} - -.man-content section.Sh p.Pp:has(+ .Bd-indent) b { -color: var(--accent-primary); -font-size: 1em; -} - -.man-content section.Sh p.Pp:has(+ .Bd-indent) i { -color: var(--text-secondary); -font-style: italic; -} - -/* Indented description blocks */ -.man-content .Bd-indent { -margin-left: 2.5rem; -margin-bottom: 1.5rem; -padding-left: 1rem; -border-left: 2px solid var(--border-color); -color: var(--text-primary); -} - -/* Add spacing between nested paragraphs in descriptions */ -.man-content .Bd-indent > p.Pp { -margin-top: 0.75rem; -margin-bottom: 0.75rem; -} - -.man-content .Bd-indent > p.Pp:first-child { -margin-top: 0; -} - -.man-content pre { -background-color: var(--bg-primary); -border: 1px solid var(--border-color); -border-radius: 6px; -padding: 1rem; -overflow-x: auto; --webkit-overflow-scrolling: touch; -} - -.man-content .Bl-bullet, -.man-content .Bl-enum, -.man-content .Bl-dash { -margin: 1rem 0; -padding-left: 2rem; -} - -.man-content .Bl-tag { -margin: 1rem 0; -} - -.man-content dt { -font-weight: 600; -color: var(--accent-primary); -margin-top: 0.5rem; -} - -.man-content dd { -margin-left: 2rem; -margin-bottom: 0.5rem; -} - -.man-content a { -color: var(--accent-primary); -text-decoration: none; -} - -.man-content a:hover { -text-decoration: underline; -} - -/* Table of contents */ -.man-content .Bl-compact.toc { -background-color: var(--bg-tertiary); -border: 1px solid var(--border-color); -border-radius: 6px; -padding: 1rem; -margin: 1rem 0; -} - -.man-content .toc li { -margin: 0.25rem 0; -} - -/* Responsive */ -@media (max-width: 768px) { -.man-header { -flex-direction: column; -align-items: flex-start; -gap: 1rem; -} - -.man-meta { -flex-direction: column; -gap: 0.5rem; -} - -.man-content h1, .man-content h2 { -font-size: 1.3rem; -margin-top: 1.5rem; -} - -.man-content h2 { -font-size: 1.1rem; -} - -.man-content pre { -font-size: 0.85rem; -padding: 0.75rem; -} - -.man-content code, -.man-content .Nm, -.man-content .Cm, -.man-content .Fl { -font-size: 0.85em; -word-break: break-word; -} - -.man-content table { -display: block; -overflow-x: auto; --webkit-overflow-scrolling: touch; -} - -.man-content dd { -margin-left: 1rem; -} - -.man-content .Bl-bullet, -.man-content .Bl-enum, -.man-content .Bl-dash { -padding-left: 1rem; -} - -.man-content section.Sh p.Pp:has(+ .Bd-indent) { -font-size: 1em; -padding: 0.4rem 0.5rem; -} - -.man-content .Bd-indent { -margin-left: 1.5rem; -padding-left: 0.75rem; -} -} - -@media (max-width: 480px) { -.back-button { -font-size: 0.85rem; -} - -.man-content h1, .man-content h2 { -font-size: 1.2rem; -} - -.man-content h2 { -font-size: 1rem; -} - -.man-content { -line-height: 1.6; -} -} -{% endblock %} - -{% block content %} -
    -
    -
    - - - - - Back to Search - -
    -

    {{ header_title }}

    -
    - Package: {{ package_name }} - Section: {{ section }} - {% if language != 'en' %} - Language: {{ language }} - {% endif %} -
    -
    -
    -
    - -
    - {{ content|safe }} -
    -
    -{% endblock %} \ No newline at end of file diff --git a/templates/packages.html b/templates/packages.html deleted file mode 100644 index 626b319..0000000 --- a/templates/packages.html +++ /dev/null @@ -1,226 +0,0 @@ -{% extends "base.html" %} - -{% block header_title %}All Packages{% endblock %} -{% block header_subtitle %}Browse all {{ total_packages }} packages in Rocky Linux {{ version }}{% endblock %} - -{% block extra_css %} -.back-button { -display: inline-flex; -align-items: center; -gap: 0.5rem; -color: var(--text-secondary); -font-size: 0.9rem; -font-weight: 500; -text-decoration: none; -transition: color 0.2s; -} - -.back-button:hover { -color: var(--accent-primary); -text-decoration: none; -} - -.az-nav { -display: flex; -flex-wrap: wrap; -gap: 0.5rem; -justify-content: center; -margin-bottom: 2rem; -padding-bottom: 2rem; -border-bottom: 1px solid var(--border-color); -} - -.az-link { -display: inline-flex; -align-items: center; -justify-content: center; -width: 2.5rem; -height: 2.5rem; -border-radius: 4px; -background-color: var(--bg-tertiary); -color: var(--text-primary); -text-decoration: none; -font-family: "JetBrains Mono", monospace; -font-weight: 600; -transition: all 0.2s; -} - -.az-link:hover { -background-color: var(--accent-primary); -color: white; -text-decoration: none; -} - -.az-link.disabled { -opacity: 0.3; -cursor: default; -pointer-events: none; -} - -.package-section { -margin-bottom: 3rem; -} - -.section-header { -display: flex; -align-items: center; -margin-bottom: 1.5rem; -padding-bottom: 0.5rem; -border-bottom: 1px solid var(--border-color); -} - -.section-letter { -font-size: 2rem; -font-weight: 700; -color: var(--accent-primary); -font-family: "Red Hat Display", sans-serif; -margin-right: 1rem; -} - -.section-count { -color: var(--text-secondary); -font-size: 0.9rem; -} - -.package-grid { -display: grid; -grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); -gap: 1rem; -} - -.package-card { -display: block; -padding: 1rem; -background-color: var(--bg-tertiary); -border: 1px solid var(--border-color); -border-radius: 6px; -text-decoration: none; -transition: all 0.2s; -} - -.package-card:hover { -transform: translateY(-2px); -border-color: var(--accent-primary); -box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -text-decoration: none; -} - -.pkg-name { -display: block; -font-weight: 600; -color: var(--text-primary); -margin-bottom: 0.25rem; -} - -.pkg-count { -display: block; -font-size: 0.85rem; -color: var(--text-secondary); -} - -.back-to-top { -display: inline-block; -margin-top: 2rem; -color: var(--text-secondary); -font-size: 0.9rem; -} - -@media (max-width: 768px) { - .az-nav { - gap: 0.375rem; - } - - .az-link { - width: 2.25rem; - height: 2.25rem; - font-size: 0.9rem; - } - - .package-grid { - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - } - - .section-letter { - font-size: 1.5rem; - } - - .package-card { - padding: 0.75rem; - } -} - -@media (max-width: 480px) { - .az-nav { - gap: 0.25rem; - } - - .az-link { - width: 2rem; - height: 2rem; - font-size: 0.85rem; - } - - .package-grid { - grid-template-columns: 1fr; - } - - .section-header { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - - .pkg-name { - font-size: 0.95rem; - } - - .pkg-count { - font-size: 0.8rem; - } -} -{% endblock %} - -{% block content %} -
    -
    - - - - - Back to Search - -
    - - - - {% for letter, packages in packages_by_letter.items()|sort %} -
    -
    - {{ letter }} - {{ packages|length }} packages -
    -
    - {% for package in packages %} - - {{ package.name }} - {{ package.count }} man pages - - {% endfor %} -
    - ↑ Back to top -
    - {% endfor %} -
    -{% endblock %} \ No newline at end of file diff --git a/templates/rocky-linux-logo.svg b/templates/rocky-linux-logo.svg deleted file mode 100644 index 15c8e8c..0000000 --- a/templates/rocky-linux-logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/root.html b/templates/root.html deleted file mode 100644 index 141c5bc..0000000 --- a/templates/root.html +++ /dev/null @@ -1,185 +0,0 @@ -{% extends "base.html" %} - -{% block header_title %}Rocky Linux Man Pages{% endblock %} -{% block header_subtitle %}Man page documentation for Rocky Linux packages{% endblock %} - -{% block extra_css %} -.logo-container { - text-align: center; - margin: 2rem 0 3rem 0; -} - -.logo-container svg { - max-width: 400px; - width: 100%; - height: auto; -} - - - -.version-grid { - display: grid; - grid-template-columns: repeat({{ num_columns }}, 1fr); - gap: 1.5rem; - margin-top: 2rem; -} - -@media (max-width: 768px) { - .logo-container { - margin: 1rem 0 2rem 0; - } - - .logo-container svg { - max-width: 280px; - } - - .version-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - .version-card { - 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) { - .logo-container svg { - max-width: 240px; - } - - .version-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - .version-card.small { - padding: 0.5rem; - } - - .intro { - font-size: 0.9rem; - } -} - -.version-card { - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 2rem; - text-align: center; - transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; - text-decoration: none; - 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); - border-color: var(--accent-primary); - text-decoration: none; -} - -.version-number { - font-size: 2.5rem; - font-weight: 700; - color: var(--accent-primary); - margin-bottom: 0.5rem; -} - -.version-label { - color: var(--text-secondary); - font-size: 0.9rem; - margin-bottom: 0.75rem; -} - -.version-browse { - color: var(--accent-primary); - font-size: 0.85rem; - font-weight: 500; -} - -.intro { - margin-bottom: 2rem; - color: var(--text-secondary); - line-height: 1.6; - max-width: 800px; - margin-left: auto; - margin-right: auto; - text-align: center; -} - -.version-section h2 { - margin-top: 2rem; - margin-bottom: 1rem; - color: var(--text-primary); - text-align: center; - font-size: 1.5rem; - font-weight: 600; -} -{% endblock %} - -{% block content %} -
    -
    - - - - -
    - -
    -

    - Man page documentation for packages in the Rocky Linux BaseOS and AppStream repositories. -

    -
    - -
    -

    Select Version

    -
    - {% for row in version_rows %} - {% set outer_loop = loop %} - {% for item in row %} - {% if item %} - {% set major, minor = item %} - -
    {{ major }}.{{ minor }}
    - {% if outer_loop.first %} -
    Rocky Linux
    -
    Browse man pages β†’
    - {% endif %} -
    - {% else %} -
    - {% endif %} - {% endfor %} - {% endfor %} -
    -
    -
    -{% endblock %}