From c2d03efc573bd611a454f0adf2bc53014a3794a4 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:52:22 +0200 Subject: [PATCH] [Add] Team section & teams post --- file_graph_generator.py | 369 ++++++++++++++++++++++++++ file_tree.txt | 173 ++++++++++++ server/src/main.rs | 90 ++++++- website/api/posts.json | 17 +- website/api/{team.json => staff.json} | 3 +- website/api/teams.json | 44 +++ website/app.js | 83 +++++- website/assets/api.png | Bin 0 -> 2765510 bytes website/assets/harrpy.png | Bin 6927 -> 0 bytes website/assets/pitrex.png | Bin 0 -> 61586 bytes website/directs/api | 1 + website/directs/ct | 1 + website/index.html | 76 +++--- website/posts/Teams.md | 44 +++ website/style.css | 215 +++++++++++++++ 15 files changed, 1063 insertions(+), 53 deletions(-) create mode 100644 file_graph_generator.py create mode 100644 file_tree.txt rename website/api/{team.json => staff.json} (93%) create mode 100644 website/api/teams.json create mode 100644 website/assets/api.png delete mode 100644 website/assets/harrpy.png create mode 100644 website/assets/pitrex.png create mode 100644 website/directs/api create mode 100644 website/directs/ct create mode 100644 website/posts/Teams.md diff --git a/file_graph_generator.py b/file_graph_generator.py new file mode 100644 index 0000000..8bc662e --- /dev/null +++ b/file_graph_generator.py @@ -0,0 +1,369 @@ +""" +File Graph Generator + +Creates a tree-like visualization of files with their content, +respecting .gitignore patterns with proper tree rendering. +""" + +import argparse +import fnmatch +import logging +import os +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterator, List, Optional + +# ───────────────────────────── Logging Setup ────────────────────────────── + + +def setup_logging(level: int = logging.INFO) -> None: + """Configure structured logging to stderr (stdout is reserved for output).""" + logging.basicConfig( + stream=sys.stderr, + level=level, + format="%(asctime)s | %(levelname)-8s | %(message)s", + datefmt="%H:%M:%S", + ) + + +logger = logging.getLogger("file_graph") + + +# ─────────────────────────── Gitignore Engine ───────────────────────────── + + +@dataclass +class GitignoreRule: + """A single parsed gitignore rule.""" + + pattern: str + is_negation: bool = False + is_dir_only: bool = False + is_anchored: bool = False + is_relative: bool = False # contains / (not at start) + + +class GitignoreParser: + """Parse a .gitignore file into ordered rules.""" + + @staticmethod + def parse(path: Path) -> List[GitignoreRule]: + rules = [] + if not path.exists(): + return rules + try: + with path.open("r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.rstrip("\n\r") + # Skip empty lines and comments + if not line or line.startswith("#"): + continue + + is_negation = line.startswith("!") + if is_negation: + line = line[1:] + + # Handle escaped ! and # + if line.startswith("\\!") or line.startswith("\\#"): + line = line[1:] + + line = line.rstrip() + if not line: + continue + + is_dir_only = line.endswith("/") + if is_dir_only: + line = line[:-1] + + is_anchored = line.startswith("/") + if is_anchored: + line = line[1:] + + is_relative = "/" in line + + rules.append( + GitignoreRule( + pattern=line, + is_negation=is_negation, + is_dir_only=is_dir_only, + is_anchored=is_anchored, + is_relative=is_relative, + ) + ) + except OSError as e: + logger.warning(f"Could not read {path}: {e}") + return rules + + +class GitignoreChecker: + """Check if paths should be ignored, respecting per-directory .gitignore files.""" + + def __init__(self, root: Path): + self.root = root.resolve() + self._cache: dict[Path, List[GitignoreRule]] = {} + self._skip_dirs = { + ".git", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".tox", + ".venv", + "venv", + } + self._skip_files = { + "file_graph_generator.py", + "file_graph_generator_v2.py", + "file_graph_generator_v3.py", + "file_tree.txt", + } + + def _load(self, directory: Path) -> List[GitignoreRule]: + """Load rules for a directory (cached).""" + directory = directory.resolve() + if directory not in self._cache: + gitignore_path = directory / ".gitignore" + if gitignore_path.exists(): + logger.debug(f"Parsing gitignore: {gitignore_path}") + self._cache[directory] = GitignoreParser.parse(gitignore_path) + return self._cache[directory] + + def _match_rule( + self, rule: GitignoreRule, name: str, rel_path: str, is_dir: bool + ) -> bool: + """Check if a single rule matches.""" + if rule.is_dir_only and not is_dir: + return False + + # Anchored pattern: match only at .gitignore level + if rule.is_anchored: + return fnmatch.fnmatch(rel_path, rule.pattern) or fnmatch.fnmatch( + rel_path, rule.pattern + "/" + ) + + # Relative path pattern (contains /): match against rel_path + if rule.is_relative: + return fnmatch.fnmatch(rel_path, rule.pattern) or fnmatch.fnmatch( + rel_path, rule.pattern + "/" + ) + + # Basename pattern: match name at any level + return fnmatch.fnmatch(name, rule.pattern) + + def is_ignored(self, path: Path, is_dir: bool) -> bool: + """Check if path should be ignored, walking up to root.""" + name = path.name + + # Fast-path: always skip common VCS/build dirs and script/output files + if is_dir and name in self._skip_dirs: + logger.debug(f"Skipping common dir: {name}") + return True + if not is_dir and name in self._skip_files: + logger.debug(f"Skipping script/output file: {name}") + return True + + # Walk from current directory up to root + current = path.parent.resolve() + while True: + rules = self._load(current) + if rules: + rel_path = path.relative_to(current).as_posix() + ignored = False + + for rule in rules: + if self._match_rule(rule, name, rel_path, is_dir): + if rule.is_negation: + ignored = False + else: + ignored = True + + if ignored: + logger.debug(f"Ignored by gitignore: {path}") + return True + + if current == self.root: + break + current = current.parent + + return False + + +# ──────────────────────────── File Reading ──────────────────────────────── + +MAX_CONTENT_BYTES = 50 * 100 # ~50 lines avg + + +def is_binary(path: Path) -> bool: + """Fast binary detection using first 8KB chunk.""" + try: + with path.open("rb") as f: + chunk = f.read(8192) + if not chunk: + return False + if b"\x00" in chunk: + return True + if chunk[:2] in (b"\xff\xfe", b"\xfe\xff", b"\x1f\x8b"): + return True + return False + except OSError: + return True + + +def read_content(path: Path, max_lines: int = 50) -> Iterator[str]: + """Yield content lines with minimal memory footprint.""" + if is_binary(path): + yield "[binary file — content skipped]" + return + + try: + with path.open("r", encoding="utf-8", errors="replace") as f: + for i, line in enumerate(f): + if i >= max_lines: + remaining = sum(1 for _ in f) + yield f"... ({remaining} more lines)" + return + yield line.rstrip("\n") + except OSError as e: + yield f"[Error reading file: {e}]" + + +# ─────────────────────────── Tree Generator ─────────────────────────────── + + +def get_visible_children(path: Path, checker: GitignoreChecker) -> List[os.DirEntry]: + """Get sorted, filtered children of a directory.""" + try: + entries = list(os.scandir(path)) + except PermissionError: + logger.warning(f"Permission denied: {path}") + return [] + except OSError as e: + logger.error(f"Error scanning {path}: {e}") + return [] + + visible = [] + for entry in entries: + # Skip .gitignore files entirely + if entry.name == ".gitignore": + continue + + child_path = Path(entry.path) + is_dir = entry.is_dir() + + if not checker.is_ignored(child_path, is_dir): + visible.append(entry) + + # Sort: dirs first, then files, both alphabetically (case-insensitive) + visible.sort(key=lambda e: (not e.is_dir(), e.name.lower())) + return visible + + +def build_tree_lines( + path: Path, + checker: GitignoreChecker, + prefix: str = "", + max_lines: int = 50, + max_depth: Optional[int] = None, + current_depth: int = 0, +) -> Iterator[str]: + """Recursively yield tree lines. Correctly handles is_last filtering.""" + + if path.is_dir(): + if max_depth is not None and current_depth >= max_depth: + return + + children = get_visible_children(path, checker) + count = len(children) + + for i, entry in enumerate(children): + is_last = i == count - 1 + connector = "└── " if is_last else "├── " + name = entry.name + + if entry.is_dir(): + yield prefix + connector + name + "/" + extension = " " if is_last else "│ " + yield from build_tree_lines( + Path(entry.path), + checker, + prefix + extension, + max_lines, + max_depth, + current_depth + 1, + ) + else: + yield prefix + connector + name + content_prefix = prefix + (" " if is_last else "│ ") + " " + for content_line in read_content(Path(entry.path), max_lines): + yield content_prefix + content_line + + +# ─────────────────────────────── Main ───────────────────────────────────── + + +def main() -> None: + parser = argparse.ArgumentParser( + description="File tree generator with gitignore support", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s # Scan current directory + %(prog)s ./src -o tree.txt # Scan ./src, write to tree.txt + %(prog)s --max-depth 3 # Limit depth to 3 levels + %(prog)s --max-lines 20 # Show only 20 lines per file + %(prog)s -v # Verbose logging + %(prog)s --no-gitignore # Ignore all .gitignore files + """, + ) + parser.add_argument( + "directory", nargs="?", default=".", help="Root directory to scan" + ) + parser.add_argument("-o", "--output", default="file_tree.txt", help="Output file") + parser.add_argument( + "--max-depth", type=int, default=None, help="Maximum directory depth" + ) + parser.add_argument( + "--max-lines", type=int, default=50, help="Max lines to show per file" + ) + parser.add_argument( + "--no-gitignore", action="store_true", help="Disable .gitignore processing" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable debug logging" + ) + + args = parser.parse_args() + setup_logging(logging.DEBUG if args.verbose else logging.INFO) + + root = Path(args.directory).resolve() + if not root.is_dir(): + logger.error(f"Not a directory: {root}") + sys.exit(1) + + checker = GitignoreChecker(root) + if args.no_gitignore: + # Disable by making all paths return empty rules + checker._cache = {d: [] for d in checker._cache} + # Also override _load to always return empty + checker._load = lambda d: [] + logger.info("Gitignore processing disabled") + + try: + with open(args.output, "w", encoding="utf-8") as f: + # Print root + f.write(root.name + "/\n") + + # Print tree + for line in build_tree_lines( + root, checker, max_lines=args.max_lines, max_depth=args.max_depth + ): + f.write(line + "\n") + + logger.info(f"Output written to: {args.output}") + except OSError as e: + logger.error(f"Failed to write output: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/file_tree.txt b/file_tree.txt new file mode 100644 index 0000000..06a6a3a --- /dev/null +++ b/file_tree.txt @@ -0,0 +1,173 @@ +Anarchyphase-Website/ +├── server/ +│ ├── src/ +│ │ └── main.rs +│ │ use axum::{ +│ │ body::Body, +│ │ extract::{Path, State}, +│ │ http::{header, StatusCode, Uri}, +│ │ response::{IntoResponse, Response}, +│ │ ... (194 more lines) +│ ├── Cargo.lock +│ │ # This file is automatically @generated by Cargo. +│ │ # It is not intended for manual editing. +│ │ version = 4 +│ │ +│ │ [[package]] +│ │ ... (1114 more lines) +│ └── Cargo.toml +│ [package] +│ name = "server" +│ version = "0.1.0" +│ edition = "2021" +│ +│ ... (11 more lines) +├── website/ +│ ├── api/ +│ │ ├── backdrops.json +│ │ │ [ +│ │ │ { +│ │ │ "name": "horse_fog", +│ │ │ "destructive": false +│ │ │ }, +│ │ │ ... (26 more lines) +│ │ ├── posts.json +│ │ │ [ +│ │ │ { +│ │ │ "date": "2026-04-21, by AlexEmmet", +│ │ │ +│ │ │ "head_img": "village_sunset.png", +│ │ │ ... (32 more lines) +│ │ ├── staff.json +│ │ │ [ +│ │ │ { +│ │ │ "icon": "morice.png", +│ │ │ "name": "MoriceMC", +│ │ │ "priority": 0, +│ │ │ ... (48 more lines) +│ │ └── teams.json +│ │ [ +│ │ { +│ │ "title": "Anarchyphase Public Infrastructure", +│ │ "acronym": "API", +│ │ "leader": "NiniVX", +│ │ ... (5 more lines) +│ ├── assets/ +│ │ ├── ahnyokatzin.png +│ │ │ [binary file — content skipped] +│ │ ├── alexemmet.png +│ │ │ [binary file — content skipped] +│ │ ├── anarchyphase.png +│ │ │ [binary file — content skipped] +│ │ ├── api.png +│ │ │ [binary file — content skipped] +│ │ ├── banner.png +│ │ │ [binary file — content skipped] +│ │ ├── burrata.png +│ │ │ [binary file — content skipped] +│ │ ├── campsite.png +│ │ │ [binary file — content skipped] +│ │ ├── harrpy.png +│ │ │ [binary file — content skipped] +│ │ ├── harrpyrose.png +│ │ │ [binary file — content skipped] +│ │ ├── horse_fog.png +│ │ │ [binary file — content skipped] +│ │ ├── horse_rainbow.png +│ │ │ [binary file — content skipped] +│ │ ├── icon.png +│ │ │ [binary file — content skipped] +│ │ ├── morice.png +│ │ │ [binary file — content skipped] +│ │ ├── ninivx.png +│ │ │ [binary file — content skipped] +│ │ ├── outpost.png +│ │ │ [binary file — content skipped] +│ │ ├── pitrex.png +│ │ │ [binary file — content skipped] +│ │ ├── tower_large.png +│ │ │ [binary file — content skipped] +│ │ ├── tower_small.png +│ │ │ [binary file — content skipped] +│ │ ├── trailer.mp4 +│ │ │ [binary file — content skipped] +│ │ └── village_sunset.png +│ │ [binary file — content skipped] +│ ├── directs/ +│ │ └── api +│ │ https://dsc.gg/api +│ ├── posts/ +│ │ ├── AlexsOpinion.md +│ │ │ # AnarchyPhase Balance Changes +│ │ │ An advice & opinion +│ │ │ +│ │ │ ## Proposed by AlexEmmet +│ │ │ ### Edited by xLagging & reviewed mewjo_ +│ │ │ ... (201 more lines) +│ │ ├── Home.md +│ │ │ # The /Home command +│ │ │ +│ │ │ Hello Yall, Alex here, +│ │ │ +│ │ │ I made this website so a little selfinsert won't hurt. +│ │ │ ... (6 more lines) +│ │ ├── JoinAnarchyphase.md +│ │ │ # Join Anarchyphase +│ │ │ +│ │ │ Join the Anarchyphase community today! +│ │ └── ThisWebsite.md +│ │ # This website +│ │ +│ │ Hello Yall, Alex here, +│ │ +│ │ I made this website so a little selfinsert won't hurt. +│ │ ... (7 more lines) +│ ├── app.js +│ │ // Smooth scroll for nav links +│ │ document.querySelectorAll('a[href^="#"]').forEach((anchor) => { +│ │ anchor.addEventListener("click", function (e) { +│ │ e.preventDefault(); +│ │ const target = document.querySelector(this.getAttribute("href")); +│ │ ... (304 more lines) +│ ├── index.html +│ │ +│ │ +│ │
+│ │ +│ │ +│ │ ... (163 more lines) +│ ├── md-render.js +│ │ // Basic Markdown renderer +│ │ function renderMarkdown(md) { +│ │ let html = md +│ │ .replace(/&/g, "&") +│ │ .replace(/ +│ │ +│ │ +│ │ +│ │ +│ │ ... (39 more lines) +│ └── style.css +│ * { +│ margin: 0; +│ padding: 0; +│ box-sizing: border-box; +│ } +│ ... (987 more lines) +├── flake.lock +│ { +│ "nodes": { +│ "flake-utils": { +│ "inputs": { +│ "systems": "systems" +│ ... (55 more lines) +└── flake.nix + { + description = "Rust Development Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + ... (49 more lines) diff --git a/server/src/main.rs b/server/src/main.rs index a134e9b..1b2ae8d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -18,6 +18,46 @@ struct AppState { website_root: PathBuf, } +async fn git_pull(State(state): State${team.description}
+ ${ + hasDiscord + ? ` + + + Discord + + ` + : "" + } +q8vXIX-NxGMK3lv{c{K;bg#x
z1bZtEmfgh;hMTLMj_@21@N+V3w6IlZY~-p{wstgH; vZ!;?yCQUrI_kQT|-;*}f
z?9M(MwR5@Tjne0i5#P?Ra%Eg(th)c5&(+wW(ce5*{ci8)hkyO%dg(>f&i9}D+-}|F
zzf;a%KA(R6(ERm(1wy?}RKH(Yb#K?(d8?{}t9SF}DBo??x@TS8_r_S_W9({`nQGq0
z_I}@ymw(pccV~LUts;TTH~wzm GY_RgjCf6iUsXn6iZ
zvYhY%`?}|^IToB<{xkHB)YKWBoemA9(0NsxanPizSE>m!%ub=ZfiBe
zs|#lRP~NJ@Jn{ba|HtdQ>i=cVPmA`}erEkf4aeO&(01BS0{h``1-!`+!s}0Z#ox*-_6pJ*r8h(se44ij9>WgpAEm8
zr6+T}cCiVa{o?wMq_Wd*jm^(FDj!+(sOra-7xBiw(k}_Ktkj>9<>yd6x502mE@Sw_
z?e93*b3(61FMKL7*KbRIqCuvu_+3_*Qtx$NA%jU*Sv3g;y<@;Q2Oxk^L>1`X!?KvBQmT#w?)|fA0Tt{eSMYLZ6u@=DOPS7#j<^nmpd~^tDRHF6ppi
z*Uchy&6te6lZ>C7e<3#cd(vN11