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

452
docs/PLUGIN_SYSTEM.md Normal file
View File

@@ -0,0 +1,452 @@
# Markitect Plugin System Documentation
## Overview
The Markitect plugin system provides a modular architecture for extending rendering capabilities with independent JavaScript UI components. This system enables JavaScript-first development while maintaining clean integration with the Python ecosystem.
## Architecture
### Core Components
1. **RenderingEnginePlugin**: Base class for UI rendering engines
2. **RenderingConfig**: Asset management and deployment configuration
3. **RenderingEngineManager**: Plugin discovery and lifecycle management
4. **PluginManager**: Integration with existing Markitect plugin system
### Key Features
- **Clean Separation**: JSON-based configuration interface (Python ↔ JavaScript)
- **Independent Development**: JavaScript components work standalone
- **Asset Management**: Configurable deployment strategies
- **Multiple Engines**: Support for different UI frameworks
- **Fallback Support**: Graceful degradation to standard rendering
## Plugin Development
### Creating a Rendering Engine Plugin
1. **Extend RenderingEnginePlugin**:
```python
from markitect.plugins.rendering import RenderingEnginePlugin, RenderingConfig
from markitect.plugins.base import PluginMetadata, PluginType
class MyUIEngine(RenderingEnginePlugin):
def __init__(self):
super().__init__()
self._metadata = PluginMetadata(
name="my-ui-engine",
version="1.0.0",
description="Custom UI rendering engine",
author="Your Name",
plugin_type=PluginType.RENDERING
)
@property
def metadata(self) -> PluginMetadata:
return self._metadata
def get_supported_modes(self) -> List[str]:
return ["edit", "view"]
def get_required_assets(self) -> Dict[str, List[str]]:
return {
"js": ["static/js/main.js"],
"css": ["static/css/style.css"]
}
def render_document(self, content: str, mode: str, config: RenderingConfig) -> str:
# Your rendering logic here
return html_output
```
2. **Directory Structure**:
```
my-ui-engine/
├── static/
│ ├── js/ # JavaScript components
│ └── css/ # Stylesheets
├── templates/ # HTML templates
├── images/ # Icons and images
├── test-documents/ # Sample markdown files
├── package.json # Node.js configuration
└── README.md # Plugin documentation
```
3. **Asset Management**:
Assets are automatically deployed based on configuration:
- **Development**: Served from plugin source directory
- **Production**: Copied to `_markitect/plugins/{plugin-name}/`
## TestDrive JSUI Plugin
### Overview
The TestDrive JSUI plugin demonstrates the plugin architecture with a complete JavaScript UI for markdown editing.
### Features
- **Modular Components**: Clean separation of UI components
- **Compass Positioning**: NW, NE, E, SE control panel layout
- **Section Management**: Click-to-edit markdown sections
- **Debug System**: Built-in debugging and logging
- **Asset Pipeline**: Configurable asset deployment
### Directory Structure
```
testdrive-jsui/
├── static/js/
│ ├── core/ # Core systems
│ │ ├── debug-system.js
│ │ └── section-manager.js
│ ├── components/ # UI components
│ │ ├── debug-panel.js
│ │ ├── document-controls.js
│ │ └── dom-renderer.js
│ ├── controls/ # Control panels
│ │ ├── control-base.js
│ │ ├── contents-control.js # Northwest
│ │ ├── status-control.js # East
│ │ ├── debug-control.js # Southeast
│ │ └── edit-control.js # Northeast
│ ├── config-loader.js # Configuration interface
│ └── main-updated.js # Application entry point
├── static/css/ # Stylesheets (future)
├── images/ # Icons and images (future)
├── templates/
│ └── index.html # Main HTML template
├── test-documents/
│ └── sample.md # Test content
├── test.html # Standalone development
├── package.json # Node.js configuration
└── README.md # Plugin documentation
```
### JavaScript Architecture
- **Configuration Interface**: Clean JSON data transfer via `markitect-config` script element
- **Modular Components**: Each component has single responsibility
- **Event System**: Pub/sub for component communication
- **Control System**: Abstract base class for UI controls
- **Compass Positioning**: Consistent control panel layout
## CLI Integration
### Command Line Usage
```bash
# Use default engine (testdrive-jsui for edit/insert, standard for view)
markitect md-render --edit document.md
# Specify engine explicitly
markitect md-render --engine testdrive-jsui --edit document.md
# Use standard engine
markitect md-render --engine standard --edit document.md
# View available engines
markitect md-render --help
```
### Engine Selection Logic
1. **Default Selection**:
- Edit/Insert modes: `testdrive-jsui`
- View mode: `standard`
2. **Explicit Selection**: Use `--engine` parameter
3. **Fallback Strategy**:
- Engine not found → fallback to standard
- Mode not supported → fallback to standard
- Plugin error → fallback to standard
### Integration Points
The CLI integrates with the plugin system through:
```python
# Engine discovery
plugin_manager = PluginManager()
rendering_manager = RenderingEngineManager(plugin_manager)
# Engine selection
engine = rendering_manager.get_engine(engine_name)
# Configuration
config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False,
output_directory=output_path.parent
)
# Rendering
html_content = engine.render_document(content, mode, config)
```
## Development Workflows
### Standalone JavaScript Development
1. **Setup**:
```bash
cd testdrive-jsui
python -m http.server 8080
```
2. **Development**:
- Edit JavaScript files in `static/js/`
- Refresh browser to see changes
- Use `test.html` for testing
- Browser DevTools for debugging
3. **Benefits**:
- No Python environment required
- Fast iteration cycle
- Standard web development tools
- Hot reloading
### Integrated Development
1. **Plugin Testing**:
```bash
python demo_plugin_integration.py
```
2. **CLI Testing**:
```bash
markitect md-render --engine testdrive-jsui --edit test.md
```
3. **Integration Verification**:
```bash
python test_complete_integration.py
```
## Asset Management
### Development Mode
```python
config = RenderingConfig(
asset_base_url=".",
development_mode=True,
plugin_source_dirs={"testdrive-jsui": Path("testdrive-jsui")}
)
# Assets served as: file://testdrive-jsui/static/js/main.js
```
### Production Mode
```python
config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False,
output_directory=Path("/output")
)
# Assets served as: _markitect/plugins/testdrive-jsui/static/js/main.js
```
### Asset Types
- **js**: JavaScript files
- **css**: Stylesheets
- **images**: Icons, graphics
- **external**: CDN resources
### Deployment Strategy
1. **Assets Copying**: Plugin assets copied to `_markitect/plugins/{name}/`
2. **URL Generation**: Automatic URL generation for templates
3. **Cache Management**: Asset versioning and cache control
4. **Error Handling**: Fallback for missing assets
## Configuration Interface
### Python → JavaScript Data Transfer
All dynamic data passes through a clean JSON interface:
```html
<script id="markitect-config" type="application/json">
{
"markdownContent": "# Document content...",
"mode": "edit",
"theme": "github",
"keyboardShortcuts": true,
"originalFilename": "document.md",
"version": "Markitect v0.8.1"
}
</script>
```
### JavaScript Configuration Loading
```javascript
// Clean configuration loading
class MarkitectConfig {
constructor() {
this.loadConfig();
}
loadConfig() {
const configElement = document.getElementById('markitect-config');
this.config = JSON.parse(configElement.textContent);
}
}
```
### Benefits
- **No String Interpolation**: Prevents template literal escaping issues
- **Type Safety**: JSON validation and error handling
- **Clean Separation**: No JavaScript code in Python strings
- **Debuggable**: Easy to inspect configuration in browser
## Testing
### Plugin Testing
```bash
# Basic plugin discovery
python test_plugin_discovery.py
# CLI integration logic
python test_cli_simple.py
# Complete scenario testing
python test_complete_integration.py
# Full integration demo
python demo_plugin_integration.py
```
### Browser Testing
1. **Standalone**: Open `testdrive-jsui/test.html`
2. **Generated**: Open CLI-generated HTML files
3. **DevTools**: Use browser debugging tools
4. **Console**: Check for JavaScript errors
### Integration Testing
- **Unit Tests**: Individual component testing
- **Integration Tests**: Component interaction testing
- **E2E Tests**: Full workflow testing
- **Regression Tests**: Ensure stability across changes
## Extension Points
### Adding New Engines
1. Create new plugin extending `RenderingEnginePlugin`
2. Implement required methods (`get_supported_modes`, `render_document`, etc.)
3. Register in `RenderingEngineManager._register_builtin_rendering_engines()`
4. Test with CLI integration
### Adding New Modes
1. Add mode to engine's `get_supported_modes()`
2. Update `render_document()` to handle new mode
3. Test mode validation and rendering
4. Update CLI integration if needed
### Adding New Asset Types
1. Update `get_required_assets()` return format
2. Modify asset deployment logic in `RenderingConfig`
3. Update template system to handle new asset types
4. Test asset URL generation
## Best Practices
### Plugin Development
- **Single Responsibility**: Each component has one clear purpose
- **Clean Interfaces**: Well-defined APIs between components
- **Error Handling**: Graceful degradation on failures
- **Documentation**: Clear README and code comments
### JavaScript Development
- **Modular Architecture**: Avoid monolithic JavaScript files
- **Event-Driven**: Use pub/sub for component communication
- **Configuration-Driven**: Avoid hardcoded values
- **Browser Compatibility**: Test across different browsers
### Asset Management
- **Relative Paths**: Use relative paths in asset definitions
- **Versioning**: Include version info for cache management
- **Optimization**: Minimize asset size for production
- **CDN Integration**: Use CDN for external dependencies
### Testing Strategy
- **Automated Testing**: Comprehensive test coverage
- **Manual Testing**: User workflow validation
- **Cross-Platform**: Test on different environments
- **Performance Testing**: Monitor rendering performance
## Troubleshooting
### Common Issues
1. **Plugin Not Found**:
- Check plugin registration in `_register_builtin_rendering_engines()`
- Verify plugin class inheritance from `RenderingEnginePlugin`
- Check import paths and module availability
2. **Asset Loading Errors**:
- Verify asset paths in `get_required_assets()`
- Check file permissions and existence
- Validate URL generation in different modes
3. **Configuration Errors**:
- Check JSON syntax in configuration
- Verify configuration element ID (`markitect-config`)
- Test configuration loading in JavaScript
4. **Rendering Failures**:
- Check template file existence and permissions
- Verify template placeholder replacement
- Test with minimal content for debugging
### Debug Techniques
- **Console Logging**: Use browser console for debugging
- **Debug Panel**: TestDrive JSUI includes debug information
- **Verbose Mode**: Use CLI `--verbose` flag for detailed output
- **Test Scripts**: Run individual test scripts for isolation
## Future Enhancements
### Planned Features
- **Plugin Package Manager**: npm-like plugin distribution
- **Theme System Integration**: Plugin-aware theme system
- **Performance Monitoring**: Built-in performance tracking
- **Hot Reloading**: Automatic reload on file changes
### Extension Opportunities
- **React Integration**: React-based rendering engine
- **Vue Integration**: Vue.js-based rendering engine
- **TypeScript Support**: TypeScript plugin development
- **Testing Framework**: Automated JavaScript testing
### Community
- **Plugin Registry**: Central repository for community plugins
- **Documentation**: Expanded examples and tutorials
- **Templates**: Starter templates for new plugins
- **Best Practices**: Community guidelines and patterns
---
*This plugin system enables JavaScript-first development while maintaining clean integration with the MarkiTect Python ecosystem, providing the best of both worlds for UI development and backend processing.*

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>

171
test_cli_integration.py Normal file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
Test CLI integration with plugin system
"""
import sys
from pathlib import Path
# Add current directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
def test_plugin_cli_integration():
"""Test the CLI integration with plugin system."""
# Import the command function
from markitect.plugins.builtin.markdown_commands import md_render_command
import click
# Create a mock context
class MockContext:
def __init__(self):
self.obj = {}
self.resilient_parsing = False
self.allow_extra_args = False
self.allow_interspersed_args = True
self.ignore_unknown_options = False
self.help_option_names = ['--help']
self.token_normalize_func = None
self.color = None
self.terminal_width = None
self.max_content_width = None
ctx = MockContext()
# Test parameters
input_file = "test_cli_plugin.md"
output = "/tmp/test_cli_plugin_default.html"
print("🧪 Testing CLI Plugin Integration")
print("=" * 50)
try:
# Test 1: Default engine (should use testdrive-jsui for edit mode)
print("\n1⃣ Testing default engine for edit mode...")
md_render_command(
ctx=ctx,
input_file=input_file,
output=output,
theme=None,
css=None,
edit=True,
insert=False,
engine=None, # Should default to testdrive-jsui
editor_theme='github',
keyboard_shortcuts=True,
use_publication_dir=False,
dont_use_publication_dir=False,
nodogtag=False,
ship_assets=None,
no_ship_assets=False,
verbose=True,
silent=False,
image_max_width=None,
image_max_height=None
)
print("✅ Default engine test completed")
# Test 2: Explicit testdrive-jsui engine
print("\n2⃣ Testing explicit testdrive-jsui engine...")
output2 = "/tmp/test_cli_plugin_explicit.html"
md_render_command(
ctx=ctx,
input_file=input_file,
output=output2,
theme=None,
css=None,
edit=True,
insert=False,
engine='testdrive-jsui', # Explicit engine
editor_theme='github',
keyboard_shortcuts=True,
use_publication_dir=False,
dont_use_publication_dir=False,
nodogtag=False,
ship_assets=None,
no_ship_assets=False,
verbose=True,
silent=False,
image_max_width=None,
image_max_height=None
)
print("✅ Explicit engine test completed")
# Test 3: Standard engine fallback
print("\n3⃣ Testing standard engine fallback...")
output3 = "/tmp/test_cli_plugin_standard.html"
md_render_command(
ctx=ctx,
input_file=input_file,
output=output3,
theme=None,
css=None,
edit=True,
insert=False,
engine='standard', # Explicit standard engine
editor_theme='github',
keyboard_shortcuts=True,
use_publication_dir=False,
dont_use_publication_dir=False,
nodogtag=False,
ship_assets=None,
no_ship_assets=False,
verbose=True,
silent=False,
image_max_width=None,
image_max_height=None
)
print("✅ Standard engine test completed")
# Test 4: Unknown engine (should fallback)
print("\n4⃣ Testing unknown engine (should fallback to standard)...")
output4 = "/tmp/test_cli_plugin_unknown.html"
md_render_command(
ctx=ctx,
input_file=input_file,
output=output4,
theme=None,
css=None,
edit=True,
insert=False,
engine='unknown-engine', # Unknown engine
editor_theme='github',
keyboard_shortcuts=True,
use_publication_dir=False,
dont_use_publication_dir=False,
nodogtag=False,
ship_assets=None,
no_ship_assets=False,
verbose=True,
silent=False,
image_max_width=None,
image_max_height=None
)
print("✅ Unknown engine test completed")
print("\n🎉 All CLI integration tests completed!")
print("\nGenerated files:")
for output_file in [output, output2, output3, output4]:
if Path(output_file).exists():
size = Path(output_file).stat().st_size
print(f" 📄 {output_file} ({size:,} bytes)")
except Exception as e:
print(f"❌ CLI integration test failed: {e}")
import traceback
traceback.print_exc()
return False
return True
if __name__ == "__main__":
success = test_plugin_cli_integration()
sys.exit(0 if success else 1)

25
test_cli_plugin.md Normal file
View File

@@ -0,0 +1,25 @@
# CLI Plugin Integration Test
This is a test document to verify that the CLI integration with the testdrive-jsui plugin works correctly.
## Features to Test
- Plugin selection via `--engine` parameter
- Default engine selection for edit mode
- Fallback to standard rendering if plugin fails
## Test Scenarios
1. **Default behavior**: `markitect md-render --edit test.md`
- Should use testdrive-jsui by default
2. **Explicit plugin**: `markitect md-render --engine testdrive-jsui --edit test.md`
- Should use testdrive-jsui explicitly
3. **Standard fallback**: `markitect md-render --engine standard --edit test.md`
- Should use standard CleanDocumentManager
4. **Unknown engine**: `markitect md-render --engine unknown --edit test.md`
- Should fallback to standard with warning
This test will verify the plugin infrastructure integration.

121
test_cli_simple.py Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Test CLI plugin integration by directly calling the core logic
"""
import sys
from pathlib import Path
# Add current directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
def test_cli_integration():
"""Test CLI integration logic without Click framework."""
print("🧪 Testing CLI Plugin Integration Logic")
print("=" * 50)
try:
# Import required components
from markitect.plugins import PluginManager, RenderingEngineManager, RenderingConfig
# Test input
input_file = "test_cli_plugin.md"
output_file = "/tmp/test_cli_integration.html"
if not Path(input_file).exists():
print(f"❌ Test file {input_file} not found")
return False
# Read markdown content
content = Path(input_file).read_text(encoding='utf-8')
print(f"📄 Read test content ({len(content)} characters)")
# Test 1: Plugin system initialization
print("\n1⃣ Initializing plugin system...")
plugin_manager = PluginManager()
rendering_manager = RenderingEngineManager(plugin_manager)
print(" ✅ Plugin system initialized")
# Test 2: Engine selection logic (same as CLI)
engine_name = None
edit_mode = True
# Default engine selection (copied from CLI logic)
if engine_name is None:
if edit_mode:
engine_name = 'testdrive-jsui' # Default to testdrive-jsui for interactive modes
else:
engine_name = 'standard' # Use standard CleanDocumentManager for non-interactive
print(f" 🎯 Selected engine: {engine_name}")
# Test 3: Engine loading
print(f"\n2⃣ Loading rendering engine '{engine_name}'...")
rendering_engine = rendering_manager.get_engine(engine_name)
if rendering_engine is None:
print(f" ❌ Rendering engine '{engine_name}' not found")
return False
print(f" ✅ Engine loaded: {rendering_engine.metadata.name}")
print(f" 📝 Description: {rendering_engine.metadata.description}")
print(f" 🎯 Supported modes: {rendering_engine.get_supported_modes()}")
# Test 4: Mode validation
current_mode = 'edit'
if not rendering_engine.validate_mode(current_mode):
print(f" ❌ Engine doesn't support mode '{current_mode}'")
return False
print(f" ✅ Mode '{current_mode}' is supported")
# Test 5: Rendering configuration
print(f"\n3⃣ Setting up rendering configuration...")
render_config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False, # Production deployment for CLI usage
output_directory=Path(output_file).parent
)
print(f" ✅ Configuration created")
print(f" 📁 Output directory: {render_config.output_directory}")
print(f" 🔗 Asset base URL: {render_config.asset_base_url}")
# Test 6: Document rendering
print(f"\n4⃣ Rendering document...")
html_content = rendering_engine.render_document(content, current_mode, render_config)
print(f" ✅ Document rendered ({len(html_content):,} characters)")
# Test 7: Output writing
print(f"\n5⃣ Writing output file...")
Path(output_file).write_text(html_content, encoding='utf-8')
output_size = Path(output_file).stat().st_size
print(f" ✅ Output written: {output_file} ({output_size:,} bytes)")
# Test 8: Verification
print(f"\n6⃣ Verifying output...")
if Path(output_file).exists() and output_size > 0:
print(f" ✅ Output file exists and has content")
print(f" 🌐 Open in browser: file://{Path(output_file).absolute()}")
else:
print(f" ❌ Output file missing or empty")
return False
print(f"\n🎉 CLI integration test completed successfully!")
print(f"\n📊 Summary:")
print(f" Engine: {engine_name}")
print(f" Mode: {current_mode}")
print(f" Input: {input_file} ({len(content)} chars)")
print(f" Output: {output_file} ({output_size:,} bytes)")
return True
except Exception as e:
print(f"❌ CLI integration test failed: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = test_cli_integration()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Complete integration test demonstrating all CLI plugin functionality
"""
import sys
from pathlib import Path
# Add current directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
def test_engine_scenarios():
"""Test different engine scenarios."""
print("🚀 Complete CLI Plugin Integration Test")
print("=" * 60)
scenarios = [
("Default (edit mode)", None, True, False),
("Explicit testdrive-jsui", "testdrive-jsui", True, False),
("Standard engine", "standard", True, False),
("Unknown engine", "unknown-engine", True, False),
("Default (view mode)", None, False, False),
]
results = []
for scenario_name, engine, edit, insert in scenarios:
print(f"\n🧪 Testing: {scenario_name}")
print("-" * 50)
try:
# Import the core logic components
from markitect.plugins import PluginManager, RenderingEngineManager, RenderingConfig
input_file = "test_cli_plugin.md"
if not Path(input_file).exists():
print(f" ❌ Test file {input_file} not found")
continue
content = Path(input_file).read_text(encoding='utf-8')
# Initialize plugin system
plugin_manager = PluginManager()
rendering_manager = RenderingEngineManager(plugin_manager)
# Engine selection logic (copied from CLI)
selected_engine = engine
if selected_engine is None:
# Default engine selection
if edit or insert:
selected_engine = 'testdrive-jsui' # Default to testdrive-jsui for interactive modes
else:
selected_engine = 'standard' # Use standard CleanDocumentManager for non-interactive
print(f" 🎯 Selected engine: {selected_engine}")
# Check if engine is available
if selected_engine != 'standard':
rendering_engine = rendering_manager.get_engine(selected_engine)
if rendering_engine is None:
print(f" ⚠️ Engine '{selected_engine}' not found, would fallback to standard")
selected_engine = 'standard'
rendering_engine = None
else:
# Check mode support
current_mode = 'edit' if edit else ('insert' if insert else 'view')
if not rendering_engine.validate_mode(current_mode):
print(f" ⚠️ Engine '{selected_engine}' doesn't support '{current_mode}', would fallback to standard")
selected_engine = 'standard'
rendering_engine = None
else:
print(f" ✅ Engine supports mode '{current_mode}'")
# Perform rendering if plugin engine is available
if selected_engine != 'standard' and rendering_engine:
current_mode = 'edit' if edit else ('insert' if insert else 'view')
render_config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False,
output_directory=Path("/tmp")
)
html_content = rendering_engine.render_document(content, current_mode, render_config)
# Save output
output_file = f"/tmp/test_scenario_{scenario_name.lower().replace(' ', '_').replace('(', '').replace(')', '')}.html"
Path(output_file).write_text(html_content, encoding='utf-8')
output_size = Path(output_file).stat().st_size
print(f" ✅ Rendered using plugin engine ({output_size:,} bytes)")
print(f" 📄 Output: {output_file}")
results.append({
'scenario': scenario_name,
'engine': selected_engine,
'status': 'success',
'output_file': output_file,
'size': output_size
})
else:
print(f" Would use standard CleanDocumentManager")
results.append({
'scenario': scenario_name,
'engine': 'standard',
'status': 'fallback',
'output_file': None,
'size': 0
})
except Exception as e:
print(f" ❌ Failed: {e}")
results.append({
'scenario': scenario_name,
'engine': selected_engine,
'status': 'error',
'output_file': None,
'size': 0
})
# Summary
print(f"\n📊 Test Summary")
print("=" * 60)
successful = sum(1 for r in results if r['status'] == 'success')
fallback = sum(1 for r in results if r['status'] == 'fallback')
failed = sum(1 for r in results if r['status'] == 'error')
for result in results:
status_icon = {
'success': '',
'fallback': '🔄',
'error': ''
}[result['status']]
size_info = f"({result['size']:,} bytes)" if result['size'] > 0 else ""
print(f" {status_icon} {result['scenario']:<25}{result['engine']:<15} {size_info}")
print(f"\n🎯 Results: {successful} successful, {fallback} fallback, {failed} failed")
if successful > 0:
print(f"\n🌐 Generated files can be opened in browser:")
for result in results:
if result['output_file']:
print(f" file://{Path(result['output_file']).absolute()}")
return failed == 0
if __name__ == "__main__":
success = test_engine_scenarios()
sys.exit(0 if success else 1)

80
test_plugin_discovery.py Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Test plugin discovery and basic integration
"""
import sys
from pathlib import Path
# Add current directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
def test_plugin_discovery():
"""Test that the plugin system can discover testdrive-jsui."""
print("🔍 Testing Plugin Discovery")
print("=" * 50)
try:
# Test basic plugin imports
print("1⃣ Testing plugin imports...")
from markitect.plugins import PluginManager, RenderingEngineManager, RenderingConfig
print(" ✅ Plugin classes imported successfully")
# Test plugin manager initialization
print("\n2⃣ Testing plugin manager initialization...")
plugin_manager = PluginManager()
print(" ✅ Plugin manager initialized")
# Test rendering engine manager
print("\n3⃣ Testing rendering engine manager...")
rendering_manager = RenderingEngineManager(plugin_manager)
print(" ✅ Rendering engine manager initialized")
# List available engines
print("\n4⃣ Testing engine discovery...")
engines = rendering_manager.list_engines()
print(f" 📋 Found engines: {engines}")
# Test testdrive-jsui specifically
print("\n5⃣ Testing testdrive-jsui engine...")
engine = rendering_manager.get_engine('testdrive-jsui')
if engine:
print(f" ✅ TestDrive JSUI engine found!")
print(f" 📝 Name: {engine.metadata.name}")
print(f" 📄 Description: {engine.metadata.description}")
print(f" 🎯 Supported modes: {engine.get_supported_modes()}")
# Test render capabilities
print("\n6⃣ Testing render capabilities...")
test_content = "# Test\n\nThis is a test document."
config = RenderingConfig(
asset_base_url="_markitect",
development_mode=False,
output_directory=Path("/tmp")
)
html_output = engine.render_document(test_content, 'edit', config)
print(f" ✅ Rendered HTML ({len(html_output):,} characters)")
# Save test output
test_file = Path("/tmp/plugin_discovery_test.html")
test_file.write_text(html_output)
print(f" 📄 Saved test output: {test_file}")
else:
print(" ❌ TestDrive JSUI engine not found")
return False
print(f"\n🎉 Plugin discovery test completed successfully!")
return True
except Exception as e:
print(f"❌ Plugin discovery test failed: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = test_plugin_discovery()
sys.exit(0 if success else 1)