291 lines
11 KiB
Python
291 lines
11 KiB
Python
from pathlib import Path
|
|
from log import Logger
|
|
import re
|
|
import json
|
|
|
|
PLUGINS_DIR = Path(__file__).parent / "plugins"
|
|
HTML_DIR = Path(__file__).parent / "../html"
|
|
MARKDOWN_DIR = Path(__file__).parent / ".." / "markdown"
|
|
|
|
class Actions:
|
|
def __init__(self):
|
|
"""Initialize Actions with directory paths"""
|
|
self.plugins_dir = PLUGINS_DIR
|
|
self.html_dir = HTML_DIR
|
|
self.markdown_dir = MARKDOWN_DIR
|
|
|
|
# Ensure directories exist
|
|
self.plugins_dir.mkdir(exist_ok=True)
|
|
self.html_dir.mkdir(exist_ok=True)
|
|
self.markdown_dir.mkdir(exist_ok=True)
|
|
|
|
# File I/O Operations
|
|
def _safe_read_file(self, path):
|
|
"""Safe file reading with path validation"""
|
|
try:
|
|
p = Path(path)
|
|
# Prevent directory traversal
|
|
if ".." in str(p.parts):
|
|
raise ValueError("Path traversal not allowed")
|
|
return p.read_text(encoding="utf-8")
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error reading file {path}: {e}")
|
|
return None
|
|
|
|
def _safe_write_file(self, path, content):
|
|
"""Safe file writing with path validation"""
|
|
try:
|
|
p = Path(path)
|
|
# Prevent directory traversal
|
|
if ".." in str(p.parts):
|
|
raise ValueError("Path traversal not allowed")
|
|
p.write_text(content, encoding="utf-8")
|
|
return True
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error writing file {path}: {e}")
|
|
return False
|
|
|
|
# HTML/Markdown Content Operations
|
|
def _read_content(self, base_dir, filename):
|
|
"""Read content from HTML or Markdown directory"""
|
|
try:
|
|
path = base_dir / filename
|
|
if not path.is_relative_to(base_dir):
|
|
raise ValueError("Invalid path")
|
|
if path.exists():
|
|
return path.read_text(encoding="utf-8")
|
|
return None
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error reading {filename}: {e}")
|
|
return None
|
|
|
|
def _write_content(self, base_dir, filename, content):
|
|
"""Write content to HTML or Markdown directory"""
|
|
try:
|
|
path = base_dir / filename
|
|
if not path.is_relative_to(base_dir):
|
|
raise ValueError("Invalid path")
|
|
path.write_text(content, encoding="utf-8")
|
|
return True
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error writing {filename}: {e}")
|
|
return False
|
|
|
|
def _list_files(self, base_dir, extension):
|
|
"""List files with given extension"""
|
|
try:
|
|
return [f.name for f in base_dir.glob(f"*{extension}")]
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error listing files: {e}")
|
|
return []
|
|
|
|
# HTML Manipulation Helpers
|
|
def _html_find_tag(self, html, tag):
|
|
"""Find first occurrence of HTML tag"""
|
|
pattern = f"<{tag}[^>]*>.*?</{tag}>"
|
|
match = re.search(pattern, html, re.DOTALL | re.IGNORECASE)
|
|
return match.group(0) if match else None
|
|
|
|
def _html_replace_tag(self, html, tag, new_content):
|
|
"""Replace HTML tag content"""
|
|
pattern = f"(<{tag}[^>]*>).*?(</{tag}>)"
|
|
return re.sub(pattern, f"\\1{new_content}\\2", html, flags=re.DOTALL | re.IGNORECASE)
|
|
|
|
def _html_insert_before(self, html, marker, content):
|
|
"""Insert content before a marker"""
|
|
return html.replace(marker, content + marker)
|
|
|
|
def _html_insert_after(self, html, marker, content):
|
|
"""Insert content after a marker"""
|
|
return html.replace(marker, marker + content)
|
|
|
|
def _html_wrap_content(self, html, tag, wrapper_tag, attrs=""):
|
|
"""Wrap tag content with another tag"""
|
|
pattern = f"(<{tag}[^>]*>)(.*?)(</{tag}>)"
|
|
def replacer(match):
|
|
open_tag, content, close_tag = match.groups()
|
|
return f"{open_tag}<{wrapper_tag} {attrs}>{content}</{wrapper_tag}>{close_tag}"
|
|
return re.sub(pattern, replacer, html, flags=re.DOTALL | re.IGNORECASE)
|
|
|
|
def _html_remove_tag(self, html, tag, keep_content=True):
|
|
"""Remove HTML tag, optionally keeping its content"""
|
|
if keep_content:
|
|
pattern = f"<{tag}[^>]*>(.*?)</{tag}>"
|
|
return re.sub(pattern, r"\1", html, flags=re.DOTALL | re.IGNORECASE)
|
|
else:
|
|
pattern = f"<{tag}[^>]*>.*?</{tag}>"
|
|
return re.sub(pattern, "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
|
|
def _html_add_class(self, html, tag, class_name):
|
|
"""Add a CSS class to all instances of a tag"""
|
|
def add_class_to_tag(match):
|
|
tag_content = match.group(0)
|
|
if 'class=' in tag_content:
|
|
return re.sub(
|
|
r'class="([^"]*)"',
|
|
f'class="\\1 {class_name}"',
|
|
tag_content
|
|
)
|
|
else:
|
|
return tag_content.replace('>', f' class="{class_name}">', 1)
|
|
|
|
pattern = f"<{tag}[^>]*>"
|
|
return re.sub(pattern, add_class_to_tag, html, flags=re.IGNORECASE)
|
|
|
|
def _html_add_attribute(self, html, tag, attr_name, attr_value):
|
|
"""Add an attribute to all instances of a tag"""
|
|
def add_attr_to_tag(match):
|
|
tag_content = match.group(0)
|
|
if attr_name in tag_content:
|
|
return tag_content # Don't duplicate
|
|
return tag_content.replace('>', f' {attr_name}="{attr_value}">', 1)
|
|
|
|
pattern = f"<{tag}[^>]*>"
|
|
return re.sub(pattern, add_attr_to_tag, html, flags=re.IGNORECASE)
|
|
|
|
# Markdown Manipulation Helpers
|
|
def _md_add_header(self, markdown, level, text):
|
|
"""Add header to markdown"""
|
|
prefix = "#" * level
|
|
return f"{prefix} {text}\n\n{markdown}"
|
|
|
|
def _md_replace_section(self, markdown, header, new_content):
|
|
"""Replace markdown section"""
|
|
# Find section starting with header
|
|
pattern = f"(#{1,6}\\s+{re.escape(header)}.*?)(?=#{1,6}\\s+|$)"
|
|
return re.sub(pattern, f"## {header}\n\n{new_content}\n\n", markdown, flags=re.DOTALL)
|
|
|
|
def _md_append_content(self, markdown, content):
|
|
"""Append content to markdown"""
|
|
return markdown.rstrip() + "\n\n" + content
|
|
|
|
def _md_prepend_content(self, markdown, content):
|
|
"""Prepend content to markdown"""
|
|
return content + "\n\n" + markdown.lstrip()
|
|
|
|
def _md_insert_at_position(self, markdown, position, content):
|
|
"""Insert content at specific line position"""
|
|
lines = markdown.split('\n')
|
|
if 0 <= position <= len(lines):
|
|
lines.insert(position, content)
|
|
return '\n'.join(lines)
|
|
|
|
def _md_find_section(self, markdown, header):
|
|
"""Find a markdown section and return its content"""
|
|
pattern = f"#{1,6}\\s+{re.escape(header)}\\s*\n(.*?)(?=#{1,6}\\s+|$)"
|
|
match = re.search(pattern, markdown, re.DOTALL)
|
|
return match.group(1).strip() if match else None
|
|
|
|
def _md_remove_section(self, markdown, header):
|
|
"""Remove a markdown section"""
|
|
pattern = f"#{1,6}\\s+{re.escape(header)}.*?(?=#{1,6}\\s+|$)"
|
|
return re.sub(pattern, "", markdown, flags=re.DOTALL).strip()
|
|
|
|
def _md_add_list_item(self, markdown, item, ordered=False):
|
|
"""Add an item to the end of markdown (as list item)"""
|
|
prefix = "1. " if ordered else "- "
|
|
return markdown.rstrip() + f"\n{prefix}{item}\n"
|
|
|
|
def _md_wrap_code_block(self, markdown, language=""):
|
|
"""Wrap content in a markdown code block"""
|
|
return f"```{language}\n{markdown}\n```"
|
|
|
|
# JSON Conversion Helpers
|
|
def _table_to_json(self, lua_table):
|
|
"""Convert Lua table to JSON string"""
|
|
try:
|
|
# Handle lupa tables by converting to dict
|
|
if hasattr(lua_table, 'items'):
|
|
py_dict = dict(lua_table.items())
|
|
elif hasattr(lua_table, '__iter__') and not isinstance(lua_table, str):
|
|
# Handle array-like lua tables
|
|
py_dict = list(lua_table)
|
|
else:
|
|
py_dict = dict(lua_table)
|
|
|
|
return json.dumps(py_dict, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error converting table to JSON: {e}")
|
|
return "{}"
|
|
|
|
def _json_to_table(self, json_str):
|
|
"""Convert JSON string to Lua table (Python dict)"""
|
|
try:
|
|
return json.loads(json_str)
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error parsing JSON: {e}")
|
|
return {}
|
|
|
|
def _json_parse(self, json_str):
|
|
"""Alias for _json_to_table"""
|
|
return self._json_to_table(json_str)
|
|
|
|
def _json_stringify(self, data):
|
|
"""Convert Python object to JSON string"""
|
|
try:
|
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error stringifying to JSON: {e}")
|
|
return "{}"
|
|
|
|
# Utility Functions
|
|
def _string_split(self, text, delimiter):
|
|
"""Split string by delimiter"""
|
|
return text.split(delimiter)
|
|
|
|
def _string_join(self, items, delimiter):
|
|
"""Join list of strings with delimiter"""
|
|
try:
|
|
return delimiter.join(str(item) for item in items)
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error joining strings: {e}")
|
|
return ""
|
|
|
|
def _string_replace(self, text, old, new):
|
|
"""Replace all occurrences of old with new"""
|
|
return text.replace(old, new)
|
|
|
|
def _string_match(self, text, pattern):
|
|
"""Match string against regex pattern"""
|
|
try:
|
|
match = re.search(pattern, text)
|
|
return match.group(0) if match else None
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error in regex match: {e}")
|
|
return None
|
|
|
|
def _string_match_all(self, text, pattern):
|
|
"""Find all matches of pattern in text"""
|
|
try:
|
|
return re.findall(pattern, text)
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error in regex findall: {e}")
|
|
return []
|
|
|
|
def _file_exists(self, path):
|
|
"""Check if file exists"""
|
|
try:
|
|
return Path(path).exists()
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error checking file existence: {e}")
|
|
return False
|
|
|
|
def _file_size(self, path):
|
|
"""Get file size in bytes"""
|
|
try:
|
|
return Path(path).stat().st_size
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error getting file size: {e}")
|
|
return 0
|
|
|
|
def _list_directory(self, path):
|
|
"""List all files and directories in path"""
|
|
try:
|
|
p = Path(path)
|
|
if not p.exists():
|
|
return []
|
|
return [item.name for item in p.iterdir()]
|
|
except Exception as e:
|
|
Logger.log_lua_error(f"Error listing directory: {e}")
|
|
return []
|