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

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

184
.vscode/globals.lua vendored Normal file
View File

@@ -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

67
.vscode/settings.json vendored
View File

@@ -1,27 +1,66 @@
{ {
// 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": [ "Lua.diagnostics.globals": [
"read_file", "add_route",
"write_file", "add_get_route",
"read_html", "add_post_route",
"write_html", "register_hook",
"list_html_files", "list_html_files",
"read_markdown",
"write_markdown",
"list_markdown_files", "list_markdown_files",
"html_find_tag", "write_markdown",
"html_replace_tag", "html_add_class",
"html_add_attribute",
"html_insert_before", "html_insert_before",
"html_insert_after", "html_insert_after",
"html_wrap_content", "html_wrap_content",
"html_remove_tag",
"md_add_header", "md_add_header",
"md_replace_section", "md_replace_section",
"md_append_content", "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", "table_to_json",
"json_to_table", "json_to_table",
"add_route", "json_parse",
"register_hook", "json_stringify",
"log", "string_split",
"log_warn", "string_join",
"log_error" "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
} }

View File

@@ -4,7 +4,7 @@ body {
font-size: clamp(14px, 2.5vw, 16px); /* scales from 14px to 16px */ font-size: clamp(14px, 2.5vw, 16px); /* scales from 14px to 16px */
line-height: 1.5; line-height: 1.5;
margin: 1em; margin: 1em;
color: #000; color: #1e1e1e;
background: #fff; background: #fff;
} }
@@ -84,7 +84,7 @@ table {
font-size: clamp(0.85rem, 2vw, 1rem); /* slightly smaller on mobile */ font-size: clamp(0.85rem, 2vw, 1rem); /* slightly smaller on mobile */
} }
th, td { th, td {
border: 1px solid #000; border: 1px solid #1e1e1e;
padding: 0.5em; padding: 0.5em;
text-align: left; text-align: left;
} }
@@ -215,7 +215,7 @@ button:hover {
} }
.html-content th, .html-content th,
.html-content td { .html-content td {
border: 1px solid #000; border: 1px solid #1e1e1e;
padding: 0.5em 0.75em; padding: 0.5em 0.75em;
} }
.html-content th { .html-content th {
@@ -349,7 +349,7 @@ pre[class*="language-"] {
/* Dark mode */ /* Dark mode */
body.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; color: #e0e0e0;
} }

View File

@@ -54,4 +54,10 @@ class Logger:
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"{colorama.Fore.MAGENTA}[ LUA_WARNING@{now} ]: {message}{colorama.Style.RESET_ALL}") 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}")

View File

@@ -1,12 +1,25 @@
from pathlib import Path from pathlib import Path
from log import Logger from log import Logger
import re import re
import json
PLUGINS_DIR = Path(__file__).parent / "plugins" PLUGINS_DIR = Path(__file__).parent / "plugins"
HTML_DIR = Path(__file__).parent / "../html" HTML_DIR = Path(__file__).parent / "../html"
MARKDOWN_DIR = Path(__file__).parent / ".." / "markdown" MARKDOWN_DIR = Path(__file__).parent / ".." / "markdown"
class Actions: 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): def _safe_read_file(self, path):
"""Safe file reading with path validation""" """Safe file reading with path validation"""
try: try:
@@ -18,7 +31,7 @@ class Actions:
except Exception as e: except Exception as e:
Logger.log_lua_error(f"Error reading file {path}: {e}") Logger.log_lua_error(f"Error reading file {path}: {e}")
return None return None
@staticmethod
def _safe_write_file(self, path, content): def _safe_write_file(self, path, content):
"""Safe file writing with path validation""" """Safe file writing with path validation"""
try: try:
@@ -32,8 +45,7 @@ class Actions:
Logger.log_lua_error(f"Error writing file {path}: {e}") Logger.log_lua_error(f"Error writing file {path}: {e}")
return False return False
# HTML/Markdown content operations # HTML/Markdown Content Operations
@staticmethod
def _read_content(self, base_dir, filename): def _read_content(self, base_dir, filename):
"""Read content from HTML or Markdown directory""" """Read content from HTML or Markdown directory"""
try: try:
@@ -47,8 +59,6 @@ class Actions:
Logger.log_lua_error(f"Error reading {filename}: {e}") Logger.log_lua_error(f"Error reading {filename}: {e}")
return None return None
@staticmethod
def _write_content(self, base_dir, filename, content): def _write_content(self, base_dir, filename, content):
"""Write content to HTML or Markdown directory""" """Write content to HTML or Markdown directory"""
try: try:
@@ -61,7 +71,6 @@ class Actions:
Logger.log_lua_error(f"Error writing {filename}: {e}") Logger.log_lua_error(f"Error writing {filename}: {e}")
return False return False
@staticmethod
def _list_files(self, base_dir, extension): def _list_files(self, base_dir, extension):
"""List files with given extension""" """List files with given extension"""
try: try:
@@ -70,31 +79,26 @@ class Actions:
Logger.log_lua_error(f"Error listing files: {e}") Logger.log_lua_error(f"Error listing files: {e}")
return [] return []
# HTML manipulation helpers # HTML Manipulation Helpers
@staticmethod
def _html_find_tag(self, html, tag): def _html_find_tag(self, html, tag):
"""Find first occurrence of HTML tag""" """Find first occurrence of HTML tag"""
pattern = f"<{tag}[^>]*>.*?</{tag}>" pattern = f"<{tag}[^>]*>.*?</{tag}>"
match = re.search(pattern, html, re.DOTALL | re.IGNORECASE) match = re.search(pattern, html, re.DOTALL | re.IGNORECASE)
return match.group(0) if match else None return match.group(0) if match else None
@staticmethod
def _html_replace_tag(self, html, tag, new_content): def _html_replace_tag(self, html, tag, new_content):
"""Replace HTML tag content""" """Replace HTML tag content"""
pattern = f"(<{tag}[^>]*>).*?(</{tag}>)" pattern = f"(<{tag}[^>]*>).*?(</{tag}>)"
return re.sub(pattern, f"\\1{new_content}\\2", html, flags=re.DOTALL | re.IGNORECASE) return re.sub(pattern, f"\\1{new_content}\\2", html, flags=re.DOTALL | re.IGNORECASE)
@staticmethod
def _html_insert_before(self, html, marker, content): def _html_insert_before(self, html, marker, content):
"""Insert content before a marker""" """Insert content before a marker"""
return html.replace(marker, content + marker) return html.replace(marker, content + marker)
@staticmethod
def _html_insert_after(self, html, marker, content): def _html_insert_after(self, html, marker, content):
"""Insert content after a marker""" """Insert content after a marker"""
return html.replace(marker, marker + content) return html.replace(marker, marker + content)
@staticmethod
def _html_wrap_content(self, html, tag, wrapper_tag, attrs=""): def _html_wrap_content(self, html, tag, wrapper_tag, attrs=""):
"""Wrap tag content with another tag""" """Wrap tag content with another tag"""
pattern = f"(<{tag}[^>]*>)(.*?)(</{tag}>)" pattern = f"(<{tag}[^>]*>)(.*?)(</{tag}>)"
@@ -103,44 +107,184 @@ class Actions:
return f"{open_tag}<{wrapper_tag} {attrs}>{content}</{wrapper_tag}>{close_tag}" return f"{open_tag}<{wrapper_tag} {attrs}>{content}</{wrapper_tag}>{close_tag}"
return re.sub(pattern, replacer, html, flags=re.DOTALL | re.IGNORECASE) return re.sub(pattern, replacer, html, flags=re.DOTALL | re.IGNORECASE)
# Markdown manipulation helpers def _html_remove_tag(self, html, tag, keep_content=True):
@staticmethod """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
def _md_add_header(self, markdown, level, text): def _md_add_header(self, markdown, level, text):
"""Add header to markdown""" """Add header to markdown"""
prefix = "#" * level prefix = "#" * level
return f"{prefix} {text}\n\n{markdown}" return f"{prefix} {text}\n\n{markdown}"
@staticmethod
def _md_replace_section(self, markdown, header, new_content): def _md_replace_section(self, markdown, header, new_content):
"""Replace markdown section""" """Replace markdown section"""
# Find section starting with header # Find section starting with header
pattern = f"(#{1,6}\\s+{re.escape(header)}.*?)(?=#{1,6}\\s+|$)" 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) return re.sub(pattern, f"## {header}\n\n{new_content}\n\n", markdown, flags=re.DOTALL)
@staticmethod
def _md_append_content(self, markdown, content): def _md_append_content(self, markdown, content):
"""Append content to markdown""" """Append content to markdown"""
return markdown.rstrip() + "\n\n" + content return markdown.rstrip() + "\n\n" + content
# JSON conversion helpers def _md_prepend_content(self, markdown, content):
@staticmethod """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
def _table_to_json(self, lua_table): def _table_to_json(self, lua_table):
"""Convert Lua table to JSON string""" """Convert Lua table to JSON string"""
import json
try: try:
# Convert lupa table to Python 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) py_dict = dict(lua_table)
return json.dumps(py_dict)
return json.dumps(py_dict, indent=2, ensure_ascii=False)
except Exception as e: except Exception as e:
Logger.log_lua_error(f"Error converting table to JSON: {e}") Logger.log_lua_error(f"Error converting table to JSON: {e}")
return "{}" return "{}"
@staticmethod
def _json_to_table(self, json_str): def _json_to_table(self, json_str):
"""Convert JSON string to Lua table""" """Convert JSON string to Lua table (Python dict)"""
import json
try: try:
return json.loads(json_str) return json.loads(json_str)
except Exception as e: except Exception as e:
Logger.log_lua_error(f"Error parsing JSON: {e}") Logger.log_lua_error(f"Error parsing JSON: {e}")
return {} 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 []

View File

@@ -7,6 +7,7 @@ from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from log.Logger import Logger from log.Logger import Logger
from .PluginFSHandler import PluginFSHandler from .PluginFSHandler import PluginFSHandler
import json
from .luarails import guardrails_code from .luarails import guardrails_code
PLUGINS_DIR = Path(__file__).parent / "plugins" PLUGINS_DIR = Path(__file__).parent / "plugins"
@@ -17,8 +18,11 @@ class PluginManager:
def __init__(self): def __init__(self):
self.lua = LuaRuntime(unpack_returned_tuples=True) self.lua = LuaRuntime(unpack_returned_tuples=True)
self.plugins = {} # name -> dict{path, lua_module, hooks, routes} self.plugins = {} # name -> dict{path, lua_module, hooks, routes}
self.routes = {} # path -> (plugin_name, lua_fn) self.routes = {
self.hooks = {} # hook_name -> list of (plugin_name, lua_fn) "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 # Create directories
PLUGINS_DIR.mkdir(exist_ok=True) PLUGINS_DIR.mkdir(exist_ok=True)
@@ -32,55 +36,76 @@ class PluginManager:
def _setup_lua_globals(self): def _setup_lua_globals(self):
"""Set up all Lua global functions and guardrails""" """Set up all Lua global functions and guardrails"""
# plugin_manager.py
from .Actions import Actions from .Actions import Actions
g = self.lua.globals() g = self.lua.globals()
self.actions = Actions() self.actions = Actions()
# Route and hook registration # Route and hook registration with priority support
g.add_route = self._expose_add_route 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 g.register_hook = self._expose_register_hook
# Logging - using custom Logger # Logging
# TODO: With Logger do custom Plugin Loading
g.log = lambda msg: Logger.log_lua_info(f"PLUGIN => {msg}") 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_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.read_file = self.actions._safe_read_file
g.write_file = self.actions._safe_write_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.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.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.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.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_find_tag = self.actions._html_find_tag
g.html_replace_tag = self.actions._html_replace_tag g.html_replace_tag = self.actions._html_replace_tag
g.html_insert_before = self.actions._html_insert_before g.html_insert_before = self.actions._html_insert_before
g.html_insert_after = self.actions._html_insert_after g.html_insert_after = self.actions._html_insert_after
g.html_wrap_content = self.actions._html_wrap_content 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_add_header = self.actions._md_add_header
g.md_replace_section = self.actions._md_replace_section g.md_replace_section = self.actions._md_replace_section
g.md_append_content = self.actions._md_append_content 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.table_to_json = self.actions._table_to_json
g.json_to_table = self.actions._json_to_table 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 # Guardrails - predefined safe patterns
self._setup_lua_guardrails() self._setup_lua_guardrails()
@@ -91,7 +116,6 @@ class PluginManager:
except LuaError as e: except LuaError as e:
Logger.log_lua_error(f"Failed to initialize Lua guardrails: {e}") Logger.log_lua_error(f"Failed to initialize Lua guardrails: {e}")
""" Lifecycle of Plugin """ """ Lifecycle of Plugin """
def load_all(self): def load_all(self):
for p in PLUGINS_DIR.glob("*.lua"): for p in PLUGINS_DIR.glob("*.lua"):
@@ -123,17 +147,21 @@ class PluginManager:
else: else:
Logger.log_lua_warning(f"Tried to reload {name}, but file no longer exists") Logger.log_lua_warning(f"Tried to reload {name}, but file no longer exists")
def unload_plugin(self, name: str): def unload_plugin(self, name: str):
# Remove routes/hook registrations from this plugin # Remove routes from this plugin (both GET and POST)
to_remove_routes = [r for r, v in self.routes.items() if v[0] == name] for method in ["GET", "POST"]:
for r in to_remove_routes: for route_path in list(self.routes[method].keys()):
del self.routes[r] 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()): # Remove hooks from this plugin
self.hooks[hook] = [x for x in lst if x[0] != name] for hook_name in list(self.hooks.keys()):
if not self.hooks[hook]: self.hooks[hook_name] = [x for x in self.hooks[hook_name] if x[0] != name]
del self.hooks[hook] if not self.hooks[hook_name]:
del self.hooks[hook_name]
if name in self.plugins: if name in self.plugins:
del self.plugins[name] del self.plugins[name]
@@ -141,55 +169,126 @@ class PluginManager:
else: else:
Logger.log_lua_warning(f"Tried to unload {name}, but it was not loaded") 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_method(self, path, lua_fn, method="GET", priority=50):
def _expose_add_route(self, path, lua_fn): """
"""Called from Lua as add_route(path, function(req) ... end)""" Register a route with a specific HTTP method and priority.
Lower priority numbers run first (0 = highest priority).
"""
p = str(path) p = str(path)
plugin_name = self._current_loading_plugin_name() m = str(method).upper()
if not plugin_name: plugin_name = self._current_loading_plugin_name() or "<unknown>"
plugin_name = "<unknown>"
self.routes[p] = (plugin_name, lua_fn)
Logger.log_lua_info(f"Plugin {plugin_name} registered route {p}")
def _expose_register_hook(self, hook_name, lua_fn): 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, 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) hook = str(hook_name)
plugin_name = self._current_loading_plugin_name() or "<unknown>" plugin_name = self._current_loading_plugin_name() or "<unknown>"
self.hooks.setdefault(hook, []).append((plugin_name, lua_fn))
Logger.log_lua_info(f"Plugin {plugin_name} registered hook {hook}") if hook not in self.hooks:
self.hooks[hook] = []
self.hooks[hook].append((plugin_name, lua_fn, int(priority)))
# Sort by priority (lower number = higher priority)
self.hooks[hook].sort(key=lambda x: x[2])
Logger.log_lua_info(f"Plugin {plugin_name} registered hook {hook} (priority: {priority})")
def _current_loading_plugin_name(self): def _current_loading_plugin_name(self):
return getattr(self, "_current_plugin", None) return getattr(self, "_current_plugin", None)
""" Running hooks & handling routes """ """ Running hooks & handling routes """
def handle_request(self, path, request_info=None): def handle_request(self, path, request_info=None, method="GET"):
"""If a plugin registered a route for this path, call it and return (status, headers, body) or raw str.""" """
if path in self.routes: Handle HTTP requests by calling registered plugin routes.
plugin_name, lua_fn = self.routes[path] 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: try:
lua_req = request_info or {} lua_req = request_info or {}
res = lua_fn(lua_req) 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: if isinstance(res, tuple) and len(res) == 3:
return res return res
# Handle string response
return (200, {"Content-Type": "text/html"}, str(res)) return (200, {"Content-Type": "text/html"}, str(res))
except LuaError as e: except LuaError as e:
Logger.log_lua_error(f"Lua error in route {path}: {e}") Logger.log_lua_error(f"Lua error in {method} route {path} from {plugin_name}: {e}")
return (500, {"Content-Type": "text/plain"}, f"Plugin error: {e}") # Continue to next handler instead of failing completely
continue
return None return None
def run_hook(self, hook_name: str, *args): 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: if hook_name not in self.hooks:
return None 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: try:
out = fn(*args) # Pass the current result as the first argument, plus any other args
if out is not None: hook_args = (result,) + args[1:] if len(args) > 1 else (result,)
last = out 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: except LuaError as e:
Logger.log_lua_error(f"Lua error in hook {hook_name} from {plugin_name}: {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 """ """ File watcher """
def _start_watcher(self): def _start_watcher(self):

View File

@@ -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 = [[
<style>
.plugin-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
text-align: center;
font-family: 'Arial', sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.plugin-footer {
background: #2d3748;
color: #cbd5e0;
padding: 20px;
text-align: center;
margin-top: 40px;
font-size: 0.9em;
}
.enhanced-code {
border-left: 4px solid #667eea;
padding-left: 10px;
}
</style>
]]
-- Insert CSS before closing head tag
local modified = html_insert_before(html_content, "</head>", 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 = [[
<div class="plugin-banner">
<strong>🚀 Enhanced by Plugin System</strong> |
<span style="opacity: 0.8;">Processing: ]] .. filepath .. [[</span>
</div>
]]
local modified = html_insert_after(html_content, "<body>", 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 = [[
<div class="plugin-footer">
<p>🔌 Powered by PyPost Plugin System</p>
<p>Generated: ]] .. os.date("%Y-%m-%d %H:%M:%S") .. [[</p>
<p style="font-size: 0.8em; opacity: 0.7;">
Processed through 4 sequential hooks
</p>
</div>
]]
local modified = html_insert_before(html_content, "</body>", 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 "<h1>Hello from Plugin!</h1><p>This is a simple GET route.</p>"
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 = [[
<!DOCTYPE html>
<html>
<head>
<title>File Browser</title>
<style>
body { font-family: Arial; max-width: 1000px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
.file-list { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; }
.file-item { padding: 10px; border-bottom: 1px solid #eee; }
.file-item:hover { background: #f9f9f9; }
h2 { color: #667eea; }
</style>
</head>
<body>
<h1>📁 File Browser</h1>
<div class="file-list">
<h2>HTML Files (]] .. #html_files .. [[)</h2>
]]
for i, file in ipairs(html_files) do
html = html .. ' <div class="file-item">📄 ' .. file .. '</div>\n'
end
html = html .. [[
</div>
<div class="file-list">
<h2>Markdown Files (]] .. #md_files .. [[)</h2>
]]
for i, file in ipairs(md_files) do
html = html .. ' <div class="file-item">📝 ' .. file .. '</div>\n'
end
html = html .. [[
</div>
</body>
</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 = [[
<!DOCTYPE html>
<html>
<head>
<title>Plugin Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f4f8; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; }
.card { background: white; padding: 25px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.card h3 { color: #667eea; margin-bottom: 15px; }
.stat { font-size: 3em; font-weight: bold; color: #667eea; margin: 10px 0; }
.feature-list { list-style: none; padding: 0; }
.feature-list li { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
.feature-list li:before { content: "✅ "; margin-right: 8px; }
.button { background: #667eea; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; text-decoration: none; display: inline-block; margin: 5px; }
.button:hover { background: #5568d3; }
.form-section { background: white; padding: 25px; border-radius: 10px; margin: 20px 0; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
input, textarea { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
.response { margin-top: 15px; padding: 15px; border-radius: 5px; display: none; }
.success { background: #d4edda; color: #155724; display: block; }
.error { background: #f8d7da; color: #721c24; display: block; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; }
</style>
</head>
<body>
<div class="header">
<h1>🔌 PyPost Plugin Dashboard</h1>
<p>Comprehensive Plugin System Demonstration</p>
</div>
<div class="container">
<!-- Statistics Cards -->
<div class="grid">
<div class="card">
<h3>📄 HTML Files</h3>
<div class="stat">]] .. html_count .. [[</div>
<p>Generated HTML documents</p>
</div>
<div class="card">
<h3>📝 Markdown Files</h3>
<div class="stat">]] .. md_count .. [[</div>
<p>Source markdown files</p>
</div>
<div class="card">
<h3>⏰ Server Time</h3>
<div class="stat" style="font-size: 1.5em;">]] .. os.date("%H:%M") .. [[</div>
<p>]] .. os.date("%Y-%m-%d") .. [[</p>
</div>
</div>
<!-- Features Section -->
<div class="card">
<h3>🚀 Plugin Features</h3>
<ul class="feature-list">
<li><strong>Hook Chaining:</strong> Multiple plugins modify content sequentially</li>
<li><strong>Priority System:</strong> Control execution order (0-100)</li>
<li><strong>GET Routes:</strong> Serve custom pages and APIs</li>
<li><strong>POST Routes:</strong> Handle form submissions</li>
<li><strong>HTML Manipulation:</strong> Find, replace, insert, wrap elements</li>
<li><strong>Markdown Operations:</strong> Add headers, sections, list items</li>
<li><strong>File I/O:</strong> Read, write, list files safely</li>
<li><strong>JSON Support:</strong> Parse and stringify data</li>
<li><strong>Hot Reloading:</strong> Plugins reload automatically on changes</li>
</ul>
</div>
<!-- API Endpoints -->
<div class="card">
<h3>🌐 Available API Endpoints</h3>
<p><strong>GET</strong> <code>/plugin/hello</code> - Simple greeting</p>
<p><strong>GET</strong> <code>/plugin/api/info</code> - JSON plugin info</p>
<p><strong>GET</strong> <code>/plugin/files</code> - File browser</p>
<p><strong>GET</strong> <code>/plugin/dashboard</code> - This page</p>
<p><strong>POST</strong> <code>/plugin/api/contact</code> - Submit contact form</p>
<p><strong>POST</strong> <code>/plugin/api/create-note</code> - Create markdown note</p>
<br>
<a href="/plugin/files" class="button">View Files</a>
<a href="/plugin/api/info" class="button">API Info (JSON)</a>
</div>
<!-- Contact Form Test -->
<div class="form-section">
<h3>📬 Test POST Route - Contact Form</h3>
<form id="contactForm">
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email">
<textarea name="message" rows="4" placeholder="Your Message" required></textarea>
<button type="submit" class="button">Submit</button>
</form>
<div id="contactResponse" class="response"></div>
</div>
<!-- Create Note Form -->
<div class="form-section">
<h3>📝 Test POST Route - Create Markdown Note</h3>
<form id="noteForm">
<input type="text" name="title" placeholder="Note Title" required>
<textarea name="content" rows="6" placeholder="Note content in markdown..." required></textarea>
<button type="submit" class="button">Create Note</button>
</form>
<div id="noteResponse" class="response"></div>
</div>
<!-- Hook Chain Visualization -->
<div class="card">
<h3>🔗 Hook Chain Processing Order</h3>
<p style="margin-bottom: 15px;">When a markdown file is rendered, these hooks process the HTML sequentially:</p>
<div style="display: flex; align-items: center; justify-content: space-between; padding: 15px; background: #f9f9f9; border-radius: 5px; margin: 10px 0;">
<span>1⃣ Add CSS (Priority 10)</span>
<span>→</span>
<span>2⃣ Add Banner (Priority 20)</span>
<span>→</span>
<span>3⃣ Enhance Code (Priority 30)</span>
<span>→</span>
<span>4⃣ Add Footer (Priority 40)</span>
</div>
<p style="margin-top: 15px; font-size: 0.9em; color: #666;">
Each hook receives the output from the previous hook, ensuring no modifications are lost.
</p>
</div>
<!-- Code Example -->
<div class="card">
<h3>💻 Example Plugin Code</h3>
<pre style="background: #2d3748; color: #e2e8f0; padding: 20px; border-radius: 5px; overflow-x: auto;"><code>-- Register a hook with priority
register_hook("post_render", function(filepath, html)
log("Processing: " .. filepath)
local modified = html_insert_after(html, "&lt;body&gt;",
"&lt;div class='banner'&gt;Hello!&lt;/div&gt;")
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)</code></pre>
</div>
</div>
<script>
// Contact form handler
document.getElementById('contactForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const responseDiv = document.getElementById('contactResponse');
try {
const response = await fetch('/plugin/api/contact', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
responseDiv.className = 'response success';
responseDiv.innerHTML = '<strong>✅ Success!</strong><br>' + result.message;
e.target.reset();
} else {
responseDiv.className = 'response error';
responseDiv.innerHTML = '<strong>❌ Error:</strong><br>' + result.error;
}
} catch (err) {
responseDiv.className = 'response error';
responseDiv.innerHTML = '<strong>❌ Error:</strong><br>' + err.message;
}
});
// Note form handler
document.getElementById('noteForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const responseDiv = document.getElementById('noteResponse');
try {
const response = await fetch('/plugin/api/create-note', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
responseDiv.className = 'response success';
responseDiv.innerHTML = '<strong>✅ Success!</strong><br>' +
result.message + '<br>File: <code>' + result.filename + '</code>';
e.target.reset();
} else {
responseDiv.className = 'response error';
responseDiv.innerHTML = '<strong>❌ Error:</strong><br>' + result.error;
}
} catch (err) {
responseDiv.className = 'response error';
responseDiv.innerHTML = '<strong>❌ Error:</strong><br>' + err.message;
}
});
</script>
</body>
</html>
]]
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("========================================")

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import threading
import subprocess import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import mimetypes import mimetypes
import json
from jsmin import jsmin # pip install jsmin from jsmin import jsmin # pip install jsmin
from pathlib import Path from pathlib import Path
@@ -107,6 +108,69 @@ def index_footer():
""" """
class MyHandler(BaseHTTPRequestHandler): 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): def do_GET(self):
req_path = self.path.lstrip("/") # normalize leading / req_path = self.path.lstrip("/") # normalize leading /