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:
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
131
markitect/templates/edit-mode.html
Normal file
131
markitect/templates/edit-mode.html
Normal 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>
|
||||
Reference in New Issue
Block a user