feat: complete CLI integration with plugin system

**CLI Integration:**
- Added --engine parameter to md-render command
- Default engine selection: testdrive-jsui for edit/insert, standard for view
- Graceful fallback to standard rendering when plugin unavailable
- Engine validation and mode compatibility checking

**Plugin Discovery:**
- Enhanced RenderingEngineManager with builtin plugin registration
- Automatic discovery and registration of testdrive-jsui engine
- Support for both plugin system discovery and direct registration

**Configuration Management:**
- Production-ready RenderingConfig for CLI usage
- Asset deployment to _markitect/plugins/ structure
- Configurable asset base URLs and deployment strategies

**Testing Infrastructure:**
- Comprehensive test suite for plugin discovery
- CLI integration testing without Click framework dependencies
- Complete scenario testing (default, explicit, fallback, unknown engines)
- Integration verification scripts

**Documentation:**
- Complete PLUGIN_SYSTEM.md documentation
- Architecture overview and development workflows
- JavaScript-first development guide
- Asset management and deployment strategies
- CLI usage examples and troubleshooting guide

**Key Features:**
- `markitect md-render --edit` now uses testdrive-jsui by default
- `markitect md-render --engine testdrive-jsui --edit` for explicit selection
- `markitect md-render --engine standard --edit` for legacy behavior
- Automatic fallback with user-friendly error messages

This completes the plugin infrastructure implementation, enabling
independent JavaScript development with seamless CLI integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 08:47:30 +01:00
parent 8ef356af57
commit 8f1cc0faf9
9 changed files with 1268 additions and 23 deletions

View File

@@ -2033,6 +2033,8 @@ def md_list_command(ctx, output_format, names_only):
help='Open in interactive edit mode with stable section editing')
@click.option('--insert', is_flag=True,
help='Open in interactive insert mode with heading protection (levels 1-3 read-only)')
@click.option('--engine', type=str, default=None,
help='Rendering engine to use (default: testdrive-jsui for edit/insert, standard for view)')
@click.option('--editor-theme', default='github',
type=click.Choice(['github', 'monokai', 'tomorrow', 'dark']),
help='Editor theme for live edit mode (default: github)')
@@ -2057,7 +2059,7 @@ def md_list_command(ctx, output_format, names_only):
@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,
def md_render_command(ctx, input_file, output, theme, css, edit, insert, engine, editor_theme,
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag,
ship_assets, no_ship_assets, verbose, silent, image_max_width, image_max_height):
"""
@@ -2171,21 +2173,83 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
# For directory output, ship to the same directory as the HTML file
_ship_assets(input_path, output_path.parent, verbose, silent)
# Initialize clean document manager
from markitect.clean_document_manager import CleanDocumentManager
doc_manager = CleanDocumentManager(config.get('db_manager'))
# Determine rendering engine to use
if engine is None:
# Default engine selection
if edit or insert:
engine = 'testdrive-jsui' # Default to testdrive-jsui for interactive modes
else:
engine = 'standard' # Use standard CleanDocumentManager for non-interactive
# Use plugin system for rendering engines, fallback to standard
if engine != 'standard':
try:
from markitect.plugins import PluginManager, RenderingEngineManager, RenderingConfig
plugin_manager = PluginManager()
rendering_manager = RenderingEngineManager(plugin_manager)
rendering_engine = rendering_manager.get_engine(engine)
if rendering_engine is None:
if not silent:
click.echo(f"⚠️ Rendering engine '{engine}' not found, falling back to standard", err=True)
engine = 'standard'
elif not silent:
modes = rendering_engine.get_supported_modes()
current_mode = 'edit' if edit else ('insert' if insert else 'view')
if not rendering_engine.validate_mode(current_mode):
click.echo(f"⚠️ Engine '{engine}' doesn't support mode '{current_mode}', falling back to standard", err=True)
engine = 'standard'
else:
click.echo(f"🎯 Using rendering engine: {engine} (supports: {', '.join(modes)})")
except ImportError as e:
if not silent:
click.echo(f"⚠️ Plugin system not available ({e}), using standard rendering", err=True)
engine = 'standard'
# Initialize document manager or rendering engine
if engine == 'standard':
from markitect.clean_document_manager import CleanDocumentManager
doc_manager = CleanDocumentManager(config.get('db_manager'))
# Render the file
if edit:
# Edit mode - generate HTML with editing capabilities
result = doc_manager.render_file(input_file, str(output_path),
template=theme, css=css,
edit_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if engine != 'standard':
# Plugin-based rendering for edit mode
try:
# Read markdown content
content = input_path.read_text(encoding='utf-8')
# Configure rendering
render_config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False, # Production deployment for CLI usage
output_directory=output_path.parent
)
# Render using plugin
html_content = rendering_engine.render_document(content, 'edit', render_config)
# Write output
output_path.write_text(html_content, encoding='utf-8')
result = True
except Exception as e:
if not silent:
click.echo(f"❌ Plugin rendering failed: {e}", err=True)
click.echo(" Falling back to standard rendering...", err=True)
engine = 'standard'
if engine == 'standard':
# Standard edit mode - generate HTML with editing capabilities
result = doc_manager.render_file(input_file, str(output_path),
template=theme, css=css,
edit_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if not silent:
click.echo(f"✅ Rendered with INTERACTIVE editing mode to: {output_path}")
@@ -2197,15 +2261,48 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
click.echo(f"Theme: {theme or 'default'}")
click.echo(f"CSS: {css or 'default'}")
elif insert:
# Insert mode - generate HTML with insert capabilities and heading protection
result = doc_manager.render_file(input_file, str(output_path),
template=theme, css=css,
insert_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if engine != 'standard':
# Plugin-based rendering for insert mode
try:
# Read markdown content
content = input_path.read_text(encoding='utf-8')
# Configure rendering
render_config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False, # Production deployment for CLI usage
output_directory=output_path.parent
)
# Render using plugin (note: insert mode may not be supported by all plugins)
if rendering_engine.validate_mode('insert'):
html_content = rendering_engine.render_document(content, 'insert', render_config)
else:
# Fallback to edit mode if insert not supported
html_content = rendering_engine.render_document(content, 'edit', render_config)
if not silent:
click.echo(f" Engine '{engine}' doesn't support insert mode, using edit mode instead")
# Write output
output_path.write_text(html_content, encoding='utf-8')
result = True
except Exception as e:
if not silent:
click.echo(f"❌ Plugin rendering failed: {e}", err=True)
click.echo(" Falling back to standard rendering...", err=True)
engine = 'standard'
if engine == 'standard':
# Standard insert mode - generate HTML with insert capabilities and heading protection
result = doc_manager.render_file(input_file, str(output_path),
template=theme, css=css,
insert_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if not silent:
click.echo(f"✅ Rendered with INTERACTIVE insert mode to: {output_path}")

View File

@@ -183,7 +183,7 @@ class RenderingEngineManager:
def _discover_rendering_engines(self):
"""Discover rendering engine plugins."""
# Get all plugins from the main plugin manager
# First, try to load plugins from main plugin manager
all_plugins = self.plugin_manager.discover_plugins()
for plugin_name, plugin_info in all_plugins.items():
@@ -197,6 +197,22 @@ class RenderingEngineManager:
except Exception as e:
print(f"⚠️ Failed to load rendering engine {plugin_name}: {e}")
# Additionally, try to directly import and register known rendering engines
self._register_builtin_rendering_engines()
def _register_builtin_rendering_engines(self):
"""Register built-in rendering engines directly."""
try:
# Import and register testdrive-jsui engine
from .testdrive_jsui import TestDriveJSUIEngine
engine = TestDriveJSUIEngine()
self._engines[engine.metadata.name] = engine
print(f"✅ Registered built-in rendering engine: {engine.metadata.name}")
except ImportError as e:
print(f"⚠️ Could not import testdrive-jsui engine: {e}")
except Exception as e:
print(f"⚠️ Failed to register testdrive-jsui engine: {e}")
def get_engine(self, name: str) -> Optional[RenderingEnginePlugin]:
"""Get a rendering engine by name."""
return self._engines.get(name)