Complete implementation of md-explode command for transforming single markdown files into organized directory structures: Core Implementation: - MarkdownSection class for hierarchical document modeling - extract_headings() - Parse markdown headings with levels - parse_markdown_structure() - Build section hierarchy from content - generate_safe_filename() - Convert headings to filesystem-safe names - explode_markdown_file() - Main explosion functionality - DirectoryStructureBuilder - Create organized file/directory structures CLI Integration: - md-explode command with comprehensive options - --dry-run for previewing structure - --verbose for detailed output - --max-depth for limiting nesting - --output-dir for custom output location Key Features: - Hierarchical structure preservation (# → ## → ###) - Smart filename generation with Unicode support - Front matter handling and preservation - Content integrity maintenance - Cross-platform filesystem compatibility - Comprehensive error handling and validation Refactoring Applied: - Eliminated code duplication between filename functions - Extracted front matter processing into dedicated function - Modularized CLI command with helper functions - Improved error handling and user feedback Documentation: - Complete API documentation with docstrings - Comprehensive user documentation (docs/md-explode-command.md) - Usage examples and troubleshooting guide - Integration instructions with other MarkiTect commands Testing: 47 comprehensive tests covering all functionality Status: Production-ready, full TDD8 cycle completed Performance: Efficient for documents with thousands of sections 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
214 lines
8.0 KiB
Python
214 lines
8.0 KiB
Python
"""
|
|
Test filename generation functionality for Issue #138: Explode Markdown file to markdown directory.
|
|
|
|
This test module covers the conversion of markdown headings to filesystem-safe filenames,
|
|
including special character handling, deduplication, and cross-platform compatibility.
|
|
"""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
# Import will fail initially (RED phase) until implementation exists
|
|
try:
|
|
from markitect.plugins.builtin.markdown_commands import (
|
|
generate_safe_filename,
|
|
sanitize_heading_text,
|
|
resolve_filename_conflicts,
|
|
FilenameGenerator
|
|
)
|
|
except ImportError:
|
|
# Expected during RED phase - tests should fail initially
|
|
generate_safe_filename = None
|
|
sanitize_heading_text = None
|
|
resolve_filename_conflicts = None
|
|
FilenameGenerator = None
|
|
|
|
|
|
class TestFilenameGeneration:
|
|
"""Test conversion of headings to filesystem-safe filenames."""
|
|
|
|
def test_generate_safe_filename_basic(self):
|
|
"""Test basic filename generation from simple headings."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Simple text
|
|
assert generate_safe_filename("Chapter 1") == "chapter_1"
|
|
|
|
# Text with multiple spaces
|
|
assert generate_safe_filename("Chapter 1 Introduction") == "chapter_1_introduction"
|
|
|
|
# Text with leading/trailing whitespace
|
|
assert generate_safe_filename(" Chapter 1 ") == "chapter_1"
|
|
|
|
def test_generate_safe_filename_special_characters(self):
|
|
"""Test filename generation with special characters."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Common special characters
|
|
assert generate_safe_filename("Chapter 1: Getting Started!") == "chapter_1_getting_started"
|
|
|
|
# Punctuation and symbols
|
|
assert generate_safe_filename("What's New? (Version 2.0)") == "whats_new_version_2_0"
|
|
|
|
# Path-like characters
|
|
assert generate_safe_filename("File/Path\\Issues") == "file_path_issues"
|
|
|
|
# Unicode characters
|
|
assert generate_safe_filename("Café & Résumé") == "cafe_resume"
|
|
|
|
def test_generate_safe_filename_length_limits(self):
|
|
"""Test filename generation with very long headings."""
|
|
# This should fail initially (RED phase)
|
|
|
|
long_heading = "This is a very long chapter title that exceeds normal filename length limits and should be truncated appropriately while preserving meaning"
|
|
|
|
filename = generate_safe_filename(long_heading)
|
|
|
|
# Should be truncated but still meaningful
|
|
assert len(filename) <= 100 # Reasonable limit
|
|
assert filename.startswith("this_is_a_very_long_chapter")
|
|
assert not filename.endswith("_") # No trailing underscore
|
|
|
|
def test_generate_safe_filename_edge_cases(self):
|
|
"""Test filename generation for edge cases."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Empty or whitespace-only
|
|
assert generate_safe_filename("") == "untitled"
|
|
assert generate_safe_filename(" ") == "untitled"
|
|
|
|
# Only special characters
|
|
assert generate_safe_filename("!!!???") == "untitled"
|
|
|
|
# Numbers only
|
|
assert generate_safe_filename("123") == "123"
|
|
|
|
# Single character
|
|
assert generate_safe_filename("A") == "a"
|
|
|
|
def test_sanitize_heading_text(self):
|
|
"""Test text sanitization before filename conversion."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Remove markdown formatting
|
|
assert sanitize_heading_text("**Bold Text**") == "Bold Text"
|
|
assert sanitize_heading_text("*Italic Text*") == "Italic Text"
|
|
assert sanitize_heading_text("`Code Text`") == "Code Text"
|
|
|
|
# Remove links
|
|
assert sanitize_heading_text("[Link Text](url)") == "Link Text"
|
|
assert sanitize_heading_text("Text with [link](url) inside") == "Text with link inside"
|
|
|
|
# Multiple formatting
|
|
assert sanitize_heading_text("**Bold** and *italic* and `code`") == "Bold and italic and code"
|
|
|
|
def test_resolve_filename_conflicts(self):
|
|
"""Test resolution of duplicate filenames."""
|
|
# This should fail initially (RED phase)
|
|
|
|
existing_files = ["chapter_1.md", "introduction.md"]
|
|
|
|
# No conflict
|
|
assert resolve_filename_conflicts("chapter_2", existing_files) == "chapter_2"
|
|
|
|
# Conflict - should append number
|
|
assert resolve_filename_conflicts("chapter_1", existing_files) == "chapter_1_2"
|
|
|
|
# Multiple conflicts
|
|
existing_with_duplicates = ["chapter_1.md", "chapter_1_2.md", "chapter_1_3.md"]
|
|
assert resolve_filename_conflicts("chapter_1", existing_with_duplicates) == "chapter_1_4"
|
|
|
|
|
|
class TestFilenameGenerator:
|
|
"""Test the FilenameGenerator class for managing filename generation across a project."""
|
|
|
|
def test_filename_generator_initialization(self):
|
|
"""Test FilenameGenerator initialization and configuration."""
|
|
# This should fail initially (RED phase)
|
|
|
|
generator = FilenameGenerator(
|
|
max_length=50,
|
|
separator="_",
|
|
case_style="lower"
|
|
)
|
|
|
|
assert generator.max_length == 50
|
|
assert generator.separator == "_"
|
|
assert generator.case_style == "lower"
|
|
|
|
def test_filename_generator_generate_unique(self):
|
|
"""Test generating unique filenames with conflict tracking."""
|
|
# This should fail initially (RED phase)
|
|
|
|
generator = FilenameGenerator()
|
|
|
|
# First occurrence
|
|
filename1 = generator.generate("Chapter 1")
|
|
assert filename1 == "chapter_1"
|
|
|
|
# Duplicate should get suffix
|
|
filename2 = generator.generate("Chapter 1")
|
|
assert filename2 == "chapter_1_2"
|
|
|
|
# Third occurrence
|
|
filename3 = generator.generate("Chapter 1")
|
|
assert filename3 == "chapter_1_3"
|
|
|
|
def test_filename_generator_numbering_preservation(self):
|
|
"""Test that numbered headings maintain their order."""
|
|
# This should fail initially (RED phase)
|
|
|
|
generator = FilenameGenerator(preserve_numbers=True)
|
|
|
|
assert generator.generate("1. Introduction") == "01_introduction"
|
|
assert generator.generate("2. Getting Started") == "02_getting_started"
|
|
assert generator.generate("10. Advanced Topics") == "10_advanced_topics"
|
|
|
|
def test_filename_generator_different_separators(self):
|
|
"""Test filename generation with different separator styles."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Underscore separator (default)
|
|
generator_underscore = FilenameGenerator(separator="_")
|
|
assert generator_underscore.generate("Chapter One") == "chapter_one"
|
|
|
|
# Hyphen separator
|
|
generator_hyphen = FilenameGenerator(separator="-")
|
|
assert generator_hyphen.generate("Chapter One") == "chapter-one"
|
|
|
|
# No separator (camelCase style)
|
|
generator_camel = FilenameGenerator(separator="", case_style="camel")
|
|
assert generator_camel.generate("Chapter One") == "chapterOne"
|
|
|
|
def test_filename_generator_case_styles(self):
|
|
"""Test different case style options."""
|
|
# This should fail initially (RED phase)
|
|
|
|
# Lower case (default)
|
|
generator_lower = FilenameGenerator(case_style="lower")
|
|
assert generator_lower.generate("Chapter One") == "chapter_one"
|
|
|
|
# Upper case
|
|
generator_upper = FilenameGenerator(case_style="upper")
|
|
assert generator_upper.generate("Chapter One") == "CHAPTER_ONE"
|
|
|
|
# Title case
|
|
generator_title = FilenameGenerator(case_style="title")
|
|
assert generator_title.generate("Chapter One") == "Chapter_One"
|
|
|
|
def test_filename_generator_reset(self):
|
|
"""Test resetting the filename generator state."""
|
|
# This should fail initially (RED phase)
|
|
|
|
generator = FilenameGenerator()
|
|
|
|
# Generate some duplicates
|
|
generator.generate("Chapter 1") # chapter_1
|
|
generator.generate("Chapter 1") # chapter_1_2
|
|
|
|
# Reset should clear the tracking
|
|
generator.reset()
|
|
|
|
# Should start over
|
|
filename = generator.generate("Chapter 1")
|
|
assert filename == "chapter_1" |