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}[^>]*>.*?{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}[^>]*>).*?({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}[^>]*>)(.*?)({tag}>)"
@@ -102,45 +106,185 @@ class Actions:
open_tag, content, close_tag = match.groups()
return f"{open_tag}<{wrapper_tag} {attrs}>{content}{wrapper_tag}>{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}[^>]*>(.*?){tag}>"
+ return re.sub(pattern, r"\1", html, flags=re.DOTALL | re.IGNORECASE)
+ else:
+ pattern = f"<{tag}[^>]*>.*?{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 = " This is a simple GET route. Comprehensive Plugin System Demonstration Generated HTML documents Source markdown files ]] .. os.date("%Y-%m-%d") .. [[ GET GET GET GET POST POST When a markdown file is rendered, these hooks process the HTML sequentially:
+ Each hook receives the output from the previous hook, ensuring no modifications are lost.
+ Hello from Plugin!
📁 File Browser
+
+ HTML Files (]] .. #html_files .. [[)
+]]
+
+ for i, file in ipairs(html_files) do
+ html = html .. ' Markdown Files (]] .. #md_files .. [[)
+]]
+
+ for i, file in ipairs(md_files) do
+ html = html .. ' 🔌 PyPost Plugin Dashboard
+ 📄 HTML Files
+ 📝 Markdown Files
+ ⏰ Server Time
+ 🚀 Plugin Features
+
+
+ 🌐 Available API Endpoints
+ /plugin/hello - Simple greeting/plugin/api/info - JSON plugin info/plugin/files - File browser/plugin/dashboard - This page/plugin/api/contact - Submit contact form/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
+ 💻 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)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, "
By: %s
-", "") -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: "
" -``` - -#### `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 = "
" -local modified = html_replace_tag(html, "title", "New Title") --- Result: "
" -``` - -**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, "
", 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 /