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}[^>]*>.*?" 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}[^>]*>).*?()" 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}[^>]*>)(.*?)()" def replacer(match): open_tag, content, close_tag = match.groups() return f"{open_tag}<{wrapper_tag} {attrs}>{content}{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}[^>]*>(.*?)" return re.sub(pattern, r"\1", html, flags=re.DOTALL | re.IGNORECASE) else: pattern = f"<{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 []