From 06217188e12a62046e71e7e16041347efe51e01f Mon Sep 17 00:00:00 2001
From: rattatwinko
Date: Mon, 6 Oct 2025 11:22:05 +0200
Subject: [PATCH] fixed Lua Runtime. Plugins now have priority for hooks and
POST for routes if requested. also changed the CSS for main.css to color
#1e1e1e for darkmode body
---
.vscode/globals.lua | 184 +++
.vscode/settings.json | 89 +-
css/main.css | 8 +-
log/Logger.py | 6 +
lua/Actions.py | 196 ++-
lua/plugin_manager.py | 203 ++-
.../{ => examples}/demo/demo_route.lua | 0
.../{ => examples}/demo/generatemd.lua | 0
lua/plugins/{ => examples}/demo/hello.lua | 0
lua/plugins/{ => examples}/demo/md_banner.lua | 0
.../demo/return_available_routes.lua | 0
lua/plugins/examples/demo/test.lua | 530 ++++++++
lua/plugins/examples/post.lua | 7 +
lua/readme.md | 1155 -----------------
webserver.py | 64 +
15 files changed, 1180 insertions(+), 1262 deletions(-)
create mode 100644 .vscode/globals.lua
rename lua/plugins/{ => examples}/demo/demo_route.lua (100%)
rename lua/plugins/{ => examples}/demo/generatemd.lua (100%)
rename lua/plugins/{ => examples}/demo/hello.lua (100%)
rename lua/plugins/{ => examples}/demo/md_banner.lua (100%)
rename lua/plugins/{ => examples}/demo/return_available_routes.lua (100%)
create mode 100644 lua/plugins/examples/demo/test.lua
create mode 100644 lua/plugins/examples/post.lua
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 = ""
- 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
+
+
+
+
+
+
+
+
+
+
📄 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)
+
+
+
+
+
+
+
+
+
+
+
🔗 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, "
", "")
-```
-
-**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, "", "")
-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, "
"
-```
-
-#### `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 = "
"
-```
-
-**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 = "
"
-```
-
-**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 /