feat: implement comprehensive plugin architecture and extensions system (issue #19)

Complete plugin system implementation providing extensible architecture for MarkiTect:

🏗️ **Core Plugin Architecture**:
- BasePlugin abstract class with lifecycle management (initialize/cleanup)
- Specialized plugin types: ProcessorPlugin, FormatterPlugin, ValidatorPlugin, ExporterPlugin, CommandPlugin
- PluginMetadata system with version, dependencies, and type information
- Plugin initialization and configuration validation

🔍 **Plugin Discovery & Management**:
- PluginManager with automatic discovery from built-in modules and directories
- PluginRegistry for centralized plugin registration and lifecycle management
- Support for plugin loading, unloading, and reloading with configuration
- Plugin discovery from multiple sources (built-in, directories, packages)

🛠️ **CLI Integration**:
- markitect plugin-list: List all available plugins with metadata
- markitect plugin-load: Load plugins with optional configuration
- markitect plugin-unload: Unload plugins and cleanup resources
- markitect plugin-info: Show detailed plugin information
- markitect plugin-discover: Discover and refresh plugin catalog

📦 **Built-in Plugins**:
- JSON/YAML/Table formatters for output formatting
- Markdown/Text processors for content processing
- Auto-registered via @register_plugin decorator
- Comprehensive configuration options

🔧 **Developer Experience**:
- @register_plugin decorator for easy plugin registration
- Plugin configuration validation and error handling
- Comprehensive API documentation with examples
- Plugin development guide and best practices

📋 **Example Plugins**:
- Advanced text processor with case conversion and pattern replacement
- XML/CSV formatters demonstrating custom output formats
- Complete examples showing plugin development patterns

🧪 **Test Coverage**:
- 59 comprehensive tests covering all plugin functionality
- Tests for plugin lifecycle, registration, discovery, and CLI integration
- Error handling and edge case coverage
- Built-in plugin validation

Technical Implementation:
- Plugin types: processor, formatter, validator, exporter, generator, importer, transformer, extension, backend, command
- Configuration-driven plugin management with YAML/JSON support
- Graceful error handling and plugin isolation
- Plugin dependency validation and compatibility checking

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 11:23:32 +02:00
parent e6adb3e6db
commit b0de32d083
13 changed files with 3160 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""
Built-in plugins for MarkiTect.
This package contains the core plugins that ship with MarkiTect.
"""

View File

@@ -0,0 +1,154 @@
"""
Built-in formatter plugins for MarkiTect.
These formatters provide various output formats for MarkiTect data.
"""
import json
import yaml
from typing import Any
from ..base import FormatterPlugin, PluginMetadata, PluginType
from ..decorators import register_plugin
@register_plugin("json_formatter")
class JsonFormatter(FormatterPlugin):
"""JSON output formatter."""
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="json_formatter",
version="1.0.0",
description="Format output as JSON",
author="MarkiTect Team",
plugin_type=PluginType.FORMATTER
)
def format(self, data: Any, **kwargs) -> str:
"""Format data as JSON."""
indent = kwargs.get('indent', 2)
ensure_ascii = kwargs.get('ensure_ascii', False)
return json.dumps(data, indent=indent, ensure_ascii=ensure_ascii)
def get_file_extension(self) -> str:
"""Get JSON file extension."""
return '.json'
@register_plugin("yaml_formatter")
class YamlFormatter(FormatterPlugin):
"""YAML output formatter."""
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="yaml_formatter",
version="1.0.0",
description="Format output as YAML",
author="MarkiTect Team",
plugin_type=PluginType.FORMATTER
)
def format(self, data: Any, **kwargs) -> str:
"""Format data as YAML."""
default_flow_style = kwargs.get('default_flow_style', False)
indent = kwargs.get('indent', 2)
return yaml.dump(data, default_flow_style=default_flow_style, indent=indent)
def get_file_extension(self) -> str:
"""Get YAML file extension."""
return '.yaml'
@register_plugin("table_formatter")
class TableFormatter(FormatterPlugin):
"""Table output formatter for structured data."""
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="table_formatter",
version="1.0.0",
description="Format output as ASCII table",
author="MarkiTect Team",
plugin_type=PluginType.FORMATTER
)
def format(self, data: Any, **kwargs) -> str:
"""Format data as ASCII table."""
if not isinstance(data, (list, tuple)):
return str(data)
if not data:
return "No data"
# Handle list of dictionaries (most common case)
if isinstance(data[0], dict):
return self._format_dict_table(data, **kwargs)
# Handle simple list
return self._format_simple_table(data, **kwargs)
def _format_dict_table(self, data: list, **kwargs) -> str:
"""Format list of dictionaries as table."""
if not data:
return "No data"
# Get all unique keys
all_keys = set()
for item in data:
if isinstance(item, dict):
all_keys.update(item.keys())
headers = sorted(all_keys)
# Calculate column widths
col_widths = {}
for header in headers:
col_widths[header] = len(str(header))
for item in data:
if isinstance(item, dict) and header in item:
col_widths[header] = max(col_widths[header], len(str(item[header])))
# Build table
lines = []
# Header
header_line = "| " + " | ".join(h.ljust(col_widths[h]) for h in headers) + " |"
lines.append(header_line)
# Separator
sep_line = "|-" + "-|-".join("-" * col_widths[h] for h in headers) + "-|"
lines.append(sep_line)
# Data rows
for item in data:
if isinstance(item, dict):
row_line = "| " + " | ".join(
str(item.get(h, "")).ljust(col_widths[h]) for h in headers
) + " |"
lines.append(row_line)
return "\n".join(lines)
def _format_simple_table(self, data: list, **kwargs) -> str:
"""Format simple list as single-column table."""
max_width = max(len(str(item)) for item in data)
max_width = max(max_width, len("Value"))
lines = []
lines.append(f"| {'Value'.ljust(max_width)} |")
lines.append(f"|-{'-' * max_width}-|")
for item in data:
lines.append(f"| {str(item).ljust(max_width)} |")
return "\n".join(lines)
def get_file_extension(self) -> str:
"""Get table file extension."""
return '.txt'

View File

@@ -0,0 +1,136 @@
"""
Built-in processor plugins for MarkiTect.
These processors handle various content processing tasks.
"""
import re
from typing import Any
from ..base import ProcessorPlugin, PluginMetadata, PluginType
from ..decorators import register_plugin
@register_plugin("markdown_processor")
class MarkdownProcessor(ProcessorPlugin):
"""Basic markdown content processor."""
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="markdown_processor",
version="1.0.0",
description="Process markdown content",
author="MarkiTect Team",
plugin_type=PluginType.PROCESSOR
)
def process(self, content: str, **kwargs) -> str:
"""Process markdown content."""
# Basic markdown processing - normalize line endings
content = content.replace('\r\n', '\n').replace('\r', '\n')
# Add processing options
if kwargs.get('normalize_headers', False):
content = self._normalize_headers(content)
if kwargs.get('fix_line_endings', True):
content = self._fix_line_endings(content)
return content
def can_process(self, content: str, **kwargs) -> bool:
"""Check if content appears to be markdown."""
# Simple heuristic - check for markdown patterns
markdown_patterns = [
r'^#{1,6}\s', # Headers
r'^\*\s', # Unordered lists
r'^\d+\.\s', # Ordered lists
r'\*\*.*\*\*', # Bold
r'\*.*\*', # Italic
r'`.*`', # Inline code
r'```', # Code blocks
]
for pattern in markdown_patterns:
if re.search(pattern, content, re.MULTILINE):
return True
return False
def _normalize_headers(self, content: str) -> str:
"""Normalize header formatting."""
lines = content.split('\n')
normalized_lines = []
for line in lines:
# Ensure space after # in headers
if re.match(r'^#{1,6}[^#\s]', line):
hash_count = len(line) - len(line.lstrip('#'))
rest = line[hash_count:].lstrip()
normalized_lines.append('#' * hash_count + ' ' + rest)
else:
normalized_lines.append(line)
return '\n'.join(normalized_lines)
def _fix_line_endings(self, content: str) -> str:
"""Fix common line ending issues."""
# Remove trailing whitespace
lines = [line.rstrip() for line in content.split('\n')]
# Ensure single newline at end
while lines and not lines[-1]:
lines.pop()
return '\n'.join(lines) + '\n' if lines else ''
@register_plugin("text_processor")
class TextProcessor(ProcessorPlugin):
"""Generic text processor."""
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="text_processor",
version="1.0.0",
description="Process generic text content",
author="MarkiTect Team",
plugin_type=PluginType.PROCESSOR
)
def process(self, content: str, **kwargs) -> str:
"""Process text content."""
if kwargs.get('normalize_whitespace', False):
content = self._normalize_whitespace(content)
if kwargs.get('remove_empty_lines', False):
content = self._remove_empty_lines(content)
if kwargs.get('trim_lines', False):
content = self._trim_lines(content)
return content
def can_process(self, content: str, **kwargs) -> bool:
"""Can process any text content."""
return isinstance(content, str)
def _normalize_whitespace(self, content: str) -> str:
"""Normalize whitespace in content."""
# Replace multiple spaces with single space
content = re.sub(r' +', ' ', content)
# Replace multiple newlines with double newline
content = re.sub(r'\n\s*\n\s*\n+', '\n\n', content)
return content
def _remove_empty_lines(self, content: str) -> str:
"""Remove completely empty lines."""
lines = content.split('\n')
return '\n'.join(line for line in lines if line.strip())
def _trim_lines(self, content: str) -> str:
"""Trim whitespace from each line."""
lines = content.split('\n')
return '\n'.join(line.strip() for line in lines)