""" Flat variant implementation for explode-implode operations. This variant represents the current default behavior where h1 headings become top-level directories with content organized beneath them. """ import re from pathlib import Path from typing import Dict, List, Any, Optional from .base_variant import ( BaseVariant, ExplodeOptions, ImplodeOptions, ExplodeResult, ImplodeResult ) from .enums import ExplodeVariant from .manifest_manager import ManifestManager, StructureEntry class FlatVariant(BaseVariant): """ Flat variant implementation. Creates directories based on h1 headings with nested content. This is the current default behavior for backward compatibility. Structure example: book.mdd/ ├── manifest.md ├── book_title/ │ ├── index.md │ ├── chapter_1.md │ └── chapter_2.md └── conclusion.md """ def __init__(self): """Initialize the flat variant.""" super().__init__(ExplodeVariant.FLAT) self.manifest_manager = ManifestManager() @property def name(self) -> str: """Human-readable name of the variant.""" return "Flat Structure" @property def description(self) -> str: """Description of the variant's behavior.""" return ("Creates directories based on h1 headings with content organized beneath them. " "This is the default structure for backward compatibility.") def explode( self, input_file: Path, options: ExplodeOptions ) -> ExplodeResult: """ Explode a markdown file using the flat structure variant. Args: input_file: Path to the markdown file to explode options: Options controlling the explode operation Returns: Result of the explode operation """ # Validate input validation_errors = self.validate_input_file(input_file) if validation_errors: return ExplodeResult( success=False, output_directory=options.output_dir or Path(), files_created=[], manifest_path=None, warnings=[], errors=validation_errors, variant_used=self.variant_type ) # Determine output directory if options.output_dir: output_dir = options.output_dir else: suffix = ".mdd" if options.create_manifest else "_exploded" output_dir = input_file.parent / f"{input_file.stem}{suffix}" # Create output directory creation_errors = self.create_output_directory(output_dir, overwrite=True) if creation_errors: return ExplodeResult( success=False, output_directory=output_dir, files_created=[], manifest_path=None, warnings=[], errors=creation_errors, variant_used=self.variant_type ) try: # Parse the markdown content content = input_file.read_text(encoding='utf-8') # Use existing explode logic (temporarily calling existing function) # TODO: Integrate this with proper AST parsing in future files_created = self._explode_using_current_logic( input_file, output_dir, content, options ) # Create manifest if requested manifest_path = None if options.create_manifest: structure = self._analyze_structure(content, output_dir) manifest_path = self.manifest_manager.create_manifest( output_dir=output_dir, original_file=input_file, variant=self.variant_type, structure=structure, preservation_options={ "front_matter": options.preserve_front_matter, "section_order": True, "heading_levels": True } ) files_created.append(manifest_path) return ExplodeResult( success=True, output_directory=output_dir, files_created=files_created, manifest_path=manifest_path, warnings=[], errors=[], variant_used=self.variant_type ) except Exception as e: return ExplodeResult( success=False, output_directory=output_dir, files_created=[], manifest_path=None, warnings=[], errors=[f"Error during explosion: {e}"], variant_used=self.variant_type ) 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 """ # Validate input validation_errors = self.validate_input_directory(input_directory) if validation_errors: return ImplodeResult( success=False, output_file=options.output_file or Path(), files_processed=[], variant_detected=self.variant_type, warnings=[], errors=validation_errors ) # Determine output file if options.output_file: output_file = options.output_file else: output_file = input_directory.parent / f"{input_directory.name}_imploded.md" try: # Read manifest if available manifest_data = self.manifest_manager.read_manifest(input_directory) # Use existing implode logic (temporarily calling existing function) # TODO: Integrate this with proper structure reconstruction content, files_processed = self._implode_using_current_logic( input_directory, manifest_data, options ) # Write output file if not options.dry_run: output_file.write_text(content, encoding='utf-8') return ImplodeResult( success=True, output_file=output_file, files_processed=files_processed, variant_detected=self.variant_type, warnings=[], errors=[] ) except Exception as e: return ImplodeResult( success=False, output_file=output_file, files_processed=[], variant_detected=self.variant_type, warnings=[], errors=[f"Error during implosion: {e}"] ) 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 """ if not directory.exists() or not directory.is_dir(): return False # Check for manifest indicating flat variant manifest_data = self.manifest_manager.read_manifest(directory) if manifest_data and manifest_data.explosion_type == "flat": return True # Check for flat structure patterns subdirs = [d for d in directory.iterdir() if d.is_dir()] # Look for typical flat patterns (no numbered prefixes, no semantic grouping) numbered_dirs = sum(1 for d in subdirs if re.match(r'^\d+_', d.name)) semantic_dirs = sum(1 for d in subdirs if any(name in d.name.lower() for name in ['parts', 'chapters', 'sections', 'appendices'])) # Flat structure has minimal numbered or semantic directories return (numbered_dirs / len(subdirs) if subdirs else 0) < 0.3 and \ (semantic_dirs / len(subdirs) if subdirs else 0) < 0.3 def get_detection_patterns(self) -> Dict[str, Any]: """ Get patterns used for auto-detecting this variant. Returns: Dictionary of detection patterns and weights """ return { "manifest_type": "flat", "numbered_directory_ratio": {"max": 0.3, "weight": 0.6}, "semantic_directory_ratio": {"max": 0.3, "weight": 0.5}, "index_file_count": {"min": 0, "weight": 0.3}, "fallback_score": 0.6 # Default choice } def _explode_using_current_logic( self, input_file: Path, output_dir: Path, content: str, options: ExplodeOptions ) -> List[Path]: """ Temporarily use existing explode logic until we integrate properly. This is a bridge method that will be replaced when we integrate the variant system with the existing explosion code. """ # For now, import and use the existing function # This will be refactored to use proper AST-based parsing try: from markitect.plugins.builtin.markdown_commands import explode_markdown_file result_dir = explode_markdown_file(input_file, output_dir) # Return list of created files files = list(output_dir.glob("**/*.md")) return files except ImportError: # Fallback basic implementation for testing return self._basic_explode_implementation(input_file, output_dir, content) def _implode_using_current_logic( self, input_directory: Path, manifest_data: Any, options: ImplodeOptions ) -> tuple[str, List[Path]]: """ Temporarily use existing implode logic until we integrate properly. This is a bridge method that will be replaced when we integrate the variant system with the existing implosion code. """ try: from markitect.plugins.builtin.markdown_commands import cli_implode_directory # Use existing implode logic result = cli_implode_directory( input_dir=input_directory, output_file=options.output_file or Path("/tmp/temp.md"), dry_run=True, # We handle file writing ourselves verbose=options.verbose, overwrite=options.overwrite, preserve_front_matter=options.preserve_front_matter, section_spacing=options.section_spacing ) if result.success: # Read the content that would have been written temp_file = options.output_file or Path("/tmp/temp.md") if temp_file.exists(): content = temp_file.read_text(encoding='utf-8') else: content = "# Imploded Content\n\n(Content generation in progress...)" files_processed = list(input_directory.glob("**/*.md")) return content, files_processed else: raise Exception(result.error_message or "Implosion failed") except ImportError: # Fallback basic implementation for testing return self._basic_implode_implementation(input_directory) def _basic_explode_implementation( self, input_file: Path, output_dir: Path, content: str ) -> List[Path]: """Basic explode implementation for testing purposes.""" files_created = [] # Simple h1-based splitting sections = re.split(r'\n# ', content) for i, section in enumerate(sections): if not section.strip(): continue if i == 0: # First section might not have leading # if not section.startswith('#'): section = '# ' + section else: # Add back the # that was removed by split section = '# ' + section # Extract title lines = section.split('\n') title_line = lines[0] title = re.sub(r'^#\s*', '', title_line).strip() # Create directory and file safe_title = re.sub(r'[^\w\s-]', '', title).strip() safe_title = re.sub(r'[-\s]+', '_', safe_title).lower() section_dir = output_dir / safe_title section_dir.mkdir(exist_ok=True) file_path = section_dir / "index.md" file_path.write_text(section, encoding='utf-8') files_created.append(file_path) return files_created def _basic_implode_implementation(self, input_directory: Path) -> tuple[str, List[Path]]: """Basic implode implementation for testing purposes.""" content_parts = [] files_processed = [] # Find all markdown files md_files = sorted(input_directory.glob("**/*.md")) for file_path in md_files: if file_path.name == "manifest.md": continue file_content = file_path.read_text(encoding='utf-8') content_parts.append(file_content) files_processed.append(file_path) # Join with appropriate spacing full_content = '\n\n\n\n'.join(content_parts) return full_content, files_processed def _analyze_structure(self, content: str, output_dir: Path) -> List[StructureEntry]: """Analyze the content structure for manifest generation.""" structure = [] lines = content.split('\n') order = 1 for i, line in enumerate(lines): # Check for headings heading_match = re.match(r'^(#{1,6})\s+(.+)', line) if heading_match: level = len(heading_match.group(1)) title = heading_match.group(2).strip() # Generate path based on title safe_title = re.sub(r'[^\w\s-]', '', title).strip() safe_title = re.sub(r'[-\s]+', '_', safe_title).lower() if level == 1: path = f"{safe_title}/index.md" else: path = f"{safe_title}.md" structure.append(StructureEntry( type=f"h{level}", title=title, path=path, order=order, level=level, original_line=i + 1 )) order += 1 return structure