feat: implement instant markdown base and publication directory - Issue #135
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# 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
|
||||
374
tests/test_issue_135_publication_directory.py
Normal file
374
tests/test_issue_135_publication_directory.py
Normal file
@@ -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"])
|
||||
Reference in New Issue
Block a user