Files
markitect-main/markitect/plugins/base.py
tegwick 8ef356af57 feat: implement plugin infrastructure for rendering engines
Added comprehensive plugin system for independent JavaScript UI development:

**Plugin Infrastructure:**
- Extended existing MarkiTect plugin system with RenderingEnginePlugin base class
- Added RENDERING plugin type to PluginType enum
- Created RenderingConfig for asset management and deployment
- Implemented RenderingEngineManager for plugin discovery and lifecycle

**TestDrive JSUI Plugin:**
- Extracted JavaScript UI components to independent testdrive-jsui plugin
- Created standalone development environment (no Python required)
- Implemented compass-positioned control panels (NW, NE, E, SE)
- Added clean JSON configuration interface for Python↔JavaScript data transfer

**Asset Management:**
- Development mode: serve assets directly from plugin source directory
- Production mode: deploy to _markitect/plugins/[plugin-name]/ structure
- Configurable asset URLs and deployment strategies
- Support for external dependencies (CDN resources)

**Standalone Development:**
- testdrive-jsui/test.html for browser-based development
- Package.json with npm scripts for development server
- Complete separation of JavaScript development from Python environment
- Hot reload and standard web development workflow

**Integration Demo:**
- demo_plugin_integration.py showcasing all plugin capabilities
- Standalone, plugin discovery, production deployment examples
- Asset URL generation for different deployment modes

This enables JavaScript-first development while maintaining clean integration
with the MarkiTect Python ecosystem. Developers can now work on UI components
independently using standard web development tools and workflows.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 06:49:41 +01:00

273 lines
7.2 KiB
Python

"""
Base classes and interfaces for MarkiTect plugins.
This module defines the core plugin architecture that all plugins must implement.
"""
from abc import ABC, abstractmethod
from enum import Enum
from typing import Dict, Any, Optional, List, Union
from pathlib import Path
import inspect
class PluginType(Enum):
"""Types of plugins supported by MarkiTect."""
PROCESSOR = "processor" # Content processors (markdown, etc.)
FORMATTER = "formatter" # Output formatters (JSON, YAML, etc.)
VALIDATOR = "validator" # Content validators
EXPORTER = "exporter" # Export handlers (PDF, HTML, etc.)
GENERATOR = "generator" # Content generators (templates, stubs, etc.)
IMPORTER = "importer" # Import handlers (various formats)
TRANSFORMER = "transformer" # Content transformers
EXTENSION = "extension" # General extensions
BACKEND = "backend" # Storage/API backends
COMMAND = "command" # CLI command extensions
RENDERING = "rendering" # UI rendering engines (edit, view modes)
class PluginMetadata:
"""Metadata about a plugin."""
def __init__(self,
name: str,
version: str,
description: str,
author: str = "",
plugin_type: PluginType = PluginType.EXTENSION,
dependencies: List[str] = None,
markitect_version: str = ">=0.1.0"):
self.name = name
self.version = version
self.description = description
self.author = author
self.plugin_type = plugin_type
self.dependencies = dependencies or []
self.markitect_version = markitect_version
class BasePlugin(ABC):
"""Abstract base class for all MarkiTect plugins."""
def __init__(self, config: Dict[str, Any] = None):
"""
Initialize plugin with configuration.
Args:
config: Plugin-specific configuration dictionary
"""
self.config = config or {}
self._metadata = None
self._initialized = False
@property
@abstractmethod
def metadata(self) -> PluginMetadata:
"""Return plugin metadata."""
pass
def initialize(self) -> bool:
"""
Initialize the plugin. Called after plugin is loaded.
Returns:
True if initialization successful, False otherwise
"""
try:
self._initialize()
self._initialized = True
return True
except Exception:
return False
def _initialize(self) -> None:
"""
Override this method to implement plugin-specific initialization.
Default implementation does nothing.
"""
pass
def cleanup(self) -> None:
"""
Cleanup plugin resources. Called when plugin is unloaded.
Override this method to implement cleanup logic.
"""
pass
@property
def is_initialized(self) -> bool:
"""Check if plugin is initialized."""
return self._initialized
def validate_config(self) -> List[str]:
"""
Validate plugin configuration.
Returns:
List of validation error messages (empty if valid)
"""
return []
class ProcessorPlugin(BasePlugin):
"""Base class for content processor plugins."""
@abstractmethod
def process(self, content: str, **kwargs) -> str:
"""
Process content and return processed result.
Args:
content: Input content to process
**kwargs: Additional processing parameters
Returns:
Processed content
"""
pass
def can_process(self, content: str, **kwargs) -> bool:
"""
Check if this processor can handle the given content.
Args:
content: Content to check
**kwargs: Additional context
Returns:
True if processor can handle content
"""
return True
class FormatterPlugin(BasePlugin):
"""Base class for output formatter plugins."""
@abstractmethod
def format(self, data: Any, **kwargs) -> str:
"""
Format data to string representation.
Args:
data: Data to format
**kwargs: Formatting options
Returns:
Formatted string
"""
pass
@abstractmethod
def get_file_extension(self) -> str:
"""
Get the file extension for this format.
Returns:
File extension (e.g., '.json', '.yaml')
"""
pass
class ValidatorPlugin(BasePlugin):
"""Base class for content validator plugins."""
@abstractmethod
def validate(self, content: str, **kwargs) -> List[str]:
"""
Validate content and return list of errors.
Args:
content: Content to validate
**kwargs: Validation options
Returns:
List of validation error messages (empty if valid)
"""
pass
class ExporterPlugin(BasePlugin):
"""Base class for export handler plugins."""
@abstractmethod
def export(self, data: Any, output_path: Path, **kwargs) -> bool:
"""
Export data to file.
Args:
data: Data to export
output_path: Output file path
**kwargs: Export options
Returns:
True if export successful
"""
pass
@abstractmethod
def get_supported_formats(self) -> List[str]:
"""
Get list of supported export formats.
Returns:
List of format names (e.g., ['pdf', 'html'])
"""
pass
class CommandPlugin(BasePlugin):
"""Base class for CLI command extension plugins."""
@abstractmethod
def get_commands(self) -> Dict[str, Any]:
"""
Get Click commands provided by this plugin.
Returns:
Dictionary mapping command names to Click command objects
"""
pass
def get_command_group_name(self) -> Optional[str]:
"""
Get the command group name if commands should be grouped.
Returns:
Group name or None for top-level commands
"""
return None
# Plugin discovery and loading utilities
def get_plugin_class_from_module(module, plugin_type: PluginType = None) -> List[type]:
"""
Discover plugin classes in a module.
Args:
module: Python module to search
plugin_type: Optional plugin type filter
Returns:
List of plugin classes found
"""
plugin_classes = []
for name, obj in inspect.getmembers(module, inspect.isclass):
if (issubclass(obj, BasePlugin) and
obj != BasePlugin and
not inspect.isabstract(obj)):
# Check plugin type if specified
if plugin_type:
try:
instance = obj()
if instance.metadata.plugin_type == plugin_type:
plugin_classes.append(obj)
except Exception:
# Skip if we can't instantiate or get metadata
continue
else:
plugin_classes.append(obj)
return plugin_classes