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:
184
.vscode/globals.lua
vendored
Normal file
184
.vscode/globals.lua
vendored
Normal 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
|
||||||
|
|
||||||
89
.vscode/settings.json
vendored
89
.vscode/settings.json
vendored
@@ -1,27 +1,66 @@
|
|||||||
{
|
{
|
||||||
"Lua.diagnostics.globals": [
|
// Lua Language Server settings
|
||||||
"read_file",
|
"Lua.runtime.version": "Lua 5.4",
|
||||||
"write_file",
|
"Lua.workspace.checkThirdParty": false,
|
||||||
"read_html",
|
"Lua.workspace.library": [
|
||||||
"write_html",
|
"./.vscode"
|
||||||
"list_html_files",
|
],
|
||||||
"read_markdown",
|
"Lua.hint.enable": true,
|
||||||
"write_markdown",
|
"Lua.completion.callSnippet": "Both",
|
||||||
"list_markdown_files",
|
"Lua.completion.autoRequire": false,
|
||||||
"html_find_tag",
|
|
||||||
"html_replace_tag",
|
// Diagnostics: treat these globals as known
|
||||||
"html_insert_before",
|
"Lua.diagnostics.globals": [
|
||||||
"html_insert_after",
|
"add_route",
|
||||||
"html_wrap_content",
|
"add_get_route",
|
||||||
"md_add_header",
|
"add_post_route",
|
||||||
"md_replace_section",
|
"register_hook",
|
||||||
"md_append_content",
|
"list_html_files",
|
||||||
"table_to_json",
|
"list_markdown_files",
|
||||||
"json_to_table",
|
"write_markdown",
|
||||||
"add_route",
|
"html_add_class",
|
||||||
"register_hook",
|
"html_add_attribute",
|
||||||
"log",
|
"html_insert_before",
|
||||||
"log_warn",
|
"html_insert_after",
|
||||||
"log_error"
|
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
196
lua/Actions.py
196
lua/Actions.py
@@ -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}>)"
|
||||||
@@ -102,45 +106,185 @@ class Actions:
|
|||||||
open_tag, content, close_tag = match.groups()
|
open_tag, content, close_tag = match.groups()
|
||||||
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)
|
||||||
|
|
||||||
|
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
|
# Markdown Manipulation Helpers
|
||||||
@staticmethod
|
|
||||||
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
|
||||||
|
|
||||||
|
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
|
# JSON Conversion Helpers
|
||||||
@staticmethod
|
|
||||||
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
|
||||||
py_dict = dict(lua_table)
|
if hasattr(lua_table, 'items'):
|
||||||
return json.dumps(py_dict)
|
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:
|
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 []
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ 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"
|
||||||
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 PluginManager:
|
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)
|
if m not in self.routes:
|
||||||
Logger.log_lua_info(f"Plugin {plugin_name} registered route {p}")
|
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)
|
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):
|
||||||
|
|||||||
530
lua/plugins/examples/demo/test.lua
Normal file
530
lua/plugins/examples/demo/test.lua
Normal 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, "<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)</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("========================================")
|
||||||
7
lua/plugins/examples/post.lua
Normal file
7
lua/plugins/examples/post.lua
Normal 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)
|
||||||
1155
lua/readme.md
1155
lua/readme.md
File diff suppressed because it is too large
Load Diff
64
webserver.py
64
webserver.py
@@ -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 /
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user