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>
273 lines
7.2 KiB
Python
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 |