From 98fe3361afc52088aa5204fea5a6e7baf1dbbcae Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 7 Oct 2025 12:47:59 +0200 Subject: [PATCH] feat: implement instant markdown base and publication directory - Issue #135 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete TDD8 implementation of publication directory support for md-render command: CORE FEATURES: • Publication directory management with ~/Notes/ default • MARKITECT_PUBLICATION_DIR environment variable override • Single file processing with --use-publication-dir flag • Directory processing with --dont-use-publication-dir flag • Recursive directory traversal with structure preservation • Automatic directory creation and path normalization IMPLEMENTATION DETAILS: • Extended md-render command with new CLI flags • Added 9 new helper functions for directory/file processing • Support for both single files and directory inputs • Comprehensive error handling and validation • Maintains backward compatibility CLI FLAGS ADDED: • --use-publication-dir: Force single files to use publication directory • --dont-use-publication-dir: Force directory processing to place HTML next to MD BEHAVIOR: • Single files: HTML next to MD by default, publication dir with flag • Directories: HTML in publication dir by default, next to MD with flag • Environment variable MARKITECT_PUBLICATION_DIR overrides default TESTING: • 18 comprehensive tests covering all functionality • Publication directory management (4 tests) • Single file processing (3 tests) • Directory processing (4 tests) • CLI integration (4 tests) • Edge cases (3 tests) • 100% test pass rate TDD8 Workflow: ISSUE→TEST→RED→GREEN→REFACTOR→DOCUMENT→REFINE→PUBLISH 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../plugins/builtin/markdown_commands.py | 328 +++++++++++---- tests/test_issue_135_publication_directory.py | 374 ++++++++++++++++++ 2 files changed, 629 insertions(+), 73 deletions(-) create mode 100644 tests/test_issue_135_publication_directory.py diff --git a/markitect/plugins/builtin/markdown_commands.py b/markitect/plugins/builtin/markdown_commands.py index 4af72626..ea68ae0b 100644 --- a/markitect/plugins/builtin/markdown_commands.py +++ b/markitect/plugins/builtin/markdown_commands.py @@ -7,6 +7,7 @@ replacing the legacy unprefixed commands for better namespace consistency. import click import json +import os import tempfile from pathlib import Path from typing import Dict, Any @@ -253,14 +254,16 @@ def md_list_command(ctx, output_format, names_only): @click.option('--editor-theme', type=click.Choice(['light', 'dark']), default='light', help='Editor interface theme (light or dark)') @click.option('--keyboard-shortcuts', is_flag=True, help='Enable keyboard shortcuts for editing actions') +@click.option('--use-publication-dir', is_flag=True, help='Force single files to use publication directory') +@click.option('--dont-use-publication-dir', is_flag=True, help='Force directory processing to place HTML next to MD files') @click.pass_context -def md_render_command(ctx, input_file, output, template, css, edit, editor_theme, keyboard_shortcuts): +def md_render_command(ctx, input_file, output, template, css, edit, editor_theme, keyboard_shortcuts, use_publication_dir, dont_use_publication_dir): """ Generate HTML with client-side JavaScript markdown rendering. - Creates a self-contained HTML file that includes the markdown content - as JavaScript data and renders it in the browser using client-side - markdown parsing with marked.js. + Creates self-contained HTML files that include markdown content as JavaScript data + and render in the browser using client-side markdown parsing with marked.js. + Supports both single files and directory processing. The generated HTML includes: • Embedded markdown content as JavaScript payload @@ -271,7 +274,17 @@ def md_render_command(ctx, input_file, output, template, css, edit, editor_theme • Optional instant editing capabilities with --edit flag • Graceful fallback if JavaScript fails - INPUT_FILE: Path to the markdown file to render + INPUT_FILE: Path to the markdown file or directory to render + + Publication Directory: + • Default publication directory: ~/Notes/ + • Override with MARKITECT_PUBLICATION_DIR environment variable + • Single files: HTML generated next to MD file by default + • Directories: HTML generated in publication directory with preserved structure + + Flags: + • --use-publication-dir: Force single files to use publication directory + • --dont-use-publication-dir: Force directory processing to place HTML next to MD files Available Templates: • basic (default) - Clean, minimal design with system fonts @@ -280,92 +293,157 @@ def md_render_command(ctx, input_file, output, template, css, edit, editor_theme • dark - GitHub dark mode inspired theme with dark background Examples: - # Basic usage with default template + # Single file - HTML next to MD file markitect md-render README.md - # Specify output file and template - markitect md-render README.md --output index.html --template github + # Single file - HTML in publication directory + markitect md-render README.md --use-publication-dir - # Dark theme for night reading - markitect md-render docs/guide.md --template dark + # Directory - HTML in publication directory with structure + markitect md-render docs/ - # Academic paper with custom styling - markitect md-render paper.md --template academic --css custom.css + # Directory - HTML next to each MD file + markitect md-render docs/ --dont-use-publication-dir - # Enable instant editing capabilities - markitect md-render README.md --edit + # Custom publication directory + MARKITECT_PUBLICATION_DIR=/tmp/pub markitect md-render docs/ - # Editing with dark editor theme and keyboard shortcuts - markitect md-render docs/guide.md --edit --editor-theme dark --keyboard-shortcuts - - # Front matter will be parsed and available to JavaScript - # Files with YAML front matter are fully supported + # Directory with custom template + markitect md-render docs/ --template github --edit """ config = ctx.obj or {} try: - if config.get('verbose', False): - click.echo(f"Rendering file: {input_file}") - - # Read markdown file input_path = Path(input_file) - markdown_content = input_path.read_text(encoding='utf-8') - # Extract front matter if present - front_matter = {} - if markdown_content.startswith('---\n'): - parts = markdown_content.split('---\n', 2) - if len(parts) >= 3: - try: - import yaml - front_matter = yaml.safe_load(parts[1]) or {} - markdown_content = parts[2] - except ImportError: - # Fallback without yaml parsing - pass + # Validate flags + if use_publication_dir and dont_use_publication_dir: + click.echo("Error: Cannot use both --use-publication-dir and --dont-use-publication-dir flags together", err=True) + raise click.Abort() - # Generate title from first heading or filename - title = front_matter.get('title', input_path.stem) - lines = markdown_content.strip().split('\n') - for line in lines: - if line.startswith('# '): - title = line[2:].strip() - break + # Get publication directory + publication_dir = get_publication_directory() - # Load custom CSS if provided - css_content = "" - if css: - css_path = Path(css) - css_content = css_path.read_text(encoding='utf-8') - - # Generate HTML with embedded markdown - html_content = generate_html_with_embedded_markdown( - markdown_content, title, template, css_content, front_matter, edit, editor_theme, keyboard_shortcuts - ) - - # Determine output path - if not output: - output = input_path.with_suffix('.html') - else: - output = Path(output) - - # Ensure output directory exists - output.parent.mkdir(parents=True, exist_ok=True) - - # Write HTML file - output.write_text(html_content, encoding='utf-8') - - click.echo(f"✓ HTML generated: {output}") if config.get('verbose', False): - click.echo(f" Template: {template}") - click.echo(f" Title: {title}") - if css: - click.echo(f" Custom CSS: {css}") + click.echo(f"Input: {input_path}") + click.echo(f"Publication directory: {publication_dir}") + + # Check if input is a directory or file + if input_path.is_dir(): + # Directory processing + use_pub_dir = not dont_use_publication_dir # Default to publication dir for directories + + if config.get('verbose', False): + click.echo(f"Processing directory: {input_path}") + click.echo(f"Use publication directory: {use_pub_dir}") + + # Find all markdown files + md_files = find_markdown_files(input_path) + + if not md_files: + click.echo(f"No markdown files found in directory: {input_path}") + return + + processed_count = 0 + for md_file in md_files: + try: + # Determine output path for this file + if use_pub_dir: + ensure_publication_directory(publication_dir) + output_path = get_relative_output_path(md_file, input_path, publication_dir) + # Ensure subdirectory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + else: + output_path = md_file.with_suffix('.html') + + # Process the markdown file + _render_single_markdown_file( + md_file, output_path, template, css, edit, editor_theme, + keyboard_shortcuts, config + ) + processed_count += 1 + + if config.get('verbose', False): + click.echo(f" ✓ {md_file} → {output_path}") + + except Exception as e: + click.echo(f" ✗ Error processing {md_file}: {e}", err=True) + + click.echo(f"✓ Processed {processed_count} markdown file(s)") + + else: + # Single file processing + use_pub_dir = use_publication_dir # Default to next to file for single files + + if config.get('verbose', False): + click.echo(f"Processing single file: {input_path}") + click.echo(f"Use publication directory: {use_pub_dir}") + + # Determine output path + if output: + output_path = Path(output) + elif use_pub_dir: + ensure_publication_directory(publication_dir) + output_path = publication_dir / get_output_filename(input_path) + else: + output_path = input_path.with_suffix('.html') + + # Process the single file + _render_single_markdown_file( + input_path, output_path, template, css, edit, editor_theme, + keyboard_shortcuts, config + ) + + click.echo(f"✓ HTML generated: {output_path}") except Exception as e: - click.echo(f"Error rendering file: {e}", err=True) + click.echo(f"Error: {e}", err=True) raise click.Abort() +def _render_single_markdown_file(input_path, output_path, template, css, edit, editor_theme, keyboard_shortcuts, config): + """Render a single markdown file to HTML.""" + # Read markdown file + markdown_content = input_path.read_text(encoding='utf-8') + + # Extract front matter if present + front_matter = {} + if markdown_content.startswith('---\n'): + parts = markdown_content.split('---\n', 2) + if len(parts) >= 3: + try: + import yaml + front_matter = yaml.safe_load(parts[1]) or {} + markdown_content = parts[2] + except ImportError: + # Fallback without yaml parsing + pass + + # Generate title from first heading or filename + title = front_matter.get('title', input_path.stem) + lines = markdown_content.strip().split('\n') + for line in lines: + if line.startswith('# '): + title = line[2:].strip() + break + + # Load custom CSS if provided + css_content = "" + if css: + css_path = Path(css) + css_content = css_path.read_text(encoding='utf-8') + + # Generate HTML with embedded markdown + html_content = generate_html_with_embedded_markdown( + markdown_content, title, template, css_content, front_matter, edit, editor_theme, keyboard_shortcuts + ) + + # Ensure output directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Write HTML file + output_path.write_text(html_content, encoding='utf-8') + + # Template definitions for cleaner code organization TEMPLATE_STYLES = { 'basic': { @@ -838,4 +916,108 @@ def generate_html_with_embedded_markdown(markdown_content, title, template, css_ markdown_json=json.dumps(markdown_content), front_matter_json=json.dumps(front_matter), **styles - ) \ No newline at end of file + ) + + +# Publication directory management functions for Issue #135 +def get_publication_directory(): + """Get the publication directory from environment variable or default.""" + pub_dir = os.environ.get('MARKITECT_PUBLICATION_DIR') + if pub_dir: + return normalize_publication_path(pub_dir) + return Path.home() / "Notes" + + +def normalize_publication_path(path_str): + """Normalize publication directory path with tilde expansion and absolute resolution.""" + path = Path(path_str) + if str(path).startswith('~'): + path = path.expanduser() + return path.resolve() + + +def ensure_publication_directory(pub_dir): + """Ensure publication directory exists, creating it if necessary.""" + pub_dir = Path(pub_dir) + pub_dir.mkdir(parents=True, exist_ok=True) + return pub_dir + + +def get_output_filename(input_file): + """Get HTML output filename from markdown input filename.""" + return input_file.stem + ".html" + + +def find_markdown_files(directory): + """Recursively find all markdown files in a directory.""" + directory = Path(directory) + md_files = [] + for pattern in ['*.md', '*.markdown']: + md_files.extend(directory.rglob(pattern)) + return sorted(md_files) + + +def get_relative_output_path(source_file, base_dir, output_dir): + """Calculate relative output path preserving directory structure.""" + source_file = Path(source_file) + base_dir = Path(base_dir) + output_dir = Path(output_dir) + + # Get relative path from base directory + relative_path = source_file.relative_to(base_dir) + + # Change extension to .html + relative_path = relative_path.with_suffix('.html') + + # Combine with output directory + return output_dir / relative_path + + +def process_single_file(input_file, use_publication_dir, publication_dir): + """Process a single markdown file, generate HTML, and return the output path.""" + input_file = Path(input_file) + + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + if use_publication_dir: + ensure_publication_directory(publication_dir) + output_file = publication_dir / get_output_filename(input_file) + else: + output_file = input_file.with_suffix('.html') + + # Actually generate the HTML file + _render_single_markdown_file( + input_file, output_file, 'basic', None, False, 'light', False, {} + ) + + return output_file + + +def process_directory(input_dir, use_publication_dir, publication_dir): + """Process all markdown files in a directory, generate HTML files, and return list of output paths.""" + input_dir = Path(input_dir) + + if not input_dir.exists() or not input_dir.is_dir(): + raise NotADirectoryError(f"Input directory not found: {input_dir}") + + md_files = find_markdown_files(input_dir) + output_files = [] + + for md_file in md_files: + if use_publication_dir: + ensure_publication_directory(publication_dir) + output_file = get_relative_output_path(md_file, input_dir, publication_dir) + # Ensure subdirectory exists + output_file.parent.mkdir(parents=True, exist_ok=True) + else: + output_file = md_file.with_suffix('.html') + + # Actually generate the HTML file + _render_single_markdown_file( + md_file, output_file, 'basic', None, False, 'light', False, {} + ) + + output_files.append(output_file) + + return output_files \ No newline at end of file diff --git a/tests/test_issue_135_publication_directory.py b/tests/test_issue_135_publication_directory.py new file mode 100644 index 00000000..f5c8a588 --- /dev/null +++ b/tests/test_issue_135_publication_directory.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +Test suite for Issue #135: Instant Markdown base and publication directory + +This test suite validates the publication directory functionality for the md-render command, +including directory processing, environment variable handling, and CLI flags. + +TDD8 Workflow: ISSUE→TEST→RED→GREEN→REFACTOR→DOCUMENT→REFINE→PUBLISH +State: RED (Tests should fail initially) +""" + +import pytest +import tempfile +import os +import shutil +from pathlib import Path +from unittest.mock import patch, MagicMock +import subprocess + + +class TestPublicationDirectoryManagement: + """Test publication directory configuration and management.""" + + def test_default_publication_directory_is_notes_home(self): + """Test that default publication directory is ~/Notes/.""" + from markitect.plugins.builtin.markdown_commands import get_publication_directory + + with patch.dict(os.environ, {}, clear=True): + # Remove any existing MARKITECT_PUBLICATION_DIR env var + if 'MARKITECT_PUBLICATION_DIR' in os.environ: + del os.environ['MARKITECT_PUBLICATION_DIR'] + + pub_dir = get_publication_directory() + expected = Path.home() / "Notes" + assert pub_dir == expected + + def test_environment_variable_overrides_default_publication_directory(self): + """Test that MARKITECT_PUBLICATION_DIR environment variable overrides default.""" + from markitect.plugins.builtin.markdown_commands import get_publication_directory + + custom_dir = "/tmp/custom_publication" + with patch.dict(os.environ, {'MARKITECT_PUBLICATION_DIR': custom_dir}): + pub_dir = get_publication_directory() + assert pub_dir == Path(custom_dir) + + def test_publication_directory_creation_when_nonexistent(self): + """Test that publication directory is created when it doesn't exist.""" + from markitect.plugins.builtin.markdown_commands import ensure_publication_directory + + with tempfile.TemporaryDirectory() as tmpdir: + pub_dir = Path(tmpdir) / "nonexistent" / "publication" + assert not pub_dir.exists() + + ensure_publication_directory(pub_dir) + assert pub_dir.exists() + assert pub_dir.is_dir() + + def test_publication_directory_path_normalization(self): + """Test that publication directory paths are properly normalized.""" + from markitect.plugins.builtin.markdown_commands import normalize_publication_path + + # Test tilde expansion + path_with_tilde = "~/Documents/Notes" + normalized = normalize_publication_path(path_with_tilde) + assert str(normalized).startswith(str(Path.home())) + + # Test relative path resolution + relative_path = "./publication" + normalized = normalize_publication_path(relative_path) + assert normalized.is_absolute() + + +class TestSingleFileProcessing: + """Test single markdown file processing behavior.""" + + def setup_method(self): + """Set up test environment with temporary files.""" + self.temp_dir = tempfile.mkdtemp() + self.test_md_file = Path(self.temp_dir) / "test.md" + self.test_md_file.write_text("# Test Document\n\nThis is a test.") + + # Set up publication directory + self.pub_dir = Path(self.temp_dir) / "publication" + self.pub_dir.mkdir(exist_ok=True) + + def teardown_method(self): + """Clean up test environment.""" + shutil.rmtree(self.temp_dir) + + def test_single_file_default_html_next_to_md(self): + """Test that by default, single file HTML is generated next to MD file.""" + from markitect.plugins.builtin.markdown_commands import process_single_file + + result = process_single_file( + input_file=self.test_md_file, + use_publication_dir=False, + publication_dir=self.pub_dir + ) + + expected_output = self.test_md_file.with_suffix('.html') + assert result == expected_output + # This will fail until implementation is added + assert expected_output.exists() + + def test_single_file_with_use_publication_dir_flag(self): + """Test single file with --use-publication-dir places HTML in publication directory.""" + from markitect.plugins.builtin.markdown_commands import process_single_file + + result = process_single_file( + input_file=self.test_md_file, + use_publication_dir=True, + publication_dir=self.pub_dir + ) + + expected_output = self.pub_dir / "test.html" + assert result == expected_output + # This will fail until implementation is added + assert expected_output.exists() + + def test_single_file_output_naming_conventions(self): + """Test that output file naming follows conventions.""" + from markitect.plugins.builtin.markdown_commands import get_output_filename + + # Test basic naming + input_file = Path("document.md") + output_name = get_output_filename(input_file) + assert output_name == "document.html" + + # Test with complex filename + input_file = Path("my-document_v2.md") + output_name = get_output_filename(input_file) + assert output_name == "my-document_v2.html" + + +class TestDirectoryProcessing: + """Test directory tree processing functionality.""" + + def setup_method(self): + """Set up test directory structure.""" + self.temp_dir = tempfile.mkdtemp() + self.base_dir = Path(self.temp_dir) / "source" + self.pub_dir = Path(self.temp_dir) / "publication" + + # Create test directory structure + self.base_dir.mkdir() + self.pub_dir.mkdir() + + # Create test markdown files + (self.base_dir / "root.md").write_text("# Root Document") + (self.base_dir / "subdir").mkdir() + (self.base_dir / "subdir" / "sub.md").write_text("# Sub Document") + (self.base_dir / "subdir" / "deep").mkdir() + (self.base_dir / "subdir" / "deep" / "deep.md").write_text("# Deep Document") + + # Create non-markdown files (should be ignored) + (self.base_dir / "readme.txt").write_text("Not markdown") + (self.base_dir / "image.png").write_bytes(b"fake image data") + + def teardown_method(self): + """Clean up test environment.""" + shutil.rmtree(self.temp_dir) + + def test_directory_default_html_in_publication_dir_with_structure(self): + """Test that directory processing puts HTML in publication directory with preserved structure.""" + from markitect.plugins.builtin.markdown_commands import process_directory + + results = process_directory( + input_dir=self.base_dir, + use_publication_dir=True, + publication_dir=self.pub_dir + ) + + # Check that all expected HTML files are in publication directory + expected_files = [ + self.pub_dir / "root.html", + self.pub_dir / "subdir" / "sub.html", + self.pub_dir / "subdir" / "deep" / "deep.html" + ] + + assert len(results) == 3 + for expected_file in expected_files: + assert expected_file in results + # This will fail until implementation is added + assert expected_file.exists() + + def test_directory_with_dont_use_publication_dir_html_next_to_md(self): + """Test directory processing with --dont-use-publication-dir places HTML next to MD files.""" + from markitect.plugins.builtin.markdown_commands import process_directory + + results = process_directory( + input_dir=self.base_dir, + use_publication_dir=False, + publication_dir=self.pub_dir + ) + + # Check that HTML files are next to their corresponding MD files + expected_files = [ + self.base_dir / "root.html", + self.base_dir / "subdir" / "sub.html", + self.base_dir / "subdir" / "deep" / "deep.html" + ] + + assert len(results) == 3 + for expected_file in expected_files: + assert expected_file in results + # This will fail until implementation is added + assert expected_file.exists() + + def test_directory_recursive_traversal(self): + """Test that directory processing recursively finds all markdown files.""" + from markitect.plugins.builtin.markdown_commands import find_markdown_files + + md_files = find_markdown_files(self.base_dir) + + expected_files = [ + self.base_dir / "root.md", + self.base_dir / "subdir" / "sub.md", + self.base_dir / "subdir" / "deep" / "deep.md" + ] + + assert len(md_files) == 3 + for expected_file in expected_files: + assert expected_file in md_files + + def test_directory_structure_preservation(self): + """Test that directory structure is preserved in publication directory.""" + from markitect.plugins.builtin.markdown_commands import get_relative_output_path + + # Test relative path calculation + source_file = self.base_dir / "subdir" / "deep" / "deep.md" + relative_path = get_relative_output_path(source_file, self.base_dir, self.pub_dir) + + expected_path = self.pub_dir / "subdir" / "deep" / "deep.html" + assert relative_path == expected_path + + +class TestCLIIntegration: + """Test CLI integration with new flags.""" + + def setup_method(self): + """Set up test environment.""" + self.temp_dir = tempfile.mkdtemp() + self.test_file = Path(self.temp_dir) / "test.md" + self.test_file.write_text("# Test Document") + + def teardown_method(self): + """Clean up test environment.""" + shutil.rmtree(self.temp_dir) + + def test_md_render_command_has_use_publication_dir_flag(self): + """Test that md-render command has --use-publication-dir flag.""" + result = subprocess.run( + ["markitect", "md-render", "--help"], + capture_output=True, + text=True + ) + + assert "--use-publication-dir" in result.stdout + assert "publication directory" in result.stdout.lower() + + def test_md_render_command_has_dont_use_publication_dir_flag(self): + """Test that md-render command has --dont-use-publication-dir flag.""" + result = subprocess.run( + ["markitect", "md-render", "--help"], + capture_output=True, + text=True + ) + + assert "--dont-use-publication-dir" in result.stdout + + def test_md_render_with_directory_input(self): + """Test that md-render command accepts directory as input.""" + # Create a test directory with markdown files + test_dir = Path(self.temp_dir) / "test_dir" + test_dir.mkdir() + (test_dir / "test.md").write_text("# Test") + + # This should not fail with directory input + result = subprocess.run( + ["markitect", "md-render", str(test_dir)], + capture_output=True, + text=True, + timeout=30 + ) + + # Should not error on directory input + assert result.returncode == 0 or "directory" in result.stderr + + def test_environment_variable_integration(self): + """Test that MARKITECT_PUBLICATION_DIR environment variable is respected.""" + pub_dir = Path(self.temp_dir) / "custom_pub" + pub_dir.mkdir() + + env = os.environ.copy() + env['MARKITECT_PUBLICATION_DIR'] = str(pub_dir) + + result = subprocess.run( + ["markitect", "md-render", str(self.test_file), "--use-publication-dir"], + capture_output=True, + text=True, + env=env, + timeout=30 + ) + + # Should succeed or at least recognize the flag + # This will fail until implementation is complete + assert result.returncode == 0 + + +class TestEdgeCases: + """Test edge cases and error conditions.""" + + def setup_method(self): + """Set up test environment.""" + self.temp_dir = tempfile.mkdtemp() + + def teardown_method(self): + """Clean up test environment.""" + shutil.rmtree(self.temp_dir) + + def test_empty_directory_processing(self): + """Test processing of empty directory.""" + from markitect.plugins.builtin.markdown_commands import process_directory + + empty_dir = Path(self.temp_dir) / "empty" + empty_dir.mkdir() + pub_dir = Path(self.temp_dir) / "pub" + pub_dir.mkdir() + + results = process_directory( + input_dir=empty_dir, + use_publication_dir=True, + publication_dir=pub_dir + ) + + assert results == [] + + def test_mixed_content_directory_processing(self): + """Test directory with mixed content (markdown and other files).""" + from markitect.plugins.builtin.markdown_commands import find_markdown_files + + mixed_dir = Path(self.temp_dir) / "mixed" + mixed_dir.mkdir() + + # Create various file types + (mixed_dir / "document.md").write_text("# Document") + (mixed_dir / "readme.txt").write_text("Text file") + (mixed_dir / "image.jpg").write_bytes(b"fake image") + (mixed_dir / "script.py").write_text("print('hello')") + (mixed_dir / "another.md").write_text("# Another") + + md_files = find_markdown_files(mixed_dir) + + # Should only find markdown files + assert len(md_files) == 2 + assert all(f.suffix == '.md' for f in md_files) + + def test_nonexistent_input_error_handling(self): + """Test error handling for nonexistent input files/directories.""" + from markitect.plugins.builtin.markdown_commands import process_single_file + + nonexistent_file = Path(self.temp_dir) / "nonexistent.md" + pub_dir = Path(self.temp_dir) / "pub" + + with pytest.raises(FileNotFoundError): + process_single_file( + input_file=nonexistent_file, + use_publication_dir=False, + publication_dir=pub_dir + ) + + +if __name__ == '__main__': + pytest.main([__file__, "-v"]) \ No newline at end of file