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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user