**Asset Deployment Infrastructure:** - Enhanced RenderingEngineManager with complete asset deployment - Automatic plugin source directory discovery and asset copying - File system operations with proper directory structure preservation - Comprehensive error handling for missing assets **CLI Integration:** - Automatic asset deployment when using plugin engines - Verbose output showing deployment progress and statistics - Asset verification and accessibility validation - Production-ready deployment to _markitect/plugins/ structure **TestDrive JSUI Assets:** - Complete CSS asset suite (editor, controls, GitHub theme) - Placeholder image assets for testing deployment - Proper asset organization following plugin conventions - All 18 assets now deployed correctly **Testing Infrastructure:** - Comprehensive asset deployment testing - CLI integration verification with asset shipping - File existence and accessibility validation - Complete directory structure verification **Key Features:** - Assets deployed to `_markitect/plugins/testdrive-jsui/` when using --edit - HTML references match deployed asset locations - 18 total assets: 12 JS, 3 CSS, 3 images - Automatic deployment without --ship-assets flag needed - Clean separation of development vs production asset handling **Example Output:** ``` 🎯 Using rendering engine: testdrive-jsui (supports: edit, view) 📦 Deploying assets for engine 'testdrive-jsui'... 📄 Deployed 18 asset files js: 12 files css: 3 files images: 3 files ✅ Rendered with INTERACTIVE editing mode ``` This fixes the "HTML assets not found" issue when using explicit output directories with plugin engines. All plugin assets are now properly deployed and accessible. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
312 lines
11 KiB
Python
312 lines
11 KiB
Python
"""
|
|
Rendering Engine Plugin Support
|
|
|
|
Extends the existing MarkiTect plugin system to support UI rendering engines
|
|
for different output modes (edit, view, print, etc.).
|
|
"""
|
|
|
|
from abc import abstractmethod
|
|
from typing import Dict, List, Optional, Any
|
|
from pathlib import Path
|
|
import json
|
|
|
|
from .base import BasePlugin, PluginType, PluginMetadata
|
|
|
|
|
|
class RenderingEnginePlugin(BasePlugin):
|
|
"""Base class for rendering engine plugins."""
|
|
|
|
def __init__(self):
|
|
"""Initialize rendering engine plugin."""
|
|
# Set plugin type to a new RENDERING type
|
|
if not hasattr(PluginType, 'RENDERING'):
|
|
# Add RENDERING type if it doesn't exist
|
|
PluginType.RENDERING = "rendering"
|
|
|
|
super().__init__()
|
|
|
|
@abstractmethod
|
|
def get_supported_modes(self) -> List[str]:
|
|
"""
|
|
Return supported rendering modes.
|
|
|
|
Returns:
|
|
List of mode strings (e.g., ['edit', 'view', 'print'])
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_required_assets(self) -> Dict[str, List[str]]:
|
|
"""
|
|
Return required assets by type.
|
|
|
|
Returns:
|
|
Dict with keys like 'js', 'css', 'images', each containing
|
|
list of relative paths within the plugin directory.
|
|
|
|
Example:
|
|
{
|
|
'js': ['static/js/main.js', 'static/js/config-loader.js'],
|
|
'css': ['static/css/editor.css'],
|
|
'images': ['images/icons/edit.png']
|
|
}
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def render_document(self,
|
|
content: str,
|
|
mode: str,
|
|
config: 'RenderingConfig') -> str:
|
|
"""
|
|
Render markdown content to HTML using this engine.
|
|
|
|
Args:
|
|
content: Markdown content to render
|
|
mode: Rendering mode ('edit', 'view', etc.)
|
|
config: Rendering configuration with asset paths
|
|
|
|
Returns:
|
|
Complete HTML document
|
|
"""
|
|
pass
|
|
|
|
def get_template_path(self) -> Optional[Path]:
|
|
"""Return path to engine's HTML template file (optional)."""
|
|
return None
|
|
|
|
def validate_mode(self, mode: str) -> bool:
|
|
"""Check if mode is supported by this engine."""
|
|
return mode in self.get_supported_modes()
|
|
|
|
def get_asset_manifest(self) -> Dict[str, Any]:
|
|
"""
|
|
Get complete asset manifest for this rendering engine.
|
|
|
|
Returns:
|
|
Manifest dict with asset information for deployment
|
|
"""
|
|
return {
|
|
'name': self.metadata.name,
|
|
'version': self.metadata.version,
|
|
'modes': self.get_supported_modes(),
|
|
'assets': self.get_required_assets(),
|
|
'template': str(self.get_template_path()) if self.get_template_path() else None
|
|
}
|
|
|
|
|
|
class RenderingConfig:
|
|
"""Configuration for rendering engine asset management and deployment."""
|
|
|
|
def __init__(self,
|
|
asset_base_url: str = "_markitect",
|
|
development_mode: bool = False,
|
|
plugin_source_dirs: Optional[Dict[str, Path]] = None,
|
|
output_directory: Optional[Path] = None):
|
|
"""
|
|
Initialize rendering configuration.
|
|
|
|
Args:
|
|
asset_base_url: Base URL/path for assets (e.g., "_markitect")
|
|
development_mode: If True, serve from source directories
|
|
plugin_source_dirs: Map of plugin_name -> source directory path
|
|
output_directory: Target directory for asset deployment
|
|
"""
|
|
self.asset_base_url = asset_base_url
|
|
self.development_mode = development_mode
|
|
self.plugin_source_dirs = plugin_source_dirs or {}
|
|
self.output_directory = output_directory
|
|
self._asset_cache = {}
|
|
|
|
def get_asset_url(self, plugin_name: str, asset_path: str) -> str:
|
|
"""
|
|
Get URL path for a plugin asset.
|
|
|
|
Args:
|
|
plugin_name: Name of the plugin (e.g., 'testdrive-jsui')
|
|
asset_path: Relative path within plugin (e.g., 'static/js/main.js')
|
|
|
|
Returns:
|
|
Full asset URL path
|
|
"""
|
|
if self.development_mode and plugin_name in self.plugin_source_dirs:
|
|
# Development: serve directly from source directory
|
|
source_dir = self.plugin_source_dirs[plugin_name]
|
|
return f"file://{source_dir}/{asset_path}"
|
|
else:
|
|
# Production: serve from _markitect/plugins/
|
|
return f"{self.asset_base_url}/plugins/{plugin_name}/{asset_path}"
|
|
|
|
def get_plugin_asset_dir(self, plugin_name: str) -> Path:
|
|
"""Get the asset directory path for a plugin."""
|
|
if self.output_directory:
|
|
return self.output_directory / self.asset_base_url / "plugins" / plugin_name
|
|
else:
|
|
return Path(self.asset_base_url) / "plugins" / plugin_name
|
|
|
|
def to_json_config(self, plugin_name: str) -> str:
|
|
"""
|
|
Generate JSON configuration for JavaScript consumption.
|
|
|
|
Args:
|
|
plugin_name: Name of the plugin for which to generate config
|
|
|
|
Returns:
|
|
JSON string suitable for embedding in HTML
|
|
"""
|
|
config_data = {
|
|
'pluginName': plugin_name,
|
|
'assetBaseUrl': self.asset_base_url,
|
|
'developmentMode': self.development_mode,
|
|
'pluginAssetDir': f"{self.asset_base_url}/plugins/{plugin_name}"
|
|
}
|
|
|
|
if plugin_name in self.plugin_source_dirs:
|
|
config_data['sourceDir'] = str(self.plugin_source_dirs[plugin_name])
|
|
|
|
return json.dumps(config_data, indent=2)
|
|
|
|
|
|
class RenderingEngineManager:
|
|
"""Manager for rendering engine plugins."""
|
|
|
|
def __init__(self, plugin_manager):
|
|
"""
|
|
Initialize with existing plugin manager.
|
|
|
|
Args:
|
|
plugin_manager: Main MarkiTect plugin manager instance
|
|
"""
|
|
self.plugin_manager = plugin_manager
|
|
self._engines: Dict[str, RenderingEnginePlugin] = {}
|
|
self._discover_rendering_engines()
|
|
|
|
def _discover_rendering_engines(self):
|
|
"""Discover rendering engine plugins."""
|
|
# 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():
|
|
if plugin_info.get('type') == 'rendering':
|
|
try:
|
|
# Load the plugin
|
|
plugin_instance = self.plugin_manager.load_plugin(plugin_name)
|
|
if isinstance(plugin_instance, RenderingEnginePlugin):
|
|
self._engines[plugin_name] = plugin_instance
|
|
print(f"✅ Discovered rendering engine: {plugin_name}")
|
|
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)
|
|
|
|
def list_engines(self) -> List[str]:
|
|
"""List all registered engine names."""
|
|
return list(self._engines.keys())
|
|
|
|
def get_engines_for_mode(self, mode: str) -> List[str]:
|
|
"""Get engine names that support a specific mode."""
|
|
return [name for name, engine in self._engines.items()
|
|
if engine.validate_mode(mode)]
|
|
|
|
def deploy_engine_assets(self,
|
|
engine_name: str,
|
|
config: RenderingConfig) -> Dict[str, str]:
|
|
"""
|
|
Deploy assets for a rendering engine.
|
|
|
|
Args:
|
|
engine_name: Name of the rendering engine
|
|
config: Rendering configuration
|
|
|
|
Returns:
|
|
Dict mapping asset types to deployment paths
|
|
"""
|
|
engine = self.get_engine(engine_name)
|
|
if not engine:
|
|
raise ValueError(f"Rendering engine '{engine_name}' not found")
|
|
|
|
if config.development_mode:
|
|
# In development mode, just return source paths
|
|
return {'status': 'development_mode', 'source': 'plugin_directory'}
|
|
|
|
if not config.output_directory:
|
|
return {'status': 'no_output_directory'}
|
|
|
|
# Production deployment: copy assets to output directory
|
|
import shutil
|
|
deployed_assets = {}
|
|
target_dir = config.get_plugin_asset_dir(engine_name)
|
|
required_assets = engine.get_required_assets()
|
|
|
|
# Create target directory
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Deploy each asset type
|
|
for asset_type, asset_list in required_assets.items():
|
|
if asset_type == 'external':
|
|
# Skip external assets (CDN resources)
|
|
continue
|
|
|
|
deployed_files = []
|
|
|
|
# Determine source directory for assets
|
|
source_base = self._get_plugin_source_dir(engine_name)
|
|
if not source_base or not source_base.exists():
|
|
print(f"⚠️ Plugin source directory not found for {engine_name}: {source_base}")
|
|
continue
|
|
|
|
for asset_path in asset_list:
|
|
source_file = source_base / asset_path
|
|
target_file = target_dir / asset_path
|
|
|
|
# Create parent directories
|
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if source_file.exists():
|
|
try:
|
|
shutil.copy2(source_file, target_file)
|
|
deployed_files.append(str(target_file))
|
|
print(f"📄 Deployed: {asset_path}")
|
|
except Exception as e:
|
|
print(f"⚠️ Failed to deploy {asset_path}: {e}")
|
|
else:
|
|
print(f"⚠️ Asset not found: {source_file}")
|
|
|
|
if deployed_files:
|
|
deployed_assets[asset_type] = deployed_files
|
|
|
|
return deployed_assets
|
|
|
|
def _get_plugin_source_dir(self, engine_name: str) -> Optional[Path]:
|
|
"""Get the source directory for a plugin."""
|
|
if engine_name == 'testdrive-jsui':
|
|
# Look for testdrive-jsui directory relative to current directory
|
|
candidates = [
|
|
Path('testdrive-jsui'),
|
|
Path(__file__).parent.parent.parent / 'testdrive-jsui',
|
|
Path('.') / 'testdrive-jsui'
|
|
]
|
|
|
|
for candidate in candidates:
|
|
if candidate.exists() and candidate.is_dir():
|
|
return candidate
|
|
|
|
return None |