""" Abstract base class for explode-implode variants. """ from abc import ABC, abstractmethod from pathlib import Path from typing import Dict, List, Any, Optional from dataclasses import dataclass from .enums import ExplodeVariant, ExplodeMode @dataclass class ExplodeOptions: """Options for explode operations.""" variant: ExplodeVariant mode: ExplodeMode = ExplodeMode.STANDARD output_dir: Optional[Path] = None max_depth: Optional[int] = None preserve_front_matter: bool = True section_spacing: int = 2 dry_run: bool = False verbose: bool = False create_manifest: bool = True @dataclass class ImplodeOptions: """Options for implode operations.""" output_file: Optional[Path] = None force_variant: Optional[ExplodeVariant] = None preserve_front_matter: bool = True section_spacing: int = 2 dry_run: bool = False verbose: bool = False overwrite: bool = False @dataclass class ExplodeResult: """Result of an explode operation.""" success: bool output_directory: Path files_created: List[Path] manifest_path: Optional[Path] warnings: List[str] errors: List[str] variant_used: ExplodeVariant @dataclass class ImplodeResult: """Result of an implode operation.""" success: bool output_file: Path files_processed: List[Path] variant_detected: Optional[ExplodeVariant] warnings: List[str] errors: List[str] class BaseVariant(ABC): """ Abstract base class for explode-implode variants. Each variant implements a specific strategy for organizing exploded markdown content and reconstructing it during implode operations. """ def __init__(self, variant_type: ExplodeVariant): """ Initialize the variant. Args: variant_type: The type of variant this implements """ self.variant_type = variant_type @property @abstractmethod def name(self) -> str: """Human-readable name of the variant.""" pass @property @abstractmethod def description(self) -> str: """Description of the variant's behavior.""" pass @abstractmethod def explode( self, input_file: Path, options: ExplodeOptions ) -> ExplodeResult: """ Explode a markdown file into a directory structure. Args: input_file: Path to the markdown file to explode options: Options controlling the explode operation Returns: Result of the explode operation Raises: FileNotFoundError: If input file doesn't exist PermissionError: If unable to create output directory ValueError: If input file is not valid markdown """ pass @abstractmethod def implode( self, input_directory: Path, options: ImplodeOptions ) -> ImplodeResult: """ Implode a directory structure back into a markdown file. Args: input_directory: Path to the directory to implode options: Options controlling the implode operation Returns: Result of the implode operation Raises: FileNotFoundError: If input directory doesn't exist ValueError: If directory structure is invalid for this variant """ pass @abstractmethod def can_handle_directory(self, directory: Path) -> bool: """ Check if this variant can handle the given directory structure. Args: directory: Path to the directory to check Returns: True if this variant can handle the directory """ pass @abstractmethod def get_detection_patterns(self) -> Dict[str, Any]: """ Get patterns used for auto-detecting this variant. Returns: Dictionary of detection patterns and weights """ pass def validate_input_file(self, input_file: Path) -> List[str]: """ Validate the input markdown file. Args: input_file: Path to the file to validate Returns: List of validation errors (empty if valid) """ errors = [] if not input_file.exists(): errors.append(f"Input file does not exist: {input_file}") return errors if not input_file.is_file(): errors.append(f"Input path is not a file: {input_file}") return errors if input_file.suffix.lower() not in ['.md', '.markdown']: errors.append(f"Input file is not a markdown file: {input_file}") try: content = input_file.read_text(encoding='utf-8') if not content.strip(): errors.append("Input file is empty") except UnicodeDecodeError: errors.append("Input file contains invalid UTF-8 encoding") except Exception as e: errors.append(f"Error reading input file: {e}") return errors def validate_input_directory(self, input_directory: Path) -> List[str]: """ Validate the input directory structure. Args: input_directory: Path to the directory to validate Returns: List of validation errors (empty if valid) """ errors = [] if not input_directory.exists(): errors.append(f"Input directory does not exist: {input_directory}") return errors if not input_directory.is_dir(): errors.append(f"Input path is not a directory: {input_directory}") return errors # Check if directory contains any markdown files md_files = list(input_directory.glob("**/*.md")) if not md_files: errors.append("Directory contains no markdown files") return errors def create_output_directory(self, output_dir: Path, overwrite: bool = False) -> List[str]: """ Create the output directory if it doesn't exist. Args: output_dir: Path to the directory to create overwrite: Whether to overwrite existing directory Returns: List of errors (empty if successful) """ errors = [] try: if output_dir.exists(): if not overwrite: errors.append(f"Output directory already exists: {output_dir}") return errors if output_dir.is_file(): errors.append(f"Output path exists and is a file: {output_dir}") return errors output_dir.mkdir(parents=True, exist_ok=overwrite) except PermissionError: errors.append(f"Permission denied creating directory: {output_dir}") except Exception as e: errors.append(f"Error creating output directory: {e}") return errors