diff --git a/.vscode/globals.lua b/.vscode/globals.lua new file mode 100644 index 0000000..d68ed16 --- /dev/null +++ b/.vscode/globals.lua @@ -0,0 +1,184 @@ +-- GLOBAL FUNCTION STUBS FOR VS CODE INTELLISENSE + +-- Routing / Hooking +---@param path string +---@param callback function +---@param priority number? optional +function add_route(path, callback, priority) end + +---@param path string +---@param callback function +---@param priority number? optional +function add_get_route(path, callback, priority) end + +---@param path string +---@param callback function +---@param priority number? optional +function add_post_route(path, callback, priority) end + +---@param hook_name string +---@param callback function +---@param priority number? optional +function register_hook(hook_name, callback, priority) end + +-- Logging +---@param msg string +function log(msg) end + +-- File Operations +---@return string[] +function list_html_files() end + +---@return string[] +function list_markdown_files() end + +---@param filename string +---@param content string +---@return boolean +function write_markdown(filename, content) end + +---@param path string +---@return boolean +function file_exists(path) end + +---@param path string +---@return integer +function file_size(path) end + +---@param path string +---@return string[] +function list_directory(path) end + +-- HTML Utilities +---@param html string +---@param tag string +---@param class_name string +---@return string +function html_add_class(html, tag, class_name) end + +---@param html string +---@param tag string +---@param attr_name string +---@param attr_value string +---@return string +function html_add_attribute(html, tag, attr_name, attr_value) end + +---@param html string +---@param marker string +---@param content string +---@return string +function html_insert_before(html, marker, content) end + +---@param html string +---@param marker string +---@param content string +---@return string +function html_insert_after(html, marker, content) end + +---@param html string +---@param tag string +---@param wrapper_tag string +---@param attrs string +---@return string +function html_wrap_content(html, tag, wrapper_tag, attrs) end + +---@param html string +---@param tag string +---@param keep_content boolean +---@return string +function html_remove_tag(html, tag, keep_content) end + +-- Markdown Utilities +---@param markdown string +---@param level integer +---@param text string +---@return string +function md_add_header(markdown, level, text) end + +---@param markdown string +---@param header string +---@param new_content string +---@return string +function md_replace_section(markdown, header, new_content) end + +---@param markdown string +---@param content string +---@return string +function md_append_content(markdown, content) end + +---@param markdown string +---@param content string +---@return string +function md_prepend_content(markdown, content) end + +---@param markdown string +---@param position integer +---@param content string +---@return string +function md_insert_at_position(markdown, position, content) end + +---@param markdown string +---@param header string +---@return string? +function md_find_section(markdown, header) end + +---@param markdown string +---@param header string +---@return string +function md_remove_section(markdown, header) end + +---@param markdown string +---@param item string +---@param ordered boolean +---@return string +function md_add_list_item(markdown, item, ordered) end + +---@param markdown string +---@param language string +---@return string +function md_wrap_code_block(markdown, language) end + +-- JSON Utilities +---@param lua_table any +---@return string +function table_to_json(lua_table) end + +---@param json_str string +---@return table +function json_to_table(json_str) end + +---@param json_str string +---@return table +function json_parse(json_str) end + +---@param data any +---@return string +function json_stringify(data) end + +-- String Utilities +---@param text string +---@param delimiter string +---@return string[] +function string_split(text, delimiter) end + +---@param items string[] +---@param delimiter string +---@return string +function string_join(items, delimiter) end + +---@param text string +---@param old string +---@param new string +---@return string +function string_replace(text, old, new) end + +---@param text string +---@param pattern string +---@return string? +function string_match(text, pattern) end + +---@param text string +---@param pattern string +---@return string[] +function string_match_all(text, pattern) end + diff --git a/.vscode/settings.json b/.vscode/settings.json index 174cf73..4cc2ad4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,27 +1,66 @@ { - "Lua.diagnostics.globals": [ - "read_file", - "write_file", - "read_html", - "write_html", - "list_html_files", - "read_markdown", - "write_markdown", - "list_markdown_files", - "html_find_tag", - "html_replace_tag", - "html_insert_before", - "html_insert_after", - "html_wrap_content", - "md_add_header", - "md_replace_section", - "md_append_content", - "table_to_json", - "json_to_table", - "add_route", - "register_hook", - "log", - "log_warn", - "log_error" - ] + // Lua Language Server settings + "Lua.runtime.version": "Lua 5.4", + "Lua.workspace.checkThirdParty": false, + "Lua.workspace.library": [ + "./.vscode" + ], + "Lua.hint.enable": true, + "Lua.completion.callSnippet": "Both", + "Lua.completion.autoRequire": false, + + // Diagnostics: treat these globals as known + "Lua.diagnostics.globals": [ + "add_route", + "add_get_route", + "add_post_route", + "register_hook", + "list_html_files", + "list_markdown_files", + "write_markdown", + "html_add_class", + "html_add_attribute", + "html_insert_before", + "html_insert_after", + "html_wrap_content", + "html_remove_tag", + "md_add_header", + "md_replace_section", + "md_append_content", + "md_prepend_content", + "md_insert_at_position", + "md_find_section", + "md_remove_section", + "md_add_list_item", + "md_wrap_code_block", + "table_to_json", + "json_to_table", + "json_parse", + "json_stringify", + "string_split", + "string_join", + "string_replace", + "string_match", + "string_match_all", + "file_exists", + "file_size", + "list_directory", + "log" + ], + + // Formatting + "Lua.format.enable": true, + "Lua.format.defaultConfig": { + "indent_style": "space", + "indent_size": "2" + }, + + // Disable warnings about lowercase globals + "Lua.diagnostics.disable": [ + "lowercase-global" + ], + + // Optional + "Lua.telemetry.enable": false } + diff --git a/css/main.css b/css/main.css index a7d6393..5dd6ccf 100644 --- a/css/main.css +++ b/css/main.css @@ -4,7 +4,7 @@ body { font-size: clamp(14px, 2.5vw, 16px); /* scales from 14px to 16px */ line-height: 1.5; margin: 1em; - color: #000; + color: #1e1e1e; background: #fff; } @@ -84,7 +84,7 @@ table { font-size: clamp(0.85rem, 2vw, 1rem); /* slightly smaller on mobile */ } th, td { - border: 1px solid #000; + border: 1px solid #1e1e1e; padding: 0.5em; text-align: left; } @@ -215,7 +215,7 @@ button:hover { } .html-content th, .html-content td { - border: 1px solid #000; + border: 1px solid #1e1e1e; padding: 0.5em 0.75em; } .html-content th { @@ -349,7 +349,7 @@ pre[class*="language-"] { /* Dark mode */ body.dark-mode { - background: #121212; + background: #1e1e1e; /* before this was #121212 or something, which wasnt blending smooth with the indexer page*/ color: #e0e0e0; } diff --git a/log/Logger.py b/log/Logger.py index 50000ac..4e2113d 100644 --- a/log/Logger.py +++ b/log/Logger.py @@ -54,4 +54,10 @@ class Logger: now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) print(f"{colorama.Fore.MAGENTA}[ LUA_WARNING@{now} ]: {message}{colorama.Style.RESET_ALL}") + @staticmethod + def log_lua_debug(message: str) -> None: + now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + print(f"{colorama.Fore.MAGENTA}[ LUA_DEBUG@{now} ]: {message}{colorama.Style.RESET_ALL}") + + diff --git a/lua/Actions.py b/lua/Actions.py index 638705b..6de0325 100644 --- a/lua/Actions.py +++ b/lua/Actions.py @@ -1,12 +1,25 @@ from pathlib import Path from log import Logger import re +import json 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 Actions: + def __init__(self): + """Initialize Actions with directory paths""" + self.plugins_dir = PLUGINS_DIR + self.html_dir = HTML_DIR + self.markdown_dir = MARKDOWN_DIR + + # Ensure directories exist + self.plugins_dir.mkdir(exist_ok=True) + self.html_dir.mkdir(exist_ok=True) + self.markdown_dir.mkdir(exist_ok=True) + + # File I/O Operations def _safe_read_file(self, path): """Safe file reading with path validation""" try: @@ -18,7 +31,7 @@ class Actions: except Exception as e: Logger.log_lua_error(f"Error reading file {path}: {e}") return None - @staticmethod + def _safe_write_file(self, path, content): """Safe file writing with path validation""" try: @@ -32,8 +45,7 @@ class Actions: Logger.log_lua_error(f"Error writing file {path}: {e}") return False - # HTML/Markdown content operations - @staticmethod + # HTML/Markdown Content Operations def _read_content(self, base_dir, filename): """Read content from HTML or Markdown directory""" try: @@ -47,8 +59,6 @@ class Actions: Logger.log_lua_error(f"Error reading {filename}: {e}") return None - - @staticmethod def _write_content(self, base_dir, filename, content): """Write content to HTML or Markdown directory""" try: @@ -61,7 +71,6 @@ class Actions: Logger.log_lua_error(f"Error writing {filename}: {e}") return False - @staticmethod def _list_files(self, base_dir, extension): """List files with given extension""" try: @@ -70,31 +79,26 @@ class Actions: Logger.log_lua_error(f"Error listing files: {e}") return [] - # HTML manipulation helpers - @staticmethod + # HTML Manipulation Helpers def _html_find_tag(self, html, tag): """Find first occurrence of HTML tag""" pattern = f"<{tag}[^>]*>.*?" match = re.search(pattern, html, re.DOTALL | re.IGNORECASE) return match.group(0) if match else None - @staticmethod def _html_replace_tag(self, html, tag, new_content): """Replace HTML tag content""" pattern = f"(<{tag}[^>]*>).*?()" return re.sub(pattern, f"\\1{new_content}\\2", html, flags=re.DOTALL | re.IGNORECASE) - @staticmethod def _html_insert_before(self, html, marker, content): """Insert content before a marker""" return html.replace(marker, content + marker) - @staticmethod def _html_insert_after(self, html, marker, content): """Insert content after a marker""" return html.replace(marker, marker + content) - @staticmethod def _html_wrap_content(self, html, tag, wrapper_tag, attrs=""): """Wrap tag content with another tag""" pattern = f"(<{tag}[^>]*>)(.*?)()" @@ -102,45 +106,185 @@ class Actions: open_tag, content, close_tag = match.groups() return f"{open_tag}<{wrapper_tag} {attrs}>{content}{close_tag}" return re.sub(pattern, replacer, html, flags=re.DOTALL | re.IGNORECASE) + + def _html_remove_tag(self, html, tag, keep_content=True): + """Remove HTML tag, optionally keeping its content""" + if keep_content: + pattern = f"<{tag}[^>]*>(.*?)" + return re.sub(pattern, r"\1", html, flags=re.DOTALL | re.IGNORECASE) + else: + pattern = f"<{tag}[^>]*>.*?" + return re.sub(pattern, "", html, flags=re.DOTALL | re.IGNORECASE) + + def _html_add_class(self, html, tag, class_name): + """Add a CSS class to all instances of a tag""" + def add_class_to_tag(match): + tag_content = match.group(0) + if 'class=' in tag_content: + return re.sub( + r'class="([^"]*)"', + f'class="\\1 {class_name}"', + tag_content + ) + else: + return tag_content.replace('>', f' class="{class_name}">', 1) + + pattern = f"<{tag}[^>]*>" + return re.sub(pattern, add_class_to_tag, html, flags=re.IGNORECASE) + + def _html_add_attribute(self, html, tag, attr_name, attr_value): + """Add an attribute to all instances of a tag""" + def add_attr_to_tag(match): + tag_content = match.group(0) + if attr_name in tag_content: + return tag_content # Don't duplicate + return tag_content.replace('>', f' {attr_name}="{attr_value}">', 1) + + pattern = f"<{tag}[^>]*>" + return re.sub(pattern, add_attr_to_tag, html, flags=re.IGNORECASE) - # Markdown manipulation helpers - @staticmethod + # Markdown Manipulation Helpers def _md_add_header(self, markdown, level, text): """Add header to markdown""" prefix = "#" * level return f"{prefix} {text}\n\n{markdown}" - @staticmethod def _md_replace_section(self, markdown, header, new_content): """Replace markdown section""" # Find section starting with header pattern = f"(#{1,6}\\s+{re.escape(header)}.*?)(?=#{1,6}\\s+|$)" return re.sub(pattern, f"## {header}\n\n{new_content}\n\n", markdown, flags=re.DOTALL) - @staticmethod def _md_append_content(self, markdown, content): """Append content to markdown""" return markdown.rstrip() + "\n\n" + content + + def _md_prepend_content(self, markdown, content): + """Prepend content to markdown""" + return content + "\n\n" + markdown.lstrip() + + def _md_insert_at_position(self, markdown, position, content): + """Insert content at specific line position""" + lines = markdown.split('\n') + if 0 <= position <= len(lines): + lines.insert(position, content) + return '\n'.join(lines) + + def _md_find_section(self, markdown, header): + """Find a markdown section and return its content""" + pattern = f"#{1,6}\\s+{re.escape(header)}\\s*\n(.*?)(?=#{1,6}\\s+|$)" + match = re.search(pattern, markdown, re.DOTALL) + return match.group(1).strip() if match else None + + def _md_remove_section(self, markdown, header): + """Remove a markdown section""" + pattern = f"#{1,6}\\s+{re.escape(header)}.*?(?=#{1,6}\\s+|$)" + return re.sub(pattern, "", markdown, flags=re.DOTALL).strip() + + def _md_add_list_item(self, markdown, item, ordered=False): + """Add an item to the end of markdown (as list item)""" + prefix = "1. " if ordered else "- " + return markdown.rstrip() + f"\n{prefix}{item}\n" + + def _md_wrap_code_block(self, markdown, language=""): + """Wrap content in a markdown code block""" + return f"```{language}\n{markdown}\n```" - # JSON conversion helpers - @staticmethod + # JSON Conversion Helpers def _table_to_json(self, lua_table): """Convert Lua table to JSON string""" - import json try: - # Convert lupa table to Python dict - py_dict = dict(lua_table) - return json.dumps(py_dict) + # Handle lupa tables by converting to dict + if hasattr(lua_table, 'items'): + py_dict = dict(lua_table.items()) + elif hasattr(lua_table, '__iter__') and not isinstance(lua_table, str): + # Handle array-like lua tables + py_dict = list(lua_table) + else: + py_dict = dict(lua_table) + + return json.dumps(py_dict, indent=2, ensure_ascii=False) except Exception as e: Logger.log_lua_error(f"Error converting table to JSON: {e}") return "{}" - @staticmethod def _json_to_table(self, json_str): - """Convert JSON string to Lua table""" - import json + """Convert JSON string to Lua table (Python dict)""" try: return json.loads(json_str) except Exception as e: Logger.log_lua_error(f"Error parsing JSON: {e}") return {} + + def _json_parse(self, json_str): + """Alias for _json_to_table""" + return self._json_to_table(json_str) + + def _json_stringify(self, data): + """Convert Python object to JSON string""" + try: + return json.dumps(data, indent=2, ensure_ascii=False) + except Exception as e: + Logger.log_lua_error(f"Error stringifying to JSON: {e}") + return "{}" + + # Utility Functions + def _string_split(self, text, delimiter): + """Split string by delimiter""" + return text.split(delimiter) + + def _string_join(self, items, delimiter): + """Join list of strings with delimiter""" + try: + return delimiter.join(str(item) for item in items) + except Exception as e: + Logger.log_lua_error(f"Error joining strings: {e}") + return "" + + def _string_replace(self, text, old, new): + """Replace all occurrences of old with new""" + return text.replace(old, new) + + def _string_match(self, text, pattern): + """Match string against regex pattern""" + try: + match = re.search(pattern, text) + return match.group(0) if match else None + except Exception as e: + Logger.log_lua_error(f"Error in regex match: {e}") + return None + + def _string_match_all(self, text, pattern): + """Find all matches of pattern in text""" + try: + return re.findall(pattern, text) + except Exception as e: + Logger.log_lua_error(f"Error in regex findall: {e}") + return [] + + def _file_exists(self, path): + """Check if file exists""" + try: + return Path(path).exists() + except Exception as e: + Logger.log_lua_error(f"Error checking file existence: {e}") + return False + + def _file_size(self, path): + """Get file size in bytes""" + try: + return Path(path).stat().st_size + except Exception as e: + Logger.log_lua_error(f"Error getting file size: {e}") + return 0 + + def _list_directory(self, path): + """List all files and directories in path""" + try: + p = Path(path) + if not p.exists(): + return [] + return [item.name for item in p.iterdir()] + except Exception as e: + Logger.log_lua_error(f"Error listing directory: {e}") + return [] diff --git a/lua/plugin_manager.py b/lua/plugin_manager.py index f2a3892..7e32357 100644 --- a/lua/plugin_manager.py +++ b/lua/plugin_manager.py @@ -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 = "" - 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 "" + + 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 "" - 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): diff --git a/lua/plugins/demo/demo_route.lua b/lua/plugins/examples/demo/demo_route.lua similarity index 100% rename from lua/plugins/demo/demo_route.lua rename to lua/plugins/examples/demo/demo_route.lua diff --git a/lua/plugins/demo/generatemd.lua b/lua/plugins/examples/demo/generatemd.lua similarity index 100% rename from lua/plugins/demo/generatemd.lua rename to lua/plugins/examples/demo/generatemd.lua diff --git a/lua/plugins/demo/hello.lua b/lua/plugins/examples/demo/hello.lua similarity index 100% rename from lua/plugins/demo/hello.lua rename to lua/plugins/examples/demo/hello.lua diff --git a/lua/plugins/demo/md_banner.lua b/lua/plugins/examples/demo/md_banner.lua similarity index 100% rename from lua/plugins/demo/md_banner.lua rename to lua/plugins/examples/demo/md_banner.lua diff --git a/lua/plugins/demo/return_available_routes.lua b/lua/plugins/examples/demo/return_available_routes.lua similarity index 100% rename from lua/plugins/demo/return_available_routes.lua rename to lua/plugins/examples/demo/return_available_routes.lua diff --git a/lua/plugins/examples/demo/test.lua b/lua/plugins/examples/demo/test.lua new file mode 100644 index 0000000..e1aaa81 --- /dev/null +++ b/lua/plugins/examples/demo/test.lua @@ -0,0 +1,530 @@ + +log("Loading Comprehensive Example Plugin...") + +-- ============================================================================ +-- PART 1: HOOK CHAINING DEMONSTRATION +-- Multiple hooks that modify HTML content sequentially +-- ============================================================================ + +-- Hook 1: Add custom CSS (Priority 10 - runs first) +register_hook("post_render", function(filepath, html_content) + log("Hook 1 (Priority 10): Adding custom CSS to " .. filepath) + + local custom_css = [[ + + ]] + + -- Insert CSS before closing head tag + local modified = html_insert_before(html_content, "", custom_css) + return modified -- Pass to next hook +end, 10) + +-- Hook 2: Add banner (Priority 20 - runs second) +register_hook("post_render", function(filepath, html_content) + log("Hook 2 (Priority 20): Adding banner") + + local banner = [[ +
+ 🚀 Enhanced by Plugin System | + Processing: ]] .. filepath .. [[ +
+ ]] + + local modified = html_insert_after(html_content, "", banner) + return modified +end, 20) + +-- Hook 3: Enhance code blocks (Priority 30 - runs third) +register_hook("post_render", function(filepath, html_content) + log("Hook 3 (Priority 30): Enhancing code blocks") + + -- Add class to all code tags + local modified = html_add_class(html_content, "code", "enhanced-code") + modified = html_add_attribute(modified, "pre", "data-enhanced", "true") + + return modified +end, 30) + +-- Hook 4: Add footer (Priority 40 - runs last) +register_hook("post_render", function(filepath, html_content) + log("Hook 4 (Priority 40): Adding footer") + + local footer = [[ + + ]] + + local modified = html_insert_before(html_content, "", footer) + return modified +end, 40) + +-- ============================================================================ +-- PART 2: GET ROUTES +-- Demonstrate various GET route patterns +-- ============================================================================ + +-- Simple text response +add_get_route("/plugin/hello", function(req) + return "

Hello from Plugin!

This is a simple GET route.

" +end, 50) + +-- JSON API endpoint +add_get_route("/plugin/api/info", function(req) + local info = { + plugin_name = "Comprehensive Example", + version = "1.0.0", + features = {"hooks", "routes", "file_ops"}, + html_files = #list_html_files(), + markdown_files = #list_markdown_files(), + timestamp = os.date("%Y-%m-%d %H:%M:%S") + } + + return 200, + {["Content-Type"] = "application/json"}, + table_to_json(info) +end, 50) + +-- File listing endpoint +add_get_route("/plugin/files", function(req) + local html_files = list_html_files() + local md_files = list_markdown_files() + + local html = [[ + + + + File Browser + + + +

📁 File Browser

+ +
+

HTML Files (]] .. #html_files .. [[)

+]] + + for i, file in ipairs(html_files) do + html = html .. '
📄 ' .. file .. '
\n' + end + + html = html .. [[ +
+ +
+

Markdown Files (]] .. #md_files .. [[)

+]] + + for i, file in ipairs(md_files) do + html = html .. '
📝 ' .. file .. '
\n' + end + + html = html .. [[ +
+ + +]] + + return html +end, 50) + +-- ============================================================================ +-- PART 3: POST ROUTES +-- Handle form submissions and API requests +-- ============================================================================ + +-- Contact form submission +add_post_route("/plugin/api/contact", function(req) + local data = req.data or {} + + log("Contact form received:") + log(" Name: " .. (data.name or "N/A")) + log(" Email: " .. (data.email or "N/A")) + log(" Message: " .. (data.message or "N/A")) + + -- Validation + if not data.name or data.name == "" then + return 400, + {["Content-Type"] = "application/json"}, + table_to_json({success = false, error = "Name is required"}) + end + + if not data.message or data.message == "" then + return 400, + {["Content-Type"] = "application/json"}, + table_to_json({success = false, error = "Message is required"}) + end + + -- Save to file (example) + local timestamp = os.date("%Y-%m-%d %H:%M:%S") + local submission = string_join({ + "---", + "Name: " .. data.name, + "Email: " .. (data.email or "N/A"), + "Time: " .. timestamp, + "Message:", + data.message, + "---", + "" + }, "\n") + + -- Note: In production, you'd want better file handling + -- write_file("submissions.txt", submission) + + local response = { + success = true, + message = "Thank you for your submission!", + received_at = timestamp + } + + return 200, + {["Content-Type"] = "application/json"}, + table_to_json(response) +end, 50) + +-- File upload/create endpoint +add_post_route("/plugin/api/create-note", function(req) + local data = req.data or {} + local title = data.title or "Untitled" + local content = data.content or "" + + if content == "" then + return 400, + {["Content-Type"] = "application/json"}, + table_to_json({success = false, error = "Content cannot be empty"}) + end + + -- Create markdown file + local filename = string_replace(title, " ", "-") .. ".md" + filename = string.lower(filename) + + local markdown_content = md_add_header("", 1, title) + markdown_content = md_append_content(markdown_content, content) + markdown_content = md_append_content(markdown_content, + "\n---\n*Created by plugin at " .. os.date("%Y-%m-%d %H:%M:%S") .. "*") + + -- Write the file + local success = write_markdown(filename, markdown_content) + + if success then + log("Created new markdown file: " .. filename) + return 200, + {["Content-Type"] = "application/json"}, + table_to_json({ + success = true, + message = "Note created successfully", + filename = filename + }) + else + return 500, + {["Content-Type"] = "application/json"}, + table_to_json({ + success = false, + error = "Failed to create file" + }) + end +end, 50) + +-- ============================================================================ +-- PART 4: DEMONSTRATION DASHBOARD +-- A full-featured page showing all capabilities +-- ============================================================================ + +add_get_route("/plugin/dashboard", function(req) + local html_count = #list_html_files() + local md_count = #list_markdown_files() + + local html = [[ + + + + Plugin Dashboard + + + +
+

🔌 PyPost Plugin Dashboard

+

Comprehensive Plugin System Demonstration

+
+ +
+ +
+
+

📄 HTML Files

+
]] .. html_count .. [[
+

Generated HTML documents

+
+ +
+

📝 Markdown Files

+
]] .. md_count .. [[
+

Source markdown files

+
+ +
+

⏰ Server Time

+
]] .. os.date("%H:%M") .. [[
+

]] .. os.date("%Y-%m-%d") .. [[

+
+
+ + +
+

🚀 Plugin Features

+
    +
  • Hook Chaining: Multiple plugins modify content sequentially
  • +
  • Priority System: Control execution order (0-100)
  • +
  • GET Routes: Serve custom pages and APIs
  • +
  • POST Routes: Handle form submissions
  • +
  • HTML Manipulation: Find, replace, insert, wrap elements
  • +
  • Markdown Operations: Add headers, sections, list items
  • +
  • File I/O: Read, write, list files safely
  • +
  • JSON Support: Parse and stringify data
  • +
  • Hot Reloading: Plugins reload automatically on changes
  • +
+
+ + +
+

🌐 Available API Endpoints

+

GET /plugin/hello - Simple greeting

+

GET /plugin/api/info - JSON plugin info

+

GET /plugin/files - File browser

+

GET /plugin/dashboard - This page

+

POST /plugin/api/contact - Submit contact form

+

POST /plugin/api/create-note - Create markdown note

+
+ View Files + API Info (JSON) +
+ + +
+

📬 Test POST Route - Contact Form

+
+ + + + +
+
+
+ + +
+

📝 Test POST Route - Create Markdown Note

+
+ + + +
+
+
+ + +
+

🔗 Hook Chain Processing Order

+

When a markdown file is rendered, these hooks process the HTML sequentially:

+
+ 1️⃣ Add CSS (Priority 10) + + 2️⃣ Add Banner (Priority 20) + + 3️⃣ Enhance Code (Priority 30) + + 4️⃣ Add Footer (Priority 40) +
+

+ Each hook receives the output from the previous hook, ensuring no modifications are lost. +

+
+ + +
+

💻 Example Plugin Code

+
-- Register a hook with priority
+register_hook("post_render", function(filepath, html)
+    log("Processing: " .. filepath)
+    local modified = html_insert_after(html, "<body>", 
+        "<div class='banner'>Hello!</div>")
+    return modified  -- Chain to next hook
+end, 20)
+
+-- Register a POST route
+add_post_route("/api/submit", function(req)
+    local data = req.data
+    log("Received: " .. data.name)
+    
+    return 200,
+           {["Content-Type"] = "application/json"},
+           table_to_json({success = true})
+end, 50)
+
+
+ + + + +]] + + return html +end, 50) + +-- ============================================================================ +-- PART 5: UTILITY DEMONSTRATIONS +-- Show string and file utilities +-- ============================================================================ + +-- String manipulation API +add_get_route("/plugin/api/string-demo", function(req) + local text = "Hello, World! This is a test." + + local demo = { + original = text, + split = string_split(text, " "), + replaced = string_replace(text, "World", "PyPost"), + uppercase = string.upper(text), + lowercase = string.lower(text), + match = string_match(text, "w+"), + all_words = string_match_all(text, "%w+") + } + + return 200, + {["Content-Type"] = "application/json"}, + table_to_json(demo) +end, 50) + +-- ============================================================================ +-- INITIALIZATION LOG +-- ============================================================================ + +log("========================================") +log("Comprehensive Plugin Loaded Successfully!") +log("========================================") +log("Registered Hooks:") +log(" - post_render (4 hooks with priorities 10, 20, 30, 40)") +log("Registered GET Routes:") +log(" - /plugin/hello") +log(" - /plugin/api/info") +log(" - /plugin/files") +log(" - /plugin/dashboard") +log(" - /plugin/api/string-demo") +log("Registered POST Routes:") +log(" - /plugin/api/contact") +log(" - /plugin/api/create-note") +log("========================================") +log("Visit /plugin/dashboard to see all features!") +log("========================================") \ No newline at end of file diff --git a/lua/plugins/examples/post.lua b/lua/plugins/examples/post.lua new file mode 100644 index 0000000..c91cf51 --- /dev/null +++ b/lua/plugins/examples/post.lua @@ -0,0 +1,7 @@ +add_post_route("/lua/post", function(req) + local data = req.data or {} + log("Data received from " .. tostring(data)) + return 200, + {["Content-Type"] = "application/text"}, + tostring(data) +end) diff --git a/lua/readme.md b/lua/readme.md index a938d78..e69de29 100644 --- a/lua/readme.md +++ b/lua/readme.md @@ -1,1155 +0,0 @@ -# Plugin Manager Documentation - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Getting Started](#getting-started) -4. [Core Concepts](#core-concepts) - - [Plugins](#plugins) - - [Routes](#routes) - - [Hooks](#hooks) -5. [API Reference](#api-reference) - - [Route Registration](#route-registration) - - [Hook Registration](#hook-registration) - - [File Operations](#file-operations) - - [HTML Manipulation](#html-manipulation) - - [Markdown Manipulation](#markdown-manipulation) - - [Logging Functions](#logging-functions) - - [Utility Functions](#utility-functions) -6. [Guardrails & Security](#guardrails--security) -7. [Examples](#examples) - ---- - -## Overview - -The Plugin Manager is a Python-based system that enables dynamic Lua plugin loading for extending application functionality. It provides a secure, sandboxed environment for plugins to register HTTP routes, hook into application events, and manipulate HTML/Markdown content. - -**Key Features:** -- Hot-reload plugins without restarting the application -- Secure file operations with path traversal protection -- Built-in HTML and Markdown manipulation tools -- Event-based hook system -- HTTP route registration -- Comprehensive logging -- Rate limiting and validation helpers - ---- - -## Architecture - -### Directory Structure - -``` -project/ -├── plugin_manager.py # Main plugin manager -├── plugins/ # Lua plugin files (*.lua) -├── html/ # HTML files for manipulation -└── markdown/ # Markdown files for manipulation -``` - -### Component Overview - -**PluginManager**: Core class that manages plugin lifecycle, Lua runtime, and API exposure - -**PluginFSHandler**: Watchdog-based file system handler for hot-reloading - -**LuaRuntime**: Embedded Lua interpreter with Python bindings via `lupa` - ---- - -## Getting Started - -### Installation Requirements - -```bash -pip install lupa watchdog -``` - -### Creating Your First Plugin - -1. Create a file `plugins/hello.lua`: - -```lua --- Register a simple route -add_route("/hello", function(req) - return "

Hello from Lua Plugin!

" -end) - -log("Hello plugin loaded") -``` - -2. Start the plugin manager: - -```python -from plugin_manager import PluginManager - -manager = PluginManager() -manager.load_all() - -# Handle a request -result = manager.handle_request("/hello") -print(result) # (200, {"Content-Type": "text/html"}, "

Hello from Lua Plugin!

") -``` - -3. The plugin will automatically reload when you modify `hello.lua` - ---- - -## Core Concepts - -### Plugins - -Plugins are Lua scripts (`.lua` files) placed in the `plugins/` directory. Each plugin can: -- Register HTTP routes via [`add_route()`](#route-registration) -- Hook into events via [`register_hook()`](#hook-registration) -- Access [file operations](#file-operations) -- Use [HTML](#html-manipulation) and [Markdown manipulation](#markdown-manipulation) tools - -**Plugin Lifecycle:** -1. **Load**: Plugin file is read and executed in Lua runtime -2. **Active**: Plugin routes/hooks are registered and callable -3. **Reload**: File change detected → unload → load -4. **Unload**: Plugin deleted → routes/hooks removed - -### Routes - -Routes map URL paths to Lua handler functions. When a request matches a registered route, the corresponding Lua function is called. - -**See:** [Route Registration](#route-registration), [Examples - Route Handler](#example-1-simple-route-handler) - -### Hooks - -Hooks are named events where plugins can register callback functions. Multiple plugins can hook into the same event, and they'll be called in registration order. - -**Common Use Cases:** -- Pre/post-processing request data -- Modifying responses before they're sent -- Content transformation pipelines -- Event notifications - -**See:** [Hook Registration](#hook-registration), [Examples - Hook System](#example-2-hook-system) - ---- - -## API Reference - -### Route Registration - -#### `add_route(path, handler_function)` - -Register a URL path handler. - -**Parameters:** -- `path` (string): URL path to handle (e.g., `/api/users`) -- `handler_function` (function): Lua function to handle requests - -**Handler Function Signature:** -```lua -function(request) -> response -``` - -**Request Object:** -```lua -{ - method = "GET", -- HTTP method - path = "/api/users", -- Request path - query = {...}, -- Query parameters - body = "...", -- Request body - headers = {...} -- Request headers -} -``` - -**Response Format:** - -Option 1 - Simple string (returns 200 with text/html): -```lua -return "..." -``` - -Option 2 - Full response tuple: -```lua -return 200, {["Content-Type"] = "application/json"}, '{"status":"ok"}' -``` - -**Example:** -```lua -add_route("/api/status", function(req) - return 200, {["Content-Type"] = "application/json"}, '{"status": "online"}' -end) -``` - -**See also:** [Hooks](#hook-registration), [Examples](#examples) - ---- - -### Hook Registration - -#### `register_hook(hook_name, handler_function)` - -Register a callback for a named hook event. - -**Parameters:** -- `hook_name` (string): Name of the hook to register for -- `handler_function` (function): Callback function - -**Handler Function Signature:** -```lua -function(...args) -> result -``` - -**Behavior:** -- All registered hooks for an event are called in order -- Return values can modify data (last non-nil value is used) -- Hooks receive arguments passed by `run_hook()` from Python - -**Example:** -```lua --- Content transformation hook -register_hook("transform_html", function(html) - -- Add analytics script - return html_insert_before(html, "", "") -end) - --- Logging hook -register_hook("request_complete", function(path, status) - log("Request to " .. path .. " returned " .. status) -end) -``` - -**Python Side - Triggering Hooks:** -```python -# Transform HTML through all registered hooks -modified_html = manager.run_hook("transform_html", original_html) - -# Notify all hooks of event -manager.run_hook("request_complete", "/api/users", 200) -``` - -**See also:** [Routes](#route-registration), [Examples - Hooks](#example-2-hook-system) - ---- - -### File Operations - -#### `read_file(path)` - -Read any file with path validation. - -**Parameters:** -- `path` (string): File path to read - -**Returns:** File content as string, or `nil` on error - -**Security:** Path traversal (`..`) is blocked - -**Example:** -```lua -local config = read_file("config.json") -if config then - log("Config loaded") -end -``` - -#### `write_file(path, content)` - -Write content to file with path validation. - -**Parameters:** -- `path` (string): File path to write -- `content` (string): Content to write - -**Returns:** `true` on success, `false` on error - -**Example:** -```lua -write_file("output.txt", "Generated content") -``` - -**See also:** [HTML Operations](#html-manipulation), [Markdown Operations](#markdown-manipulation) - ---- - -### HTML Manipulation - -#### `read_html(filename)` - -Read HTML file from the `html/` directory. - -**Parameters:** -- `filename` (string): Name of HTML file (e.g., `"index.html"`) - -**Returns:** HTML content as string, or `nil` if not found - -**Example:** -```lua -local html = read_html("index.html") -if html then - -- Process HTML -end -``` - -#### `write_html(filename, content)` - -Write HTML content to the `html/` directory. - -**Parameters:** -- `filename` (string): Name of HTML file -- `content` (string): HTML content to write - -**Returns:** `true` on success, `false` on error - -**Example:** -```lua -local modified = html_insert_after(html, "", "") -write_html("index.html", modified) -``` - -#### `list_html_files()` - -Get list of all HTML files in the `html/` directory. - -**Returns:** Array of filenames - -**Example:** -```lua -local files = list_html_files() -for _, file in ipairs(files) do - log("Found HTML file: " .. file) -end -``` - -#### `html_find_tag(html, tag)` - -Find the first occurrence of an HTML tag. - -**Parameters:** -- `html` (string): HTML content to search -- `tag` (string): Tag name (e.g., `"div"`, `"title"`) - -**Returns:** Matched tag with content, or `nil` if not found - -**Example:** -```lua -local title_tag = html_find_tag(html, "title") --- Result: "My Page" -``` - -#### `html_replace_tag(html, tag, new_content)` - -Replace the content inside an HTML tag. - -**Parameters:** -- `html` (string): HTML content -- `tag` (string): Tag name to replace content within -- `new_content` (string): New content (tag itself is preserved) - -**Returns:** Modified HTML - -**Example:** -```lua --- Replace title content -local html = "Old Title" -local modified = html_replace_tag(html, "title", "New Title") --- Result: "New Title" -``` - -**See also:** [`html_wrap_content()`](#html_wrap_contenthtml-tag-wrapper_tag-attrs) - -#### `html_insert_before(html, marker, content)` - -Insert content before a marker string. - -**Parameters:** -- `html` (string): HTML content -- `marker` (string): String to insert before -- `content` (string): Content to insert - -**Returns:** Modified HTML - -**Example:** -```lua --- Insert meta tag before closing head -local html = "" -local modified = html_insert_before(html, "", "") --- Result: "" -``` - -**See also:** [`html_insert_after()`](#html_insert_afterhtml-marker-content) - -#### `html_insert_after(html, marker, content)` - -Insert content after a marker string. - -**Parameters:** -- `html` (string): HTML content -- `marker` (string): String to insert after -- `content` (string): Content to insert - -**Returns:** Modified HTML - -**Example:** -```lua --- Insert script before closing body -local modified = html_insert_after(html, "", "") -``` - -**See also:** [`html_insert_before()`](#html_insert_beforehtml-marker-content) - -#### `html_wrap_content(html, tag, wrapper_tag, attrs)` - -Wrap the content of a tag with another tag. - -**Parameters:** -- `html` (string): HTML content -- `tag` (string): Tag whose content to wrap -- `wrapper_tag` (string): Tag to wrap with -- `attrs` (string): Attributes for wrapper tag (optional) - -**Returns:** Modified HTML - -**Example:** -```lua -local html = "
Hello World
" -local modified = html_wrap_content(html, "div", "span", 'class="highlight"') --- Result: "
Hello World
" -``` - -**See also:** [`html_replace_tag()`](#html_replace_taghtml-tag-new_content) - ---- - -### Markdown Manipulation - -#### `read_markdown(filename)` - -Read Markdown file from the `markdown/` directory. - -**Parameters:** -- `filename` (string): Name of Markdown file (e.g., `"README.md"`) - -**Returns:** Markdown content as string, or `nil` if not found - -**Example:** -```lua -local md = read_markdown("README.md") -``` - -#### `write_markdown(filename, content)` - -Write Markdown content to the `markdown/` directory. - -**Parameters:** -- `filename` (string): Name of Markdown file -- `content` (string): Markdown content to write - -**Returns:** `true` on success, `false` on error - -**Example:** -```lua -write_markdown("output.md", "# Generated Document\n\nContent here...") -``` - -#### `list_markdown_files()` - -Get list of all Markdown files in the `markdown/` directory. - -**Returns:** Array of filenames - -**Example:** -```lua -local files = list_markdown_files() -for _, file in ipairs(files) do - local content = read_markdown(file) - -- Process each file -end -``` - -#### `md_add_header(markdown, level, text)` - -Add a header at the beginning of Markdown content. - -**Parameters:** -- `markdown` (string): Existing Markdown content -- `level` (number): Header level (1-6, where 1 is `#` and 6 is `######`) -- `text` (string): Header text - -**Returns:** Modified Markdown with header prepended - -**Example:** -```lua -local md = "Some content here" -local modified = md_add_header(md, 1, "Introduction") --- Result: --- # Introduction --- --- Some content here -``` - -**See also:** [`md_replace_section()`](#md_replace_sectionmarkdown-header-new_content) - -#### `md_replace_section(markdown, header, new_content)` - -Replace an entire Markdown section (from header to next header or end). - -**Parameters:** -- `markdown` (string): Markdown content -- `header` (string): Header text to find (without `#` symbols) -- `new_content` (string): New content for the section - -**Returns:** Modified Markdown - -**Example:** -```lua -local md = [[ -# Introduction -Old intro text - -# Features -Feature list here -]] - -local modified = md_replace_section(md, "Introduction", "New introduction paragraph") --- Result: --- ## Introduction --- --- New introduction paragraph --- --- # Features --- Feature list here -``` - -**See also:** [`md_add_header()`](#md_add_headermarkdown-level-text), [`md_append_content()`](#md_append_contentmarkdown-content) - -#### `md_append_content(markdown, content)` - -Append content to the end of Markdown document. - -**Parameters:** -- `markdown` (string): Existing Markdown content -- `content` (string): Content to append - -**Returns:** Modified Markdown - -**Example:** -```lua -local md = "# Document\n\nExisting content" -local modified = md_append_content(md, "## Appendix\n\nAdditional information") -``` - -**See also:** [`md_add_header()`](#md_add_headermarkdown-level-text) - ---- - -### Logging Functions - -#### `log(message)` - -Log informational message. - -**Parameters:** -- `message` (string): Message to log - -**Example:** -```lua -log("Plugin initialized successfully") -``` - -**See also:** [`log_warn()`](#log_warnmessage), [`log_error()`](#log_errormessage) - -#### `log_warn(message)` - -Log warning message. - -**Parameters:** -- `message` (string): Warning message - -**Example:** -```lua -if not config then - log_warn("Configuration file not found, using defaults") -end -``` - -**See also:** [`log()`](#logmessage), [`log_error()`](#log_errormessage) - -#### `log_error(message)` - -Log error message. - -**Parameters:** -- `message` (string): Error message - -**Example:** -```lua -local html = read_html("template.html") -if not html then - log_error("Failed to load template.html") - return -end -``` - -**See also:** [`log()`](#logmessage), [`log_warn()`](#log_warnmessage) - ---- - -### Utility Functions - -#### `table_to_json(lua_table)` - -Convert Lua table to JSON string. - -**Parameters:** -- `lua_table` (table): Lua table to convert - -**Returns:** JSON string - -**Example:** -```lua -local data = {name = "John", age = 30} -local json = table_to_json(data) --- Result: '{"name":"John","age":30}' -``` - -**See also:** [`json_to_table()`](#json_to_tablejson_str) - -#### `json_to_table(json_str)` - -Parse JSON string into Lua table. - -**Parameters:** -- `json_str` (string): JSON string - -**Returns:** Lua table, or empty table on error - -**Example:** -```lua -local json = '{"status":"ok","count":5}' -local data = json_to_table(json) -log("Status: " .. data.status) -- Status: ok -``` - -**See also:** [`table_to_json()`](#table_to_jsonlua_table) - ---- - -## Guardrails & Security - -The plugin system includes built-in security features and helper functions to prevent common vulnerabilities. - -### Path Traversal Protection - -All file operations automatically block path traversal attempts: - -```lua --- BLOCKED - Will fail safely -read_file("../../../etc/passwd") -read_html("../../config.php") - --- ALLOWED - Sandboxed to designated directories -read_html("index.html") -read_markdown("docs.md") -``` - -**Implementation:** Any path containing `..` is rejected before file access. - -### Safe String Operations - -#### `safe_concat(...)` - -Safely concatenate multiple values, converting to strings and handling `nil`. - -**Example:** -```lua -local msg = safe_concat("User ", nil, " logged in at ", os.time()) --- Result: "User logged in at 1609459200" -``` - -**See also:** [`escape_html()`](#escape_htmlstr) - -#### `escape_html(str)` - -Escape HTML special characters to prevent XSS attacks. - -**Escapes:** -- `&` → `&` -- `<` → `<` -- `>` → `>` -- `"` → `"` -- `'` → `'` - -**Example:** -```lua -local user_input = '' -local safe = escape_html(user_input) --- Result: "<script>alert("XSS")</script>" - -return "
" .. safe .. "
" -``` - -**Use Case:** Always escape user input before including in HTML responses. - -**See also:** [Security Best Practices](#security) - -### Filename Validation - -#### `is_valid_filename(name)` - -Validate filename for safety. - -**Checks:** -- Not nil or empty -- No `..` (path traversal) -- No `/` or `\` (directory separators) - -**Returns:** `true` if valid, `false` otherwise - -**Example:** -```lua -local filename = req.query.file - -if not is_valid_filename(filename) then - return 400, {}, "Invalid filename" -end - -local content = read_html(filename) -``` - -**See also:** [Path Traversal Protection](#path-traversal-protection) - -### Error Handling - -#### `try_catch(fn, catch_fn)` - -Safe error handling wrapper using `pcall`. - -**Parameters:** -- `fn` (function): Function to execute -- `catch_fn` (function): Error handler (receives error message) - -**Returns:** `true` if successful, `false` if error occurred - -**Example:** -```lua -local success = try_catch( - function() - local html = read_html("template.html") - local modified = html_replace_tag(html, "title", "New Title") - write_html("output.html", modified) - end, - function(err) - log_error("HTML processing failed: " .. tostring(err)) - end -) - -if not success then - return 500, {}, "Internal error" -end -``` - -**See also:** [`validate_request()`](#validate_requestreq-required_fields) - -### Request Validation - -#### `validate_request(req, required_fields)` - -Validate request object has required fields. - -**Parameters:** -- `req` (table): Request object to validate -- `required_fields` (array): List of required field names - -**Returns:** `(true, nil)` if valid, or `(false, error_message)` if invalid - -**Example:** -```lua -add_route("/api/create", function(req) - local valid, err = validate_request(req, {"name", "email", "body"}) - if not valid then - return 400, {}, "Bad request: " .. err - end - - -- Process valid request - return 200, {}, "Created" -end) -``` - -**See also:** [`try_catch()`](#try_catchfn-catch_fn) - -### Rate Limiting - -#### `check_rate_limit(key, max_calls, window_seconds)` - -Simple in-memory rate limiting. - -**Parameters:** -- `key` (string): Unique identifier for rate limit (e.g., IP address, user ID) -- `max_calls` (number): Maximum calls allowed in window -- `window_seconds` (number): Time window in seconds - -**Returns:** `true` if within limit, `false` if exceeded - -**Example:** -```lua -add_route("/api/search", function(req) - local client_ip = req.headers["X-Forwarded-For"] or "unknown" - - if not check_rate_limit(client_ip, 10, 60) then - return 429, {}, "Rate limit exceeded. Try again later." - end - - -- Process search - return 200, {}, "Search results" -end) -``` - -**Note:** Rate limits are stored in memory and reset when plugin reloads. - -**See also:** [Security Best Practices](#security) - -### Table Utilities - -#### `table_contains(tbl, value)` - -Check if table contains a value. - -**Example:** -```lua -local allowed = {"admin", "moderator", "user"} -if table_contains(allowed, req.role) then - -- Process request -end -``` - -#### `table_keys(tbl)` - -Get all keys from a table. - -**Example:** -```lua -local data = {name = "John", age = 30} -local keys = table_keys(data) --- Result: {"name", "age"} -``` - -#### `table_values(tbl)` - -Get all values from a table. - -**Example:** -```lua -local data = {a = 1, b = 2, c = 3} -local values = table_values(data) --- Result: {1, 2, 3} -``` - ---- - -## Examples - -### Example 1: Simple Route Handler - -Create `plugins/api.lua`: - -```lua --- Simple API route -add_route("/api/hello", function(req) - local name = req.query.name or "World" - local safe_name = escape_html(name) - - return 200, - {["Content-Type"] = "application/json"}, - '{"message": "Hello ' .. safe_name .. '"}' -end) - -log("API plugin loaded") -``` - -**Usage:** -```python -result = manager.handle_request("/api/hello", { - "query": {"name": "Alice"} -}) -# Returns: (200, {...}, '{"message": "Hello Alice"}') -``` - -**See also:** [Route Registration](#route-registration) - ---- - -### Example 2: Hook System - -Create `plugins/analytics.lua`: - -```lua --- Hook into HTML transformation -register_hook("transform_html", function(html, page_name) - log("Adding analytics to " .. page_name) - - local analytics_script = [[ - - ]] - - return html_insert_before(html, "", analytics_script) -end) -``` - -**Python Side:** -```python -# Trigger hook -original_html = "..." -modified_html = manager.run_hook("transform_html", original_html, "home") -``` - -**See also:** [Hook Registration](#hook-registration) - ---- - -### Example 3: HTML Template Processor - -Create `plugins/templates.lua`: - -```lua --- Process HTML templates with variable substitution -add_route("/render/:template", function(req) - local template_name = req.params.template .. ".html" - - -- Validate filename - if not is_valid_filename(template_name) then - return 400, {}, "Invalid template name" - end - - -- Read template - local html = read_html(template_name) - if not html then - return 404, {}, "Template not found" - end - - -- Replace variables - local title = req.query.title or "Untitled" - local content = req.query.content or "No content" - - html = html_replace_tag(html, "title", escape_html(title)) - html = html_replace_tag(html, "main", escape_html(content)) - - return html -end) -``` - -**See also:** [HTML Manipulation](#html-manipulation), [Route Registration](#route-registration) - ---- - -### Example 4: Markdown Documentation Generator - -Create `plugins/docs_generator.lua`: - -```lua --- Generate documentation from code files -add_route("/docs/generate", function(req) - local doc_content = [[ -# API Documentation - -Generated on: ]] .. os.date() .. [[ - -## Overview - -This documentation is auto-generated from source code. - ]] - - -- Add sections for each endpoint - local endpoints = { - {path = "/api/users", method = "GET", desc = "Get all users"}, - {path = "/api/users/:id", method = "GET", desc = "Get user by ID"}, - {path = "/api/users", method = "POST", desc = "Create new user"} - } - - for _, endpoint in ipairs(endpoints) do - local section = string.format([[ - -## %s %s - -%s - -**Example:** -``` -curl -X %s http://localhost%s -``` - ]], endpoint.method, endpoint.path, endpoint.desc, endpoint.method, endpoint.path) - - doc_content = md_append_content(doc_content, section) - end - - -- Save to file - write_markdown("api_docs.md", doc_content) - - return 200, {}, "Documentation generated successfully" -end) -``` - -**See also:** [Markdown Manipulation](#markdown-manipulation) - ---- - -### Example 5: Content Security System - -Create `plugins/security.lua`: - -```lua --- Rate-limited API with validation -add_route("/api/submit", function(req) - -- Rate limiting - local client_ip = req.headers["X-Real-IP"] or "unknown" - if not check_rate_limit(client_ip, 5, 60) then - log_warn("Rate limit exceeded for " .. client_ip) - return 429, {}, "Too many requests" - end - - -- Request validation - local valid, err = validate_request(req, {"title", "content", "author"}) - if not valid then - return 400, {}, err - end - - -- Content sanitization - local title = escape_html(req.body.title) - local content = escape_html(req.body.content) - local author = escape_html(req.body.author) - - -- Save as HTML - local html = string.format([[ - - - - %s - - -

%s

-

By: %s

-
%s
- - - ]], title, title, author, content) - - local filename = "post_" .. os.time() .. ".html" - write_html(filename, html) - - log("Created post: " .. filename) - return 200, {}, "Post created successfully" -end) -``` - -**See also:** [Guardrails & Security](#guardrails--security) - ---- - -### Example 6: Multi-File HTML Processor - -Create `plugins/html_batch.lua`: - -```lua --- Process all HTML files in a directory -add_route("/admin/add_footer", function(req) - local files = list_html_files() - local processed = 0 - - local footer = [[ - - ]] - - for _, filename in ipairs(files) do - try_catch( - function() - local html = read_html(filename) - if html then - -- Add footer before closing body tag - local modified = html_insert_before(html, "", footer) - write_html(filename, modified) - processed = processed + 1 - end - end, - function(err) - log_error("Failed to process " .. filename .. ": " .. tostring(err)) - end - ) - end - - return 200, {}, "Processed " .. processed .. " files" -end) -``` - -**See also:** [HTML Manipulation](#html-manipulation), [Error Handling](#error-handling) - ---- - -## Best Practices - -### Plugin Design - -1. **Single Responsibility**: Each plugin should have one clear purpose -2. **Error Handling**: Always use [`try_catch()`](#try_catchfn-catch_fn) for operations that might fail -3. **Logging**: Use appropriate log levels ([`log()`](#logmessage), [`log_warn()`](#log_warnmessage), [`log_error()`](#log_errormessage)) -4. **Documentation**: Add comments explaining what your plugin does - -```lua --- Good: Clear purpose, error handling, logging -add_route("/api/config", function(req) - try_catch( - function() - local config = read_file("config.json") - return 200, {}, config - end, - function(err) - log_error("Config read failed: " .. tostring(err)) - return 500, {}, "Internal error" - end - ) -end) -``` - -**See also:** [Examples](#examples) - ---- - -### Security - -1. **Always Escape User Input**: Use [`escape_html()`](#escape_htmlstr) for any user-provided content -2. **Validate Filenames**: Use [`is_valid_filename()`](#is_valid_filenamename) before file operations -3. **Validate Requests**: Use [`validate_request()`](#validate_requestreq-required_fields) for required fields -4. **Implement Rate Limiting**: Use [`check_rate_limit()`](#check_rate_limitkey-max_calls-window_seconds) for public APIs -5. **Never Trust Input**: Sanitize and validate all external data - -```lua --- Good: Comprehensive security -add_route("/api/read", function(req) - local filename = req.query.file - - -- Validate filename - if not is_valid_filename(filename) then - return 400, {}, "Invalid filename" - end - - -- Rate limit - if not check_rate_limit(req.ip, 10, 60) then - return 429, {}, "Rate limited" - end - - -- Safe read - local content = read_html(filename) - if content then - return escape_html(content) - end - return 404, {}, "Not found" -end) -``` - -**See also:** [Guardrails & Security](#guardrails--security), [Path Traversal Protection](#path-traversal-protection) - diff --git a/webserver.py b/webserver.py index a048d74..5a2381b 100644 --- a/webserver.py +++ b/webserver.py @@ -4,6 +4,7 @@ import threading import subprocess from http.server import BaseHTTPRequestHandler, HTTPServer import mimetypes +import json from jsmin import jsmin # pip install jsmin from pathlib import Path @@ -107,6 +108,69 @@ def index_footer(): """ class MyHandler(BaseHTTPRequestHandler): + # This is a Helper Function for the POST Endpoints + def _parse_post_data(self): + """Parse POST request body""" + import json + content_length = int(self.headers.get('Content-Length', 0)) + if content_length == 0: + return {} + + post_data = self.rfile.read(content_length) + content_type = self.headers.get('Content-Type', '') + + try: + if 'application/json' in content_type: + return json.loads(post_data.decode('utf-8')) + elif 'application/x-www-form-urlencoded' in content_type: + from urllib.parse import parse_qs + parsed = parse_qs(post_data.decode('utf-8')) + return {k: v[0] if len(v) == 1 else v for k, v in parsed.items()} + else: + return {"raw": post_data} + except Exception as e: + logger.log_error(f"Error parsing POST data: {e}") + return {"raw": post_data} + + def do_POST(self): + """Handle POST requests - primarily for plugin routes""" + req_path = self.path.lstrip("/") + + # Parse POST data + post_data = self._parse_post_data() + + # Add additional request info + request_data = { + "path": self.path, + "headers": dict(self.headers), + "data": post_data, + "method": "POST" + } + + # Check plugin routes + plugin_result = plugin_manager.handle_request("/" + req_path, request_data, method="POST") + if plugin_result is not None: + status, headers, body = plugin_result + self.send_response(status) + for key, value in headers.items(): + self.send_header(key, value) + self.end_headers() + + if isinstance(body, str): + self.wfile.write(body.encode("utf-8")) + elif isinstance(body, bytes): + self.wfile.write(body) + else: + self.wfile.write(str(body).encode("utf-8")) + return + + # No plugin handled this POST request + self.send_response(404) + self.send_header("Content-type", "application/json") + self.end_headers() + error_response = json.dumps({"error": "Route not found"}) + self.wfile.write(error_response.encode("utf-8")) + def do_GET(self): req_path = self.path.lstrip("/") # normalize leading /