fixed Lua Runtime. Plugins now have priority for hooks and POST for routes if requested. also changed the CSS for main.css to color #1e1e1e for darkmode body

This commit is contained in:
2025-10-06 11:22:05 +02:00
parent 4a03e37aed
commit 06217188e1
15 changed files with 1180 additions and 1262 deletions

View File

@@ -7,18 +7,22 @@ from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from log.Logger import Logger
from .PluginFSHandler import PluginFSHandler
import json
from .luarails import guardrails_code
PLUGINS_DIR = Path(__file__).parent / "plugins"
HTML_DIR = Path(__file__).parent / "../html"
HTML_DIR = Path(__file__).parent / "../html"
MARKDOWN_DIR = Path(__file__).parent / ".." / "markdown"
class PluginManager:
def __init__(self):
self.lua = LuaRuntime(unpack_returned_tuples=True)
self.plugins = {} # name -> dict{path, lua_module, hooks, routes}
self.routes = {} # path -> (plugin_name, lua_fn)
self.hooks = {} # hook_name -> list of (plugin_name, lua_fn)
self.routes = {
"GET": {}, # path -> list of (plugin_name, lua_fn, priority)
"POST": {} # path -> list of (plugin_name, lua_fn, priority)
}
self.hooks = {} # hook_name -> list of (plugin_name, lua_fn, priority)
# Create directories
PLUGINS_DIR.mkdir(exist_ok=True)
@@ -32,55 +36,76 @@ class PluginManager:
def _setup_lua_globals(self):
"""Set up all Lua global functions and guardrails"""
# plugin_manager.py
from .Actions import Actions
g = self.lua.globals()
self.actions = Actions()
# Route and hook registration
# Route and hook registration with priority support
g.add_route = self._expose_add_route
g.add_get_route = lambda path, fn, priority=50: self._expose_add_route_method(path, fn, "GET", priority)
g.add_post_route = lambda path, fn, priority=50: self._expose_add_route_method(path, fn, "POST", priority)
g.register_hook = self._expose_register_hook
# Logging - using custom Logger
# TODO: With Logger do custom Plugin Loading
# Logging
g.log = lambda msg: Logger.log_lua_info(f"PLUGIN => {msg}")
g.log_warn = lambda msg: Logger.log_lua_warning(f"PLUGIN => {msg}")
g.log_lua_error = lambda msg: Logger.log_lua_error(f"PLUGIN => {msg}")
g.log_error = lambda msg: Logger.log_lua_error(f"PLUGIN => {msg}")
self.actions = Actions
# File operations
g.read_file = self.actions._safe_read_file
g.write_file = self.actions._safe_write_file
g.file_exists = self.actions._file_exists
g.file_size = self.actions._file_size
# Convert Python list to Lua table
g.list_directory = lambda path: self.lua.table_from(self.actions._list_directory(path))
# HTML
# HTML operations
g.read_html = lambda fn: self.actions._read_content(self.actions.html_dir, fn)
g.write_html = lambda fn, c: self.actions._write_content(self.actions.html_dir, fn, c)
g.list_html_files = lambda: self.actions._list_files(self.actions.html_dir, ".html")
# Convert Python list to Lua table so # operator works
g.list_html_files = lambda: self.lua.table_from(self.actions._list_files(self.actions.html_dir, ".html"))
# Markdown
# Markdown operations
g.read_markdown = lambda fn: self.actions._read_content(self.actions.markdown_dir, fn)
g.write_markdown = lambda fn, c: self.actions._write_content(self.actions.markdown_dir, fn, c)
g.list_markdown_files = lambda: self.actions._list_files(self.actions.markdown_dir, ".md")
# Convert Python list to Lua table so # operator works
g.list_markdown_files = lambda: self.lua.table_from(self.actions._list_files(self.actions.markdown_dir, ".md"))
# HTML helpers
# HTML manipulation helpers
g.html_find_tag = self.actions._html_find_tag
g.html_replace_tag = self.actions._html_replace_tag
g.html_insert_before = self.actions._html_insert_before
g.html_insert_after = self.actions._html_insert_after
g.html_wrap_content = self.actions._html_wrap_content
g.html_remove_tag = self.actions._html_remove_tag
g.html_add_class = self.actions._html_add_class
g.html_add_attribute = self.actions._html_add_attribute
# Markdown helpers
# Markdown manipulation helpers
g.md_add_header = self.actions._md_add_header
g.md_replace_section = self.actions._md_replace_section
g.md_append_content = self.actions._md_append_content
g.md_prepend_content = self.actions._md_prepend_content
g.md_insert_at_position = self.actions._md_insert_at_position
g.md_find_section = self.actions._md_find_section
g.md_remove_section = self.actions._md_remove_section
g.md_add_list_item = self.actions._md_add_list_item
g.md_wrap_code_block = self.actions._md_wrap_code_block
# JSON
# JSON helpers
g.table_to_json = self.actions._table_to_json
g.json_to_table = self.actions._json_to_table
g.json_parse = self.actions._json_parse
g.json_stringify = self.actions._json_stringify
# String utilities - also convert list results to Lua tables
g.string_split = lambda text, delim: self.lua.table_from(self.actions._string_split(text, delim))
g.string_join = self.actions._string_join
g.string_replace = self.actions._string_replace
g.string_match = self.actions._string_match
g.string_match_all = lambda text, pattern: self.lua.table_from(self.actions._string_match_all(text, pattern))
# Guardrails - predefined safe patterns
self._setup_lua_guardrails()
@@ -91,7 +116,6 @@ class PluginManager:
except LuaError as e:
Logger.log_lua_error(f"Failed to initialize Lua guardrails: {e}")
""" Lifecycle of Plugin """
def load_all(self):
for p in PLUGINS_DIR.glob("*.lua"):
@@ -123,17 +147,21 @@ class PluginManager:
else:
Logger.log_lua_warning(f"Tried to reload {name}, but file no longer exists")
def unload_plugin(self, name: str):
# Remove routes/hook registrations from this plugin
to_remove_routes = [r for r, v in self.routes.items() if v[0] == name]
for r in to_remove_routes:
del self.routes[r]
# Remove routes from this plugin (both GET and POST)
for method in ["GET", "POST"]:
for route_path in list(self.routes[method].keys()):
self.routes[method][route_path] = [
x for x in self.routes[method][route_path] if x[0] != name
]
if not self.routes[method][route_path]:
del self.routes[method][route_path]
for hook, lst in list(self.hooks.items()):
self.hooks[hook] = [x for x in lst if x[0] != name]
if not self.hooks[hook]:
del self.hooks[hook]
# Remove hooks from this plugin
for hook_name in list(self.hooks.keys()):
self.hooks[hook_name] = [x for x in self.hooks[hook_name] if x[0] != name]
if not self.hooks[hook_name]:
del self.hooks[hook_name]
if name in self.plugins:
del self.plugins[name]
@@ -141,55 +169,126 @@ class PluginManager:
else:
Logger.log_lua_warning(f"Tried to unload {name}, but it was not loaded")
""" Expose API routes """
def _expose_add_route(self, path, lua_fn, priority=50):
"""Legacy: Called from Lua as add_route(path, function(req) ... end) - defaults to GET"""
return self._expose_add_route_method(path, lua_fn, "GET", priority)
""" Expose a new API route """
def _expose_add_route(self, path, lua_fn):
"""Called from Lua as add_route(path, function(req) ... end)"""
def _expose_add_route_method(self, path, lua_fn, method="GET", priority=50):
"""
Register a route with a specific HTTP method and priority.
Lower priority numbers run first (0 = highest priority).
"""
p = str(path)
plugin_name = self._current_loading_plugin_name()
if not plugin_name:
plugin_name = "<unknown>"
self.routes[p] = (plugin_name, lua_fn)
Logger.log_lua_info(f"Plugin {plugin_name} registered route {p}")
m = str(method).upper()
plugin_name = self._current_loading_plugin_name() or "<unknown>"
if m not in self.routes:
self.routes[m] = {}
if p not in self.routes[m]:
self.routes[m][p] = []
# Add route with priority
self.routes[m][p].append((plugin_name, lua_fn, int(priority)))
# Sort by priority (lower number = higher priority)
self.routes[m][p].sort(key=lambda x: x[2])
Logger.log_lua_info(f"Plugin {plugin_name} registered {m} route {p} (priority: {priority})")
def _expose_register_hook(self, hook_name, lua_fn):
def _expose_register_hook(self, hook_name, lua_fn, priority=50):
"""
Register a hook with priority support.
Hooks are chained - each hook receives the output of the previous one.
Lower priority numbers run first (0 = highest priority).
"""
hook = str(hook_name)
plugin_name = self._current_loading_plugin_name() or "<unknown>"
self.hooks.setdefault(hook, []).append((plugin_name, lua_fn))
Logger.log_lua_info(f"Plugin {plugin_name} registered hook {hook}")
if hook not in self.hooks:
self.hooks[hook] = []
self.hooks[hook].append((plugin_name, lua_fn, int(priority)))
# Sort by priority (lower number = higher priority)
self.hooks[hook].sort(key=lambda x: x[2])
Logger.log_lua_info(f"Plugin {plugin_name} registered hook {hook} (priority: {priority})")
def _current_loading_plugin_name(self):
return getattr(self, "_current_plugin", None)
""" Running hooks & handling routes """
def handle_request(self, path, request_info=None):
"""If a plugin registered a route for this path, call it and return (status, headers, body) or raw str."""
if path in self.routes:
plugin_name, lua_fn = self.routes[path]
def handle_request(self, path, request_info=None, method="GET"):
"""
Handle HTTP requests by calling registered plugin routes.
First matching route that returns a response wins.
"""
method = method.upper()
if method not in self.routes:
return None
if path not in self.routes[method]:
return None
# Try each registered handler in priority order
for plugin_name, lua_fn, priority in self.routes[method][path]:
try:
lua_req = request_info or {}
res = lua_fn(lua_req)
# If handler returns None, try next handler
if res is None:
continue
# Handle tuple response (status, headers, body)
if isinstance(res, tuple) and len(res) == 3:
return res
# Handle string response
return (200, {"Content-Type": "text/html"}, str(res))
except LuaError as e:
Logger.log_lua_error(f"Lua error in route {path}: {e}")
return (500, {"Content-Type": "text/plain"}, f"Plugin error: {e}")
Logger.log_lua_error(f"Lua error in {method} route {path} from {plugin_name}: {e}")
# Continue to next handler instead of failing completely
continue
return None
def run_hook(self, hook_name: str, *args):
"""Run all registered hook functions for hook_name; return last non-None return value."""
"""
Run all registered hook functions for hook_name in priority order.
Each hook receives the output of the previous hook (chain pattern).
This allows multiple plugins to modify content without overwriting each other.
Example:
Initial content -> Plugin A modifies -> Plugin B modifies -> Final content
"""
if hook_name not in self.hooks:
return None
last = None
for plugin_name, fn in list(self.hooks[hook_name]):
# Start with the initial input (first arg for content hooks)
result = args[0] if args else None
for plugin_name, fn, priority in list(self.hooks[hook_name]):
try:
out = fn(*args)
if out is not None:
last = out
# Pass the current result as the first argument, plus any other args
hook_args = (result,) + args[1:] if len(args) > 1 else (result,)
output = fn(*hook_args)
# If hook returns something, use it as input for next hook
if output is not None:
result = output
Logger.log_lua_debug(f"Hook {hook_name} from {plugin_name} modified content")
except LuaError as e:
Logger.log_lua_error(f"Lua error in hook {hook_name} from {plugin_name}: {e}")
return last
# Continue with current result even if this hook fails
continue
return result
""" File watcher """
def _start_watcher(self):