feat: implement comprehensive asset shipping for md-render command
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Add automatic asset copying when rendering markdown to different output directories with intelligent defaults and full user control. Key Features: - Environment variable support: MARKITECT_OUTPUT_DIR sets default output directory - Smart defaults: auto-ship assets for directory output, disabled for file output - CLI control flags: --ship-assets and --no-ship-assets for explicit control - Timestamp-based copying: only copies when source newer than destination - Path preservation: maintains relative directory structure in output - Graceful error handling: missing assets logged as warnings, not failures Technical Implementation: - Enhanced asset discovery in markitect/assets/discovery.py with discover_assets_from_markdown() - Added environment variable priority: CLI --output > MARKITECT_OUTPUT_DIR > input directory - Comprehensive asset shipping logic with _ship_assets() function - Directory vs file output detection for intelligent default behavior Examples and Testing: - Added image-assets example directory with 6 sample images and comprehensive README - Created comprehensive TDD test suite with 10 tests covering all functionality - Tests validate environment variables, CLI flags, asset discovery, shipping logic, timestamp handling, missing assets, path preservation, and default behaviors Usage: markitect md-render file.md -o /output/dir/ # Auto-ships assets markitect md-render file.md --no-ship-assets # Suppresses shipping MARKITECT_OUTPUT_DIR=/docs markitect md-render file.md # Uses env var 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
TODO.md
19
TODO.md
@@ -29,6 +29,25 @@ This section is for tasks currently being discussed with or worked on by the cod
|
|||||||
|
|
||||||
## Completed Tasks
|
## Completed Tasks
|
||||||
|
|
||||||
|
**Asset Shipping for md-render - COMPLETED ✅**:
|
||||||
|
- ✅ Implemented automatic asset copying when rendering markdown to different output directories
|
||||||
|
- ✅ Added asset discovery functionality parsing markdown for image/link references
|
||||||
|
- ✅ Implemented timestamp-based asset copying (only copy if source newer than destination)
|
||||||
|
- ✅ Added `--ship-assets` and `--no-ship-assets` CLI flags for explicit control
|
||||||
|
- ✅ Added `MARKITECT_OUTPUT_DIR` environment variable support for default output directory
|
||||||
|
- ✅ Smart defaults: assets ship automatically when output is directory, disabled for specific files
|
||||||
|
- ✅ Preserved relative path structure in output directory maintaining markdown link compatibility
|
||||||
|
- ✅ Graceful handling of missing assets with warning messages
|
||||||
|
- ✅ Full backward compatibility with existing md-render workflows
|
||||||
|
- ✅ Comprehensive TDD test suite covering all functionality and edge cases
|
||||||
|
|
||||||
|
**Feature Capabilities**:
|
||||||
|
- Environment variable priority: CLI `--output` > `MARKITECT_OUTPUT_DIR` > input file directory
|
||||||
|
- Automatic asset discovery from standard markdown syntax: `` and `[text](path)`
|
||||||
|
- Timestamp-based incremental copying prevents unnecessary file operations
|
||||||
|
- Directory structure preservation maintains working relative links in output HTML
|
||||||
|
- Support for images, documents, and other asset types referenced in markdown
|
||||||
|
|
||||||
**CHANGELOG.md Enhancement - COMPLETED ✅**:
|
**CHANGELOG.md Enhancement - COMPLETED ✅**:
|
||||||
- ✅ Added missing version entries for 0.1.0, 0.2.0, and 0.3.0
|
- ✅ Added missing version entries for 0.1.0, 0.2.0, and 0.3.0
|
||||||
- ✅ Added standard Keep a Changelog header with proper format
|
- ✅ Added standard Keep a Changelog header with proper format
|
||||||
|
|||||||
16
examples/image-assets/README.txt
Normal file
16
examples/image-assets/README.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Image Asset Management Examples
|
||||||
|
|
||||||
|
This directory contains examples demonstrating MarkiTect's image asset management
|
||||||
|
capabilities:
|
||||||
|
|
||||||
|
- project_documentation.md: Sample project documentation with embedded images
|
||||||
|
showing how MarkiTect handles image assets in markdown documents
|
||||||
|
- images/: Directory containing sample images used in the documentation examples
|
||||||
|
|
||||||
|
These examples showcase:
|
||||||
|
- Image embedding in markdown documents
|
||||||
|
- Asset deduplication and content-addressable storage
|
||||||
|
- Relative path handling for images in MarkiTect projects
|
||||||
|
- Best practices for organizing image assets in documentation
|
||||||
|
|
||||||
|
--worsch, 25-10-29
|
||||||
BIN
examples/image-assets/images/architecture_diagram.png
Normal file
BIN
examples/image-assets/images/architecture_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
examples/image-assets/images/company_logo.png
Normal file
BIN
examples/image-assets/images/company_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
examples/image-assets/images/dashboard_screenshot.png
Normal file
BIN
examples/image-assets/images/dashboard_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
examples/image-assets/images/performance_chart.png
Normal file
BIN
examples/image-assets/images/performance_chart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
examples/image-assets/images/project_icon.png
Normal file
BIN
examples/image-assets/images/project_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 458 B |
BIN
examples/image-assets/images/settings_panel.png
Normal file
BIN
examples/image-assets/images/settings_panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
71
examples/image-assets/project_documentation.md
Normal file
71
examples/image-assets/project_documentation.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Project Documentation Example
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document demonstrates MarkiTect's image asset management capabilities by embedding various types of images commonly used in technical documentation.
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
The following diagram shows the overall system architecture:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Figure 1: High-level system architecture showing component interactions*
|
||||||
|
|
||||||
|
## User Interface Screenshots
|
||||||
|
|
||||||
|
### Dashboard View
|
||||||
|
|
||||||
|
The main dashboard provides an overview of system status:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Figure 2: Main dashboard interface with key metrics and navigation*
|
||||||
|
|
||||||
|
### Settings Panel
|
||||||
|
|
||||||
|
Users can configure system behavior through the settings panel:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Figure 3: Configuration interface for system preferences*
|
||||||
|
|
||||||
|
## Logo and Branding
|
||||||
|
|
||||||
|
### Company Logo
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Project Icon
|
||||||
|
|
||||||
|
The project uses this icon throughout the interface:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Asset Management Features
|
||||||
|
|
||||||
|
MarkiTect provides several key features for managing image assets:
|
||||||
|
|
||||||
|
1. **Content-Addressable Storage**: Images are stored using SHA-256 hashes to prevent duplication
|
||||||
|
2. **Automatic Deduplication**: Identical images are only stored once, regardless of filename
|
||||||
|
3. **Relative Path Resolution**: Images can be referenced using relative paths from the markdown file
|
||||||
|
4. **Asset Tracking**: All referenced assets are tracked and validated during document processing
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
The following chart shows system performance over time:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Figure 4: System performance metrics showing response time and throughput*
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This example demonstrates how MarkiTect seamlessly handles multiple image assets within a single document, providing:
|
||||||
|
|
||||||
|
- Efficient storage through deduplication
|
||||||
|
- Reliable asset resolution
|
||||||
|
- Clean integration with markdown syntax
|
||||||
|
- Support for various image formats (PNG, JPG, SVG, etc.)
|
||||||
|
|
||||||
|
All images in this document will be processed through MarkiTect's asset management system when the document is rendered or packaged.
|
||||||
@@ -223,6 +223,45 @@ class MarkdownScanner:
|
|||||||
return len(lines)
|
return len(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def discover_assets_from_markdown(markdown_content: str, base_path: Path) -> List[AssetReference]:
|
||||||
|
"""
|
||||||
|
Simple function to discover assets from markdown content for md-render.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markdown_content: The markdown content to scan
|
||||||
|
base_path: Base path for resolving relative asset paths
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of AssetReference objects found in the markdown
|
||||||
|
"""
|
||||||
|
scanner = MarkdownScanner()
|
||||||
|
|
||||||
|
# Create a temporary file to use the existing scan_file method
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as temp_file:
|
||||||
|
temp_file.write(markdown_content)
|
||||||
|
temp_path = Path(temp_file.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
references = scanner.scan_file(temp_path)
|
||||||
|
# Update the source_file to the actual base_path for relative resolution
|
||||||
|
for ref in references:
|
||||||
|
ref.source_file = base_path
|
||||||
|
# Resolve the asset path relative to base_path
|
||||||
|
if not ref.asset_path.startswith(('http:', 'https:', 'mailto:', 'data:')):
|
||||||
|
# Clean up relative path indicators
|
||||||
|
clean_path = ref.asset_path.lstrip('./')
|
||||||
|
resolved_path = base_path / clean_path
|
||||||
|
if resolved_path.exists():
|
||||||
|
ref.resolved_path = resolved_path
|
||||||
|
else:
|
||||||
|
ref.is_broken = True
|
||||||
|
return references
|
||||||
|
finally:
|
||||||
|
# Clean up temporary file
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class AssetDiscoveryEngine:
|
class AssetDiscoveryEngine:
|
||||||
"""Main engine for asset discovery and analysis."""
|
"""Main engine for asset discovery and analysis."""
|
||||||
|
|
||||||
|
|||||||
@@ -1974,9 +1974,14 @@ def md_list_command(ctx, output_format, names_only):
|
|||||||
help='Don\'t use publication directory for output')
|
help='Don\'t use publication directory for output')
|
||||||
@click.option('--nodogtag', is_flag=True,
|
@click.option('--nodogtag', is_flag=True,
|
||||||
help='Don\'t add HTML generation dogtag at end of document')
|
help='Don\'t add HTML generation dogtag at end of document')
|
||||||
|
@click.option('--ship-assets', is_flag=True, default=None,
|
||||||
|
help='Copy referenced assets to output directory')
|
||||||
|
@click.option('--no-ship-assets', is_flag=True,
|
||||||
|
help='Don\'t copy referenced assets to output directory')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_theme,
|
def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_theme,
|
||||||
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag):
|
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag,
|
||||||
|
ship_assets, no_ship_assets):
|
||||||
"""
|
"""
|
||||||
Render a markdown file to HTML with basic templates and live preview capabilities.
|
Render a markdown file to HTML with basic templates and live preview capabilities.
|
||||||
|
|
||||||
@@ -2008,17 +2013,61 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
|||||||
if edit and insert:
|
if edit and insert:
|
||||||
raise click.BadParameter("Cannot use both --edit and --insert flags simultaneously. Choose one mode.")
|
raise click.BadParameter("Cannot use both --edit and --insert flags simultaneously. Choose one mode.")
|
||||||
|
|
||||||
# Determine output path
|
# Validate asset shipping flags
|
||||||
|
if ship_assets and no_ship_assets:
|
||||||
|
raise click.BadParameter("Cannot use both --ship-assets and --no-ship-assets flags simultaneously.")
|
||||||
|
|
||||||
|
# Determine output path with environment variable support
|
||||||
if output:
|
if output:
|
||||||
output_path = Path(output)
|
output_path = Path(output)
|
||||||
|
# If output is a directory, use canonical filename within that directory
|
||||||
|
if output_path.is_dir() or (not output_path.suffix and not output_path.exists()):
|
||||||
|
# Ensure the directory exists
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Use canonical filename (input name + .html) in the specified directory
|
||||||
|
canonical_filename = input_path.with_suffix('.html').name
|
||||||
|
output_path = output_path / canonical_filename
|
||||||
|
output_is_directory = True
|
||||||
|
else:
|
||||||
|
output_is_directory = False
|
||||||
else:
|
else:
|
||||||
output_path = input_path.with_suffix('.html')
|
# Check for environment variable
|
||||||
|
import os
|
||||||
|
env_output_dir = os.environ.get('MARKITECT_OUTPUT_DIR')
|
||||||
|
if env_output_dir:
|
||||||
|
output_path = Path(env_output_dir)
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
canonical_filename = input_path.with_suffix('.html').name
|
||||||
|
output_path = output_path / canonical_filename
|
||||||
|
output_is_directory = True
|
||||||
|
else:
|
||||||
|
output_path = input_path.with_suffix('.html')
|
||||||
|
output_is_directory = False
|
||||||
|
|
||||||
# Use publication directory if specified
|
# Use publication directory if specified
|
||||||
if use_publication_dir and not dont_use_publication_dir:
|
if use_publication_dir and not dont_use_publication_dir:
|
||||||
pub_dir = get_publication_directory()
|
pub_dir = get_publication_directory()
|
||||||
ensure_publication_directory(pub_dir)
|
ensure_publication_directory(pub_dir)
|
||||||
output_path = pub_dir / get_output_filename(input_path)
|
output_path = pub_dir / get_output_filename(input_path)
|
||||||
|
output_is_directory = True # Publication dir is always a directory output
|
||||||
|
|
||||||
|
# Determine if we should ship assets
|
||||||
|
should_ship_assets = False
|
||||||
|
if no_ship_assets:
|
||||||
|
should_ship_assets = False
|
||||||
|
elif ship_assets:
|
||||||
|
should_ship_assets = True
|
||||||
|
elif output_is_directory:
|
||||||
|
# Default: ship assets when output is a directory
|
||||||
|
should_ship_assets = True
|
||||||
|
|
||||||
|
|
||||||
|
# Discover and ship assets if needed
|
||||||
|
if should_ship_assets:
|
||||||
|
if output_is_directory:
|
||||||
|
# For directory output, ship to the same directory as the HTML file
|
||||||
|
_ship_assets(input_path, output_path.parent, config.get('verbose', False))
|
||||||
|
# For file output, we don't ship assets (shouldn't reach here anyway)
|
||||||
|
|
||||||
# Initialize clean document manager
|
# Initialize clean document manager
|
||||||
from markitect.clean_document_manager import CleanDocumentManager
|
from markitect.clean_document_manager import CleanDocumentManager
|
||||||
@@ -3433,3 +3482,76 @@ class FilenameDecoder:
|
|||||||
return [self.decode(filename) for filename in filenames]
|
return [self.decode(filename) for filename in filenames]
|
||||||
|
|
||||||
|
|
||||||
|
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
|
||||||
|
"""
|
||||||
|
Ship (copy) assets referenced in markdown file to output directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path: Path to the markdown file
|
||||||
|
output_dir: Directory where assets should be copied
|
||||||
|
verbose: Whether to print verbose output
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
from markitect.assets.discovery import discover_assets_from_markdown
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read the markdown content
|
||||||
|
markdown_content = input_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
# Discover assets
|
||||||
|
base_path = input_path.parent
|
||||||
|
assets = discover_assets_from_markdown(markdown_content, base_path)
|
||||||
|
|
||||||
|
shipped_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
missing_count = 0
|
||||||
|
|
||||||
|
for asset_ref in assets:
|
||||||
|
# Skip URLs and broken assets
|
||||||
|
if asset_ref.asset_path.startswith(('http:', 'https:', 'mailto:', 'data:')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if asset_ref.is_broken or not asset_ref.resolved_path:
|
||||||
|
missing_count += 1
|
||||||
|
if verbose:
|
||||||
|
click.echo(f" ⚠ Missing asset: {asset_ref.asset_path}", err=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Determine output path (preserve relative directory structure)
|
||||||
|
clean_path = asset_ref.asset_path.lstrip('./')
|
||||||
|
dest_path = output_dir / clean_path
|
||||||
|
|
||||||
|
# Create destination directory
|
||||||
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Check if we need to copy (timestamp-based)
|
||||||
|
should_copy = True
|
||||||
|
if dest_path.exists():
|
||||||
|
source_mtime = asset_ref.resolved_path.stat().st_mtime
|
||||||
|
dest_mtime = dest_path.stat().st_mtime
|
||||||
|
if source_mtime <= dest_mtime:
|
||||||
|
should_copy = False
|
||||||
|
skipped_count += 1
|
||||||
|
|
||||||
|
if should_copy:
|
||||||
|
shutil.copy2(asset_ref.resolved_path, dest_path)
|
||||||
|
shipped_count += 1
|
||||||
|
if verbose:
|
||||||
|
click.echo(f" ✓ Copied: {asset_ref.asset_path}")
|
||||||
|
elif verbose:
|
||||||
|
click.echo(f" → Skipped (up-to-date): {asset_ref.asset_path}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
if verbose or shipped_count > 0:
|
||||||
|
if shipped_count > 0:
|
||||||
|
click.echo(f"✓ Shipped {shipped_count} assets")
|
||||||
|
if skipped_count > 0:
|
||||||
|
click.echo(f" → Skipped {skipped_count} up-to-date assets")
|
||||||
|
if missing_count > 0:
|
||||||
|
click.echo(f" ⚠ {missing_count} assets not found", err=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
click.echo(f"Error shipping assets: {e}", err=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
240
tests/test_md_render_asset_shipping.py
Normal file
240
tests/test_md_render_asset_shipping.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TDD tests for asset shipping in md-render command.
|
||||||
|
|
||||||
|
Tests the automatic copying of referenced assets when rendering markdown
|
||||||
|
to different output directories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from markitect.plugins.builtin.markdown_commands import md_render_command
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssetShippingMdRender:
|
||||||
|
"""Test asset shipping functionality in md-render."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Set up test environment."""
|
||||||
|
self.runner = CliRunner()
|
||||||
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
|
self.test_dir = Path(self.temp_dir)
|
||||||
|
|
||||||
|
# Create test markdown with image references
|
||||||
|
self.markdown_content = """# Test Document
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[Documentation](docs/readme.md)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create test file structure
|
||||||
|
self.md_file = self.test_dir / "test.md"
|
||||||
|
self.md_file.write_text(self.markdown_content)
|
||||||
|
|
||||||
|
# Create asset directories and files
|
||||||
|
(self.test_dir / "images").mkdir()
|
||||||
|
(self.test_dir / "assets").mkdir()
|
||||||
|
(self.test_dir / "diagrams").mkdir()
|
||||||
|
(self.test_dir / "docs").mkdir()
|
||||||
|
|
||||||
|
# Create sample asset files
|
||||||
|
(self.test_dir / "images" / "arch.png").write_bytes(b"fake png data")
|
||||||
|
(self.test_dir / "assets" / "logo.jpg").write_bytes(b"fake jpg data")
|
||||||
|
(self.test_dir / "diagrams" / "flow.svg").write_text("<svg>fake svg</svg>")
|
||||||
|
(self.test_dir / "docs" / "readme.md").write_text("# README")
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""Clean up test environment."""
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_environment_variable_output_directory(self):
|
||||||
|
"""Test that MARKITECT_OUTPUT_DIR is used when no --output is specified."""
|
||||||
|
output_dir = self.test_dir / "env_output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(output_dir)}):
|
||||||
|
result = self.runner.invoke(md_render_command, [str(self.md_file)])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_dir / "test.html").exists()
|
||||||
|
|
||||||
|
def test_cli_output_overrides_environment_variable(self):
|
||||||
|
"""Test that CLI --output parameter overrides environment variable."""
|
||||||
|
env_output = self.test_dir / "env_output"
|
||||||
|
cli_output = self.test_dir / "cli_output"
|
||||||
|
env_output.mkdir()
|
||||||
|
cli_output.mkdir()
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'MARKITECT_OUTPUT_DIR': str(env_output)}):
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(cli_output)
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (cli_output / "test.html").exists()
|
||||||
|
assert not (env_output / "test.html").exists()
|
||||||
|
|
||||||
|
def test_asset_shipping_enabled_by_default_for_directory_output(self):
|
||||||
|
"""Test that assets are shipped automatically when output is a directory."""
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir)
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_dir / "test.html").exists()
|
||||||
|
|
||||||
|
# Check that assets were copied
|
||||||
|
assert (output_dir / "images" / "arch.png").exists()
|
||||||
|
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||||
|
assert (output_dir / "diagrams" / "flow.svg").exists()
|
||||||
|
assert (output_dir / "docs" / "readme.md").exists()
|
||||||
|
|
||||||
|
def test_no_ship_assets_flag_suppresses_asset_copying(self):
|
||||||
|
"""Test that --no-ship-assets flag prevents asset copying."""
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir),
|
||||||
|
'--no-ship-assets'
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_dir / "test.html").exists()
|
||||||
|
|
||||||
|
# Check that assets were NOT copied
|
||||||
|
assert not (output_dir / "images").exists()
|
||||||
|
assert not (output_dir / "assets").exists()
|
||||||
|
assert not (output_dir / "diagrams").exists()
|
||||||
|
|
||||||
|
def test_timestamp_based_asset_copying(self):
|
||||||
|
"""Test that assets are only copied if source is newer than destination."""
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
# First render - assets should be copied
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir)
|
||||||
|
])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Mark output asset as newer
|
||||||
|
output_asset = output_dir / "images" / "arch.png"
|
||||||
|
original_mtime = output_asset.stat().st_mtime
|
||||||
|
output_asset.touch() # Update timestamp
|
||||||
|
|
||||||
|
# Second render - asset should not be overwritten
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir)
|
||||||
|
])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Check that the timestamp wasn't changed (asset wasn't overwritten)
|
||||||
|
assert output_asset.stat().st_mtime > original_mtime
|
||||||
|
|
||||||
|
def test_ship_assets_flag_explicit_enable(self):
|
||||||
|
"""Test that --ship-assets flag explicitly enables asset shipping."""
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir),
|
||||||
|
'--ship-assets'
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_dir / "test.html").exists()
|
||||||
|
assert (output_dir / "images" / "arch.png").exists()
|
||||||
|
|
||||||
|
def test_missing_assets_handled_gracefully(self):
|
||||||
|
"""Test that missing assets are handled with warnings, not errors."""
|
||||||
|
# Remove one of the assets
|
||||||
|
(self.test_dir / "images" / "arch.png").unlink()
|
||||||
|
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Should succeed despite missing asset
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (output_dir / "test.html").exists()
|
||||||
|
|
||||||
|
# Other assets should still be copied
|
||||||
|
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||||
|
|
||||||
|
def test_asset_discovery_from_markdown_content(self):
|
||||||
|
"""Test discovery of assets from markdown content."""
|
||||||
|
from markitect.assets.discovery import discover_assets_from_markdown
|
||||||
|
|
||||||
|
assets = discover_assets_from_markdown(self.markdown_content, self.test_dir)
|
||||||
|
|
||||||
|
# Should find all asset references
|
||||||
|
asset_paths = [asset.asset_path for asset in assets]
|
||||||
|
assert "images/arch.png" in asset_paths
|
||||||
|
assert "assets/logo.jpg" in asset_paths
|
||||||
|
assert "./diagrams/flow.svg" in asset_paths
|
||||||
|
assert "docs/readme.md" in asset_paths
|
||||||
|
|
||||||
|
def test_relative_path_preservation(self):
|
||||||
|
"""Test that relative path structure is preserved in output."""
|
||||||
|
output_dir = self.test_dir / "output"
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_dir)
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
# Check that directory structure is preserved
|
||||||
|
assert (output_dir / "images" / "arch.png").exists()
|
||||||
|
assert (output_dir / "assets" / "logo.jpg").exists()
|
||||||
|
assert (output_dir / "diagrams" / "flow.svg").exists()
|
||||||
|
assert (output_dir / "docs" / "readme.md").exists()
|
||||||
|
|
||||||
|
def test_asset_shipping_disabled_for_file_output(self):
|
||||||
|
"""Test that asset shipping is disabled when output is a specific file."""
|
||||||
|
# Create a separate output directory
|
||||||
|
output_dir = self.test_dir / "output_dir"
|
||||||
|
output_dir.mkdir()
|
||||||
|
output_file = output_dir / "specific_output.html"
|
||||||
|
|
||||||
|
result = self.runner.invoke(md_render_command, [
|
||||||
|
str(self.md_file),
|
||||||
|
'--output', str(output_file)
|
||||||
|
])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert output_file.exists()
|
||||||
|
|
||||||
|
# Assets should NOT be copied when output is a specific file
|
||||||
|
# (they should not exist in the output directory)
|
||||||
|
assert not (output_dir / "images").exists()
|
||||||
|
assert not (output_dir / "assets").exists()
|
||||||
Reference in New Issue
Block a user