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)

View File

@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
{css_content}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
onload="window.markitectMarkedLoaded = true"
onerror="window.markitectMarkedError = true"></script>
</head>
<body class="{mode_class}">
<div id="markdown-content"></div>
<!-- Configuration Data Interface - ONLY place where Python data enters JavaScript -->
<script id="markitect-config" type="application/json">{config_json}</script>
<!-- Pure Static JavaScript Components - Embedded inline to avoid path issues -->
<script>
{js_config_loader}
</script>
<script>
{js_debug_system}
</script>
<script>
{js_section_manager}
</script>
<script>
{js_debug_panel}
</script>
<script>
{js_document_controls}
</script>
<script>
{js_dom_renderer}
</script>
<script>
{js_control_base}
</script>
<script>
{js_contents_control}
</script>
<script>
{js_status_control}
</script>
<script>
{js_debug_control}
</script>
<script>
{js_edit_control}
</script>
<script>
{js_main}
</script>
<!-- Initialization Script -->
<script>
// Clean initialization - no Python-generated code!
document.addEventListener('DOMContentLoaded', function() {
console.log('🎯 DOM loaded, starting Markitect initialization...');
// Wait for configuration to be ready before initializing
if (window.markitectConfig) {
window.markitectConfig.waitForReady(function() {
console.log('🎯 Configuration ready, initializing ' + window.markitectConfig.mode + ' mode...');
// Initialize edit/insert capabilities
if (window.markitectConfig.isEditMode || window.markitectConfig.isInsertMode) {
try {
console.log('🚀 Initializing clean ' + window.markitectConfig.mode + ' capabilities...');
// Initialize main application
if (typeof MarkitectMain !== 'undefined' && MarkitectMain.initialize) {
MarkitectMain.initialize();
}
console.log('✅ Clean ' + window.markitectConfig.mode + ' mode active - click any section to edit');
} catch (error) {
console.error('❌ Clean ' + window.markitectConfig.mode + ' mode failed to initialize:', error);
}
}
});
} else {
console.error('❌ Configuration system not available');
}
// Check if modular components are being used for content rendering
if (typeof SectionManager !== 'undefined') {
console.log('✓ Modular components detected - using modular architecture');
return;
}
// Fallback content rendering if modular components failed
const contentDiv = document.getElementById('markdown-content');
if (contentDiv) {
if (typeof marked !== 'undefined') {
try {
const html = marked.parse(window.markitectConfig.markdownContentWithDogtag);
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
contentDiv.innerHTML = htmlWithTargetBlank;
console.log('✓ Content rendered successfully with fallback');
} catch (error) {
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
console.error('Content rendering failed:', error.message);
}
} else {
// Basic fallback without marked.js
const fallbackHtml = window.markitectConfig.markdownContent
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n\n/g, '<br><br>')
.replace(/\n/g, '<br>');
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
console.warn('Content rendered with basic fallback parser');
}
}
});
window.addEventListener('load', function() {
if (window.markitectMarkedError) {
console.error('CDN library failed to load - network or firewall blocking marked.js');
}
});
</script>
</body>
</html>