386 lines
12 KiB
Python
386 lines
12 KiB
Python
from abc import ABC, abstractmethod
|
|
from enum import Enum
|
|
from typing import Any, Callable, Optional
|
|
|
|
|
|
class EventType(Enum):
|
|
RENDER = "render"
|
|
CLICK = "click"
|
|
HOVER = "hover"
|
|
FOCUS = "focus"
|
|
|
|
|
|
class DefaultEditableTypes:
|
|
pass
|
|
|
|
|
|
class HookContext:
|
|
"""Enhanced context object passed to hook executors"""
|
|
|
|
def __init__(self, key: Any, node: Any, helper: Any, jsonviewer: Any,
|
|
json_path: str = None, parent_node: Any = None):
|
|
self.key = key
|
|
self.node = node
|
|
self.helper = helper
|
|
self.jsonviewer = jsonviewer
|
|
self.json_path = json_path or ""
|
|
self.parent_node = parent_node
|
|
self.metadata = {}
|
|
|
|
def get_node_type(self) -> str:
|
|
"""Returns string representation of node type"""
|
|
if hasattr(self.node, '__class__'):
|
|
return self.node.__class__.__name__
|
|
return type(self.node.value).__name__ if hasattr(self.node, 'value') else "unknown"
|
|
|
|
def get_value(self) -> Any:
|
|
"""Gets the actual value from the node"""
|
|
return getattr(self.node, 'value', self.node)
|
|
|
|
def is_leaf_node(self) -> bool:
|
|
"""Checks if this is a leaf node (no children)"""
|
|
return not hasattr(self.node, 'children') or not self.node.children
|
|
|
|
|
|
class Condition(ABC):
|
|
"""Base class for all conditions"""
|
|
|
|
@abstractmethod
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
pass
|
|
|
|
|
|
class WhenLongText(Condition):
|
|
"""Condition: text length > threshold"""
|
|
|
|
def __init__(self, threshold: int = 100):
|
|
self.threshold = threshold
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
value = context.get_value()
|
|
return isinstance(value, str) and len(value) > self.threshold
|
|
|
|
|
|
class WhenEditable(Condition):
|
|
"""Condition: node is editable (configurable logic)"""
|
|
|
|
def __init__(self, editable_paths: list[str] = None, editable_types: list[type] = DefaultEditableTypes):
|
|
self.editable_paths = set(editable_paths or [])
|
|
if editable_types is None:
|
|
self.editable_types = set()
|
|
else:
|
|
self.editable_types = set([str, int, float, bool] if editable_types is DefaultEditableTypes else editable_types)
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
# Check if path is in editable paths
|
|
if self.editable_paths and context.json_path in self.editable_paths:
|
|
return True
|
|
|
|
# Check if type is editable
|
|
value = context.get_value()
|
|
return type(value) in self.editable_types and context.is_leaf_node()
|
|
|
|
|
|
class WhenType(Condition):
|
|
"""Condition: node value is of specific type"""
|
|
|
|
def __init__(self, target_type: type):
|
|
self.target_type = target_type
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
value = context.get_value()
|
|
return isinstance(value, self.target_type)
|
|
|
|
|
|
class WhenKey(Condition):
|
|
"""Condition: node key matches pattern"""
|
|
|
|
def __init__(self, key_pattern: Any):
|
|
self.key_pattern = key_pattern
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
if callable(self.key_pattern):
|
|
return self.key_pattern(context.key)
|
|
return context.key == self.key_pattern
|
|
|
|
|
|
class WhenPath(Condition):
|
|
"""Condition: JSON path matches pattern"""
|
|
|
|
def __init__(self, path_pattern: str):
|
|
self.path_pattern = path_pattern
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
import re
|
|
return bool(re.match(self.path_pattern, context.json_path))
|
|
|
|
|
|
class WhenValue(Condition):
|
|
"""Condition: node value matches specific value or predicate"""
|
|
|
|
def __init__(self, target_value: Any = None, predicate: Callable[[Any], bool] = None):
|
|
if target_value is not None and predicate is not None:
|
|
raise ValueError("Cannot specify both target_value and predicate")
|
|
if target_value is None and predicate is None:
|
|
raise ValueError("Must specify either target_value or predicate")
|
|
|
|
self.target_value = target_value
|
|
self.predicate = predicate
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
value = context.get_value()
|
|
|
|
if self.predicate:
|
|
return self.predicate(value)
|
|
else:
|
|
return value == self.target_value
|
|
|
|
|
|
class CompositeCondition(Condition):
|
|
"""Allows combining conditions with AND/OR logic"""
|
|
|
|
def __init__(self, conditions: list[Condition], operator: str = "AND"):
|
|
self.conditions = conditions
|
|
self.operator = operator.upper()
|
|
|
|
def evaluate(self, context: HookContext) -> bool:
|
|
if not self.conditions:
|
|
return True
|
|
|
|
results = [condition.evaluate(context) for condition in self.conditions]
|
|
|
|
if self.operator == "AND":
|
|
return all(results)
|
|
elif self.operator == "OR":
|
|
return any(results)
|
|
else:
|
|
raise ValueError(f"Unknown operator: {self.operator}")
|
|
|
|
|
|
class Hook:
|
|
"""Represents a complete hook with event, conditions, and executor"""
|
|
|
|
def __init__(self, event_type: EventType, conditions: list[Condition],
|
|
executor: Callable, requires_modification: bool = False):
|
|
self.event_type = event_type
|
|
self.conditions = conditions
|
|
self.executor = executor
|
|
self.requires_modification = requires_modification
|
|
|
|
def matches(self, event_type: EventType, context: HookContext) -> bool:
|
|
"""Checks if this hook should be executed for given event and context"""
|
|
if self.event_type != event_type:
|
|
return False
|
|
|
|
return all(condition.evaluate(context) for condition in self.conditions)
|
|
|
|
def execute(self, context: HookContext) -> Any:
|
|
"""Executes the hook with given context"""
|
|
return self.executor(context)
|
|
|
|
|
|
class HookBuilder:
|
|
"""Builder class for creating hooks with fluent interface"""
|
|
|
|
def __init__(self):
|
|
self._event_type: Optional[EventType] = None
|
|
self._conditions: list[Condition] = []
|
|
self._executor: Optional[Callable] = None
|
|
self._requires_modification: bool = False
|
|
|
|
# Event specification methods
|
|
def on_render(self):
|
|
"""Hook will be triggered on render event"""
|
|
self._event_type = EventType.RENDER
|
|
return self
|
|
|
|
def on_click(self):
|
|
"""Hook will be triggered on click event"""
|
|
self._event_type = EventType.CLICK
|
|
return self
|
|
|
|
def on_hover(self):
|
|
"""Hook will be triggered on hover event"""
|
|
self._event_type = EventType.HOVER
|
|
return self
|
|
|
|
def on_focus(self):
|
|
"""Hook will be triggered on focus event"""
|
|
self._event_type = EventType.FOCUS
|
|
return self
|
|
|
|
# Condition methods
|
|
def when_long_text(self, threshold: int = 100):
|
|
"""Add condition: text length > threshold"""
|
|
self._conditions.append(WhenLongText(threshold))
|
|
return self
|
|
|
|
def when_editable(self, editable_paths: list[str] = None, editable_types: list[type] = None):
|
|
"""Add condition: node is editable"""
|
|
self._conditions.append(WhenEditable(editable_paths, editable_types))
|
|
return self
|
|
|
|
def when_type(self, target_type: type):
|
|
"""Add condition: node value is of specific type"""
|
|
self._conditions.append(WhenType(target_type))
|
|
return self
|
|
|
|
def when_key(self, key_pattern: Any):
|
|
"""Add condition: node key matches pattern"""
|
|
self._conditions.append(WhenKey(key_pattern))
|
|
return self
|
|
|
|
def when_path(self, path_pattern: str):
|
|
"""Add condition: JSON path matches pattern"""
|
|
self._conditions.append(WhenPath(path_pattern))
|
|
return self
|
|
|
|
def when_value(self, target_value: Any = None, predicate: Callable[[Any], bool] = None):
|
|
"""Add condition: node value matches specific value or predicate"""
|
|
self._conditions.append(WhenValue(target_value, predicate))
|
|
return self
|
|
|
|
def when_custom(self, condition):
|
|
"""Add custom condition (supports both Condition instances and predicate functions)."""
|
|
if callable(condition) and not isinstance(condition, Condition):
|
|
# Wrap the predicate function in a Condition class
|
|
class PredicateCondition(Condition):
|
|
def __init__(self, predicate):
|
|
self.predicate = predicate
|
|
|
|
def evaluate(self, context):
|
|
return self.predicate(context)
|
|
|
|
condition = PredicateCondition(condition) # Pass the function to the wrapper
|
|
|
|
elif not isinstance(condition, Condition):
|
|
raise ValueError("when_custom expects a Condition instance or a callable predicate.")
|
|
|
|
self._conditions.append(condition)
|
|
return self
|
|
|
|
def when_all(self, conditions: list[Condition]):
|
|
"""Add composite condition with AND logic"""
|
|
self._conditions.append(CompositeCondition(conditions, "AND"))
|
|
return self
|
|
|
|
def when_any(self, conditions: list[Condition]):
|
|
"""Add composite condition with OR logic"""
|
|
self._conditions.append(CompositeCondition(conditions, "OR"))
|
|
return self
|
|
|
|
# Modification flag
|
|
def requires_modification(self):
|
|
"""Indicates this hook will modify the state"""
|
|
self._requires_modification = True
|
|
return self
|
|
|
|
# Execution
|
|
def execute(self, executor: Callable) -> Hook:
|
|
"""Sets the executor function and builds the hook"""
|
|
if not self._event_type:
|
|
raise ValueError("Event type must be specified (use on_render(), on_click(), etc.)")
|
|
|
|
if not executor:
|
|
raise ValueError("Executor function must be provided")
|
|
|
|
self._executor = executor
|
|
|
|
return Hook(
|
|
event_type=self._event_type,
|
|
conditions=self._conditions,
|
|
executor=self._executor,
|
|
requires_modification=self._requires_modification
|
|
)
|
|
|
|
|
|
class HookManager:
|
|
"""Manages and executes hooks for JsonViewer"""
|
|
|
|
def __init__(self):
|
|
self.hooks: list[Hook] = []
|
|
|
|
def add_hook(self, hook: Hook):
|
|
"""Adds a hook to the manager"""
|
|
self.hooks.append(hook)
|
|
|
|
def add_hooks(self, hooks: list[Hook]):
|
|
"""Adds multiple hooks to the manager"""
|
|
self.hooks.extend(hooks)
|
|
|
|
def find_matching_hooks(self, event_type: EventType, context: HookContext) -> list[Hook]:
|
|
"""Finds all hooks that match the event and context"""
|
|
return [hook for hook in self.hooks if hook.matches(event_type, context)]
|
|
|
|
def execute_hooks(self, event_type: EventType, context: HookContext) -> list[Any]:
|
|
"""Executes all matching hooks and returns results"""
|
|
matching_hooks = self.find_matching_hooks(event_type, context)
|
|
results = []
|
|
|
|
for hook in matching_hooks:
|
|
try:
|
|
result = hook.execute(context)
|
|
results.append(result)
|
|
|
|
# If this hook requires modification, we might want to stop here
|
|
# or handle the modification differently
|
|
if hook.requires_modification:
|
|
# Could add callback to parent component here
|
|
pass
|
|
|
|
except Exception as e:
|
|
# Log error but continue with other hooks
|
|
print(f"Hook execution error: {e}")
|
|
continue
|
|
|
|
return results
|
|
|
|
def clear_hooks(self):
|
|
"""Removes all hooks"""
|
|
self.hooks.clear()
|
|
|
|
|
|
# Example usage and factory functions
|
|
def create_long_text_viewer_hook(threshold: int = 100) -> Hook:
|
|
"""Factory function for common long text viewer hook"""
|
|
|
|
def text_viewer_component(context: HookContext):
|
|
from fasthtml.components import Div, Span
|
|
|
|
value = context.get_value()
|
|
truncated = value[:threshold] + "..."
|
|
|
|
return Div(
|
|
Span(truncated, cls="text-truncated"),
|
|
Span("Click to expand", cls="expand-hint"),
|
|
cls="long-text-viewer"
|
|
)
|
|
|
|
return (HookBuilder()
|
|
.on_render()
|
|
.when_long_text(threshold)
|
|
.execute(text_viewer_component))
|
|
|
|
|
|
def create_inline_editor_hook(editable_paths: list[str] = None) -> Hook:
|
|
"""Factory function for common inline editor hook"""
|
|
|
|
def inline_editor_component(context: HookContext):
|
|
from fasthtml.components import Input, Div
|
|
|
|
value = context.get_value()
|
|
|
|
return Div(
|
|
Input(
|
|
value=str(value),
|
|
type="text" if isinstance(value, str) else "number",
|
|
cls="inline-editor"
|
|
),
|
|
cls="editable-field"
|
|
)
|
|
|
|
return (HookBuilder()
|
|
.on_click()
|
|
.when_editable(editable_paths)
|
|
.requires_modification()
|
|
.execute(inline_editor_component)) |