refactor: Still trying to reorganize edit mode to be more robust
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (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

This commit is contained in:
2025-11-04 21:59:22 +01:00
parent 85faf502c4
commit c5a5b26797
487 changed files with 94669 additions and 144 deletions

View File

@@ -1978,10 +1978,18 @@ def md_list_command(ctx, output_format, names_only):
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.option('--verbose', '-v', is_flag=True,
help='Show detailed output including asset operations')
@click.option('--silent', '-s', is_flag=True,
help='Suppress non-essential output')
@click.option('--image-max-width', type=str, default=None,
help='Maximum width for images (default: 12cm, supports px, em, %, cm, in, etc.)')
@click.option('--image-max-height', type=str, default=None,
help='Maximum height for images (default: 20cm, supports px, em, %, cm, in, etc.)')
@click.pass_context
def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_theme,
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag,
ship_assets, no_ship_assets):
ship_assets, no_ship_assets, verbose, silent, image_max_width, image_max_height):
"""
Render a markdown file to HTML with basic templates and live preview capabilities.
@@ -2013,10 +2021,35 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
if edit and insert:
raise click.BadParameter("Cannot use both --edit and --insert flags simultaneously. Choose one mode.")
# Check environment variables for edit/insert modes (if not set via CLI flags)
import os
if not edit and not insert:
if os.environ.get('MARKITECT_EDIT_MODE', '').lower() in ('true', '1', 'yes'):
edit = True
elif os.environ.get('MARKITECT_INSERT_MODE', '').lower() in ('true', '1', 'yes'):
insert = True
# 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.")
# Validate verbosity flags
if verbose and silent:
raise click.BadParameter("Cannot use both --verbose and --silent flags simultaneously.")
# Handle image size configuration with environment variable support
import os
# Get image max width (CLI > ENV > default)
final_image_max_width = image_max_width
if final_image_max_width is None:
final_image_max_width = os.environ.get('MARKITECT_IMAGE_MAX_WIDTH', '12cm')
# Get image max height (CLI > ENV > default)
final_image_max_height = image_max_height
if final_image_max_height is None:
final_image_max_height = os.environ.get('MARKITECT_IMAGE_MAX_HEIGHT', '20cm')
# Determine output path with environment variable support
if output:
output_path = Path(output)
@@ -2066,7 +2099,7 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
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))
_ship_assets(input_path, output_path.parent, verbose, silent)
# For file output, we don't ship assets (shouldn't reach here anyway)
# Initialize clean document manager
@@ -2081,11 +2114,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
edit_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag)
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
if not silent:
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Editor theme: {editor_theme}")
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"Theme: {theme or 'default'}")
@@ -2097,11 +2133,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
insert_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag)
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
if not silent:
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Editor theme: {editor_theme}")
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"Heading protection: levels 1-3 read-only")
@@ -2113,10 +2152,13 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
template=theme, css=css,
edit_mode=False,
insert_mode=False,
nodogtag=nodogtag)
click.echo(f"✓ Rendered to: {output_path}")
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if not silent:
click.echo(f"✓ Rendered to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Theme: {theme or 'default'}")
click.echo(f"CSS: {css or 'default'}")
@@ -3482,18 +3524,28 @@ class FilenameDecoder:
return [self.decode(filename) for filename in filenames]
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False, silent: 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
verbose: Whether to print detailed output
silent: Whether to suppress non-essential output
"""
import shutil
import hashlib
from markitect.assets.discovery import discover_assets_from_markdown
def get_file_hash(file_path):
"""Get SHA-256 hash of file content for content comparison."""
hash_sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
try:
# Read the markdown content
markdown_content = input_path.read_text(encoding='utf-8')
@@ -3524,14 +3576,49 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
# Create destination directory
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Check if we need to copy (timestamp-based)
# Check if we need to copy (smart comparison for cross-filesystem compatibility)
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
source_stat = asset_ref.resolved_path.stat()
dest_stat = dest_path.stat()
# Detect if we're in a cross-filesystem scenario where timestamps might be unreliable
# Heuristics: different filesystems, or timestamps that don't make sense
is_cross_fs = (
# Different device IDs suggests different filesystems
source_stat.st_dev != dest_stat.st_dev or
# Destination path starts with /mnt/ (common WSL Windows mount)
str(dest_path).startswith('/mnt/') or
# Very large timestamp differences (>1 hour) for same content suggest sync issues
abs(source_stat.st_mtime - dest_stat.st_mtime) > 3600
)
if is_cross_fs:
# Use content-based comparison for cross-filesystem scenarios
if source_stat.st_size == dest_stat.st_size:
try:
source_hash = get_file_hash(asset_ref.resolved_path)
dest_hash = get_file_hash(dest_path)
if source_hash == dest_hash:
should_copy = False
skipped_count += 1
if verbose:
click.echo(f" → Content verified (cross-fs): {asset_ref.asset_path}")
# If hashes differ, should_copy remains True
except (OSError, IOError):
if verbose:
click.echo(f" ⚠ Could not verify content, will copy: {asset_ref.asset_path}")
pass
# If sizes differ, should_copy remains True
else:
# Use fast timestamp comparison for same-filesystem scenarios
if source_stat.st_mtime <= dest_stat.st_mtime and source_stat.st_size == dest_stat.st_size:
should_copy = False
skipped_count += 1
if verbose:
click.echo(f" → Timestamp verified: {asset_ref.asset_path}")
# If timestamp suggests newer source or different size, should_copy remains True
if should_copy:
shutil.copy2(asset_ref.resolved_path, dest_path)
@@ -3541,12 +3628,21 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
elif verbose:
click.echo(f" → Skipped (up-to-date): {asset_ref.asset_path}")
# Summary
if verbose or shipped_count > 0:
# Summary - provide feedback based on verbosity settings
total_assets = shipped_count + skipped_count + missing_count
if total_assets > 0 and not silent:
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")
elif skipped_count > 0:
click.echo(f"✓ All {skipped_count} assets up-to-date")
# Additional details for verbose or when there are mixed results
if verbose or (shipped_count > 0 and skipped_count > 0):
if skipped_count > 0 and shipped_count > 0:
click.echo(f"{skipped_count} already up-to-date")
# Always show missing assets as it's important information
if missing_count > 0:
click.echo(f"{missing_count} assets not found", err=True)