commit aafabba59527b9b7c99e7c0caf6ed66a9da9a9ad Author: Philipp Urbschat Date: Sat Sep 12 21:10:04 2026 +0200 Initial commit: Clean portfolio codebase with security hardening diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..440896f --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Environment & Secrets +.env +*.env +**/.env +!.env.example +!*.env.example +!**/.env.example + +# OAuth & Sensitive Data +client_secret.json +**/client_secret.json +tokens.json +**/tokens.json +usage_stats.json +**/usage_stats.json +*.sql +*.sqlite + +# Dependencies +node_modules/ +**/node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS & IDE Files +.DS_Store +**/.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Temporary & Export Files +*export* +*.tmp +*.log diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..1e4533b --- /dev/null +++ b/.htaccess @@ -0,0 +1,30 @@ +# DATEI: ./.htaccess +Options -Indexes +RewriteEngine On + +# Interne PHP- und Template-Verzeichnisse vor direktem Webzugriff sperren +RewriteRule ^(home|philcore)/(app|core|views|src)/ - [F,L] + +# Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen + + Require all denied + + + + Require all denied + + +# 1. Dynamische XML/TXT Dateien -> Ab zum SeoController +RewriteRule ^sitemap\.xml$ home/public/index.php?url=seo/sitemap [L,QSA] +RewriteRule ^robots\.txt$ home/public/index.php?url=seo/robots [L,QSA] + +# 2. Domain-Einstieg +RewriteRule ^$ home/public/index.php [L] + +# 3. Echte Assets schützen +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} !^/home/public/ + +# 4. Globales Routing +RewriteRule ^(.*)$ home/public/index.php?url=$1 [L,QSA] \ No newline at end of file diff --git a/admin-tools/index.php b/admin-tools/index.php new file mode 100644 index 0000000..22a6819 --- /dev/null +++ b/admin-tools/index.php @@ -0,0 +1,131 @@ + + + + + + + Admin: Hash-Generator + + + + +
+
+

ADMIN TOOLS

+

Password Hash Generator

+
+ +
+ +
+
+
+ + +
+ +
+ + +
+ Original:

+ Hash (klicken zum Kopieren):
+ +
+ + + BACK +
+
+ + + + \ No newline at end of file diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..95a3f21 --- /dev/null +++ b/auth.php @@ -0,0 +1,38 @@ + '/', + 'httponly' => true, + 'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test + ]); + session_start(); +} + +// 1. Ist der User eingeloggt? +if (!isset($_SESSION['user_email'])) { + header('Location: /'); + exit(); +} + +// 2. Browser-Check gegen Session Hijacking +if (!isset($_SESSION['user_agent'])) { + $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT']; +} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) { + session_destroy(); + header('Location: /'); + exit(); +} + +// 3. Rechte-Check (Admins dürfen immer rein) +$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true; +$hasProjectAccess = isset($current_project) && isset($_SESSION['user_projects']) && in_array($current_project, $_SESSION['user_projects']); + +if (!$isAdmin && !$hasProjectAccess) { + // Kein Admin und kein spezifischer Projekt-Zugriff + header('Location: /'); + exit(); +} +?> \ No newline at end of file diff --git a/cooles-feature/index.php b/cooles-feature/index.php new file mode 100644 index 0000000..430acff --- /dev/null +++ b/cooles-feature/index.php @@ -0,0 +1,286 @@ + + + + + + + Erster Login-Check: Erfolgreich! + + + +
+
+

Willkommen beim ersten Check! 🎉

+ +
+ +
+

Glückwunsch! Deine Zugangsdaten sind korrekt und du bist erfolgreich im System gelandet.

+

Um sicherzugehen, dass auch interaktive Elemente bei dir funktionieren, teste bitte unser hochmodernes Belohnungsmodul:

+ +
+
+

Hinweis: Sound bitte einschalten!

+
+
+ + +
+ + + + \ No newline at end of file diff --git a/dinos/.env.example b/dinos/.env.example new file mode 100644 index 0000000..c094e45 --- /dev/null +++ b/dinos/.env.example @@ -0,0 +1 @@ +GEMINI_API_KEY="your_gemini_api_key_here" diff --git a/dinos/.htaccess b/dinos/.htaccess new file mode 100644 index 0000000..6fbc535 --- /dev/null +++ b/dinos/.htaccess @@ -0,0 +1,13 @@ +# 2026-01-08 18:45:00 +# Verhindert, dass Fremde deine Schlüssel-Dateien herunterladen können. +# PHP kann sie weiterhin lesen, aber der Browser nicht. + +# Verhindert, dass Fremde Schlüssel- und Status-Dateien herunterladen können. + + Require all denied + + +# Erlaubt Zugriff auf dinos/auth.php (wichtig für Google OAuth Callback) + + Require all granted + \ No newline at end of file diff --git a/dinos/Google Photo API Upload.pdf b/dinos/Google Photo API Upload.pdf new file mode 100644 index 0000000..0cf5ee3 Binary files /dev/null and b/dinos/Google Photo API Upload.pdf differ diff --git a/dinos/apis.php b/dinos/apis.php new file mode 100644 index 0000000..56d9d4f --- /dev/null +++ b/dinos/apis.php @@ -0,0 +1,109 @@ + + + + + + + API Model Explorer + + + + +
+
+

🚀 Google API Model Explorer

+

Diese Modelle sind aktuell für deinen Key freigeschaltet.

+
+ + +
+ + + + + + + + + + + + + + + + + +
ID (für den Code)AnzeigenameFeatures
+ + + + +
+
+
+
+ + + + + +
+
+
+ +
+ Fehler: Modelle konnten nicht geladen werden. Prüfe deinen API-Key. +
+ + +
+ Stand: | Phili's Dino-Labor 🦕 +
+
+ + + \ No newline at end of file diff --git a/dinos/auth.php b/dinos/auth.php new file mode 100644 index 0000000..840f793 --- /dev/null +++ b/dinos/auth.php @@ -0,0 +1,65 @@ +authenticate($_GET['code']); + if ($success) { + echo '

Verbindung erfolgreich!

'; + echo '

Der Token wurde gespeichert (tokens.json). Du kannst dieses Fenster schließen.

'; + echo 'Zurück zum Dino-Generator'; + } else { + echo '

Fehler beim Login

'; + echo '

Konnte Code nicht gegen Token tauschen.

'; + } + exit; +} + +// Szenario 2: Fehler von Google +if (isset($_GET['error'])) { + die("Google Fehler: " . htmlspecialchars($_GET['error'])); +} + +// Szenario 3: Startseite - Button anzeigen +?> + + + + + Google Photos Verbindung + + + +
+

Google Photos verbinden

+

Erlaube der Dino-App, Bilder in dein Album zu laden.

+ + isAuthenticated()): ?> +

✅ Bereits verbunden!

+

(Erneuter Klick erneuert die Verbindung)

+ + + + Mit Google anmelden + +
+ + \ No newline at end of file diff --git a/dinos/google_helper.php b/dinos/google_helper.php new file mode 100644 index 0000000..a12f309 --- /dev/null +++ b/dinos/google_helper.php @@ -0,0 +1,150 @@ +clientId = $conf['client_id']; + $this->clientSecret = $conf['client_secret']; + $this->redirectUri = $redirectUri; + } + + // --- AUTH LOGIK (Unverändert) --- + public function getAuthUrl() { + $params = [ + 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, 'response_type' => 'code', + 'scope' => 'https://www.googleapis.com/auth/photoslibrary.appendonly', + 'access_type' => 'offline', 'prompt' => 'consent' + ]; + return 'https://accounts.google.com/o/oauth2/auth?' . http_build_query($params); + } + + public function authenticate($code) { + $data = $this->makeRequest('https://oauth2.googleapis.com/token', [ + 'code' => $code, 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, + 'redirect_uri' => $this->redirectUri, 'grant_type' => 'authorization_code' + ]); + if (isset($data['access_token'])) { $this->saveTokens($data); return true; } + return false; + } + + private function getAccessToken() { + $tokens = $this->loadTokens(); + if (!$tokens) return null; + if (time() >= ($tokens['created'] + $tokens['expires_in'] - 60)) { + return $this->refreshToken($tokens['refresh_token']); + } + return $tokens['access_token']; + } + + private function refreshToken($refresh) { + $data = $this->makeRequest('https://oauth2.googleapis.com/token', [ + 'refresh_token' => $refresh, 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, 'grant_type' => 'refresh_token' + ]); + if (isset($data['access_token'])) { + $data['refresh_token'] = $refresh; // Alten Refresh Token behalten + $this->saveTokens($data); + return $data['access_token']; + } + return null; + } + + // --- NEU: UPLOAD LOGIK --- + + public function uploadImage($imageData, $description) { + $token = $this->getAccessToken(); + if (!$token) return ['error' => 'Nicht eingeloggt']; + + // 1. Das Album finden oder erstellen + $albumId = $this->getOrCreateAlbum($token); + + // 2. Die Bytes hochladen (Schritt 1 von 2 bei Google) + $uploadUrl = 'https://photoslibrary.googleapis.com/v1/uploads'; + $ch = curl_init($uploadUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer $token", + "Content-type: application/octet-stream", + "X-Goog-Upload-Protocol: raw" + ]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $imageData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $uploadToken = curl_exec($ch); + curl_close($ch); + + if (!$uploadToken) return ['error' => 'Upload der Bilddaten fehlgeschlagen']; + + // 3. Das MediaItem erstellen (Schritt 2 von 2) + $createUrl = 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate'; + $body = [ + "albumId" => $albumId, + "newMediaItems" => [[ + "description" => $description, + "simpleMediaItem" => ["uploadToken" => $uploadToken] + ]] + ]; + + $ch = curl_init($createUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token", "Content-type: application/json"]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = json_decode(curl_exec($ch), true); + curl_close($ch); + + return $result; + } + + // Hilfsfunktion: Album Management + private function getOrCreateAlbum($token) { + // Haben wir die ID schon gespeichert? + if (file_exists($this->albumFile)) { + return trim(file_get_contents($this->albumFile)); + } + + // Nein, wir erstellen es neu. + // (Hinweis: Wir könnten erst suchen, aber Erstellen ist sicherer/einfacher) + $url = 'https://photoslibrary.googleapis.com/v1/albums'; + $body = json_encode(["album" => ["title" => "Dino Wissenskarten"]]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token", "Content-type: application/json"]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = json_decode(curl_exec($ch), true); + curl_close($ch); + + if (isset($response['id'])) { + file_put_contents($this->albumFile, $response['id']); + return $response['id']; + } + return null; + } + + // --- UTIL --- + private function makeRequest($url, $fields) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $res = curl_exec($ch); curl_close($ch); + return json_decode($res, true); + } + private function saveTokens($data) { $data['created'] = time(); file_put_contents($this->tokenFile, json_encode($data)); } + private function loadTokens() { return file_exists($this->tokenFile) ? json_decode(file_get_contents($this->tokenFile), true) : null; } + public function isAuthenticated() { return file_exists($this->tokenFile); } +} +?> \ No newline at end of file diff --git a/dinos/index.php b/dinos/index.php new file mode 100644 index 0000000..0d44e53 --- /dev/null +++ b/dinos/index.php @@ -0,0 +1,1179 @@ + 0, 'total_pro' => 0, 'total_ultra' => 0, 'dates' => []]; + } + $content = file_get_contents($file); + $data = $content ? json_decode($content, true) : []; + + // Fallback/Initialisierung + if (!isset($data['total_eco'])) $data['total_eco'] = 0; + if (!isset($data['total_pro'])) $data['total_pro'] = 0; + if (!isset($data['total_ultra'])) $data['total_ultra'] = 0; + if (!isset($data['dates'])) $data['dates'] = []; + + return $data; +} + +// AJAX Handler für Stats Logging +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'log_success') { + $stats = getStats($logFile); + $today = date('Y-m-d'); + $type = isset($_POST['type']) ? $_POST['type'] : 'eco'; // eco, pro, ultra + + // Gesamt hochzählen + if ($type === 'ultra') $stats['total_ultra']++; + elseif ($type === 'pro') $stats['total_pro']++; + else $stats['total_eco']++; + + // Heute initialisieren (falls noch nicht da) + if (!isset($stats['dates'][$today])) { + $stats['dates'][$today] = ['eco' => 0, 'pro' => 0, 'ultra' => 0]; + } + // Sicherstellen dass alle Keys existieren + if (!isset($stats['dates'][$today]['eco'])) $stats['dates'][$today]['eco'] = 0; + if (!isset($stats['dates'][$today]['pro'])) $stats['dates'][$today]['pro'] = 0; + if (!isset($stats['dates'][$today]['ultra'])) $stats['dates'][$today]['ultra'] = 0; + + $stats['dates'][$today][$type]++; + + // REIHENFOLGE ERZWINGEN (SORTIERUNG) + foreach ($stats['dates'] as $dateKey => $dayStats) { + $stats['dates'][$dateKey] = [ + 'eco' => isset($dayStats['eco']) ? $dayStats['eco'] : 0, + 'pro' => isset($dayStats['pro']) ? $dayStats['pro'] : 0, + 'ultra' => isset($dayStats['ultra']) ? $dayStats['ultra'] : 0 + ]; + } + + // Hauptstruktur sortieren + $orderedStats = [ + 'total_eco' => $stats['total_eco'], + 'total_pro' => $stats['total_pro'], + 'total_ultra' => $stats['total_ultra'], + 'dates' => $stats['dates'] + ]; + + // Speichern + file_put_contents($logFile, json_encode($orderedStats, JSON_PRETTY_PRINT)); + + header('Content-Type: application/json'); + echo json_encode([ + 'status' => 'logged', + 'today_eco' => $stats['dates'][$today]['eco'], + 'today_pro' => $stats['dates'][$today]['pro'], + 'today_ultra' => $stats['dates'][$today]['ultra'], + 'total_eco' => $stats['total_eco'], + 'total_pro' => $stats['total_pro'], + 'total_ultra' => $stats['total_ultra'] + ]); + exit; +} + +// Stats laden für Initial-Anzeige +$currentStats = getStats($logFile); +$todayDate = date('Y-m-d'); +$todayEco = isset($currentStats['dates'][$todayDate]['eco']) ? $currentStats['dates'][$todayDate]['eco'] : 0; +$todayPro = isset($currentStats['dates'][$todayDate]['pro']) ? $currentStats['dates'][$todayDate]['pro'] : 0; +$todayUltra = isset($currentStats['dates'][$todayDate]['ultra']) ? $currentStats['dates'][$todayDate]['ultra'] : 0; + +$totalEco = $currentStats['total_eco']; +$totalPro = $currentStats['total_pro']; +$totalUltra = $currentStats['total_ultra']; +?> + + + + + + + Dino Wissenskarten Generator + + + + + + + + + + +
+ + +
+ +

+ 🦕 + + Dino Wissenskarten + +

+

Generiere fotorealistische Bilder und entdecke spannende Fakten.

+
+ + +
+
+ + + + + +
+ + + +
+ + +
+ + + +
+ + + +
+
+
+ + +
+ + + + + + + + +
+
+ 🦖 +
+

Tippe einen Dino-Namen ein und schau zu, was passiert.

+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/dinos/prompts.js b/dinos/prompts.js new file mode 100644 index 0000000..db41195 --- /dev/null +++ b/dinos/prompts.js @@ -0,0 +1,94 @@ +/* + DINO LERNTAFEL - PROMPT KONFIGURATION (FINAL) + */ + + const DinoPrompts = { + + // 1. PROMPT FÜR DIE FAKTEN (Gemini Flash) + // Ziel: Sauberes JSON ohne Markdown, Zahlen als echte Numbers + getFacts: (term) => ` + Du bist ein Experte für Paläontologie. Erstelle ein Daten-Profil für einen "${term}". + + WICHTIG ZUM FORMAT: + - Antworte NUR mit dem rohen JSON-String. + - KEINE Markdown-Formatierung (kein \`\`\`json). + - Alle Text-Werte auf DEUTSCH. + + JSON STRUKTUR: + { + "name": "Name des Dinos (z.B. Tyrannosaurus Rex)", + "is_fictional": true, // true bei Fantasie, false bei echten Dinos + + // Brücke zum Bild-Generator: + "visual_details": "Beschreibe visuell: Körperbau, Hautstruktur (Schuppen/Federn/Panzer), markante Merkmale (Hörner, Kämme) und Farbe. Sei präzise.", + + "fun_fact": "Max. 10 Wörter. Ein spannendes Detail für Kinder.", + "translation": "Namensbedeutung (z.B. 'König der Tyrannenechsen')", + "period": "Zeitraum Name (z.B. Oberkreide)", + "years": "Zeitraum Zahlen (z.B. vor 68–66 Mio. Jahren)", + + "mya_start": 68, // Zahl (Millionen Jahre) + "mya_end": 66, // Zahl (Millionen Jahre) + + "height": 5.5, // Höhe in Metern (nur die Zahl als Number/Float) + "length": 12.0, // Länge in Metern (nur die Zahl als Number/Float) + "weight": 8000, // Gewicht in KG (nur die Zahl als Integer) + "diet": "Fleisch" // Genau einer dieser Werte: 'Pflanzen', 'Fleisch', 'Alles', 'Fisch' + } + `, + +// 2. PROMPT FÜR DAS BILD (Generalisiert für alle Habitate) +getImage: (data) => ` + [KONZEPT]: Ein "Environmental Wide Shot" (Weitwinklige Landschaftsaufnahme). + [DAS MOTIV]: Eine weite prähistorische Landschaft (${data.period}). In der MITTLEREN DISTANZ befindet sich ein einzelner ${data.name}. + + [VISUELLE DETAILS]: + ${data.visual_details} + + [KOMPOSITION & LAYOUT - DIE "ANTI-CROP" REGELN]: + - DISTANZ: Die Kamera ist weit entfernt. Der ${data.name} darf maximal 70% der gesamten Bildhöhe einnehmen. + + - VORDERGRUND: Es muss signifikant viel Umgebung UNTER dem Tier sichtbar sein. + - Wir müssen klar sehen, worauf oder worin das Tier steht (z.B. Waldboden, Wasser, Sand, Fels). Die Füße/Basis dürfen den unteren Bildrand NICHT berühren. + + - HINTERGRUND OBEN: Es muss signifikant viel Umgebung ÜBER dem Kopf sichtbar sein (Negativraum). + - Egal ob Himmel, Baumkronen oder Felswand: Dieser Bereich muss visuell "ruhig" und hell genug sein, damit dunkler Text darauf lesbar ist. + + [FEHLER-VERMEIDUNG]: + - KEIN enges Portrait. Das Tier muss "atmen" können. + - Wenn das Tier oben oder unten anstößt, ist die Kamera zu nah -> Gehe weiter weg. + + [ATMOSPHÄRE]: + - Habitat: Passend zur Spezies (z.B. dichter Dschungel, Sumpf, Küste oder offene Ebene). + - Licht: Helles, diffuses Tageslicht für gute Sichtbarkeit. + + [REALISMUS]: + - ${data.is_fictional ? "Glaubwürdige Biologie." : "Wissenschaftlich akkurat."} + - 8k Auflösung, fotorealistisch. + + [TECHNISCHES]: + - Seitenverhältnis: 16:10 + - Objektiv: Weitwinkel (ca. 24mm). +`, + // 3. PROMPT FÜR DEN ZUFALLSGENERATOR (Gemini Flash) + getRandom: () => ` + Nenne mir ein zufälliges, spannendes prähistorisches Tier aus dem Zeitalter der Dinosaurier (Mesozoikum) oder davor. + + WICHTIGE REGELN: + 1. Die Zeitgrenze ist das Massenaussterben vor 66 Mio. Jahren. (Keine Mammuts etc.) + 2. SPRACHE: Die 'tagline' MUSS zwingend auf DEUTSCH sein. + 3. VIELFALT: Sei kreativ! Nenne nicht nur die bekanntesten Tiere. Grabe tief in der Datenbank der Paläontologie. Bringe gerne auch unbekanntere, bizarre oder bunte Arten. Wähle jedes Mal etwas ganz anderes. + + Erlaubte Gruppen: + - Dinosaurier (Theropoden, Sauropoden, Ornithischia...) + - Flugsaurier (Pterosaurier - aber nicht immer Quetzalcoatlus!) + - Meeresechsen (Mosasaurier, Plesiosaurier, Ichthyosaurier) + - Urzeitliche Krokodile, Amphibien oder frühe Synapsiden (z.B. Dimetrodon). + + Antworte als reines JSON: + { + "name": "Name des Tiers (Einfacher Name, z.B. Deinocheirus statt Deinocheirus mirificus )", + "tagline": "Eine sehr kurze, knackige Beschreibung auf Deutsch (max 6-8 Wörter)" + } + ` +}; \ No newline at end of file diff --git a/dinos/test.html b/dinos/test.html new file mode 100644 index 0000000..451d5f8 --- /dev/null +++ b/dinos/test.html @@ -0,0 +1,1049 @@ + + + + Dino Wissenskarten Generator + + + + + + + + + + +
+ + +
+ +

+ 🦕 + + Dino Wissenskarten + +

+

Generiere fotorealistische Bilder und entdecke spannende Fakten.

+
+ + +
+
+ + + + + +
+ + + +
+ + +
+ + + +
+ + + +
+
+
+ + +
+ + + + + + + + +
+
+ 🦖 +
+

Tippe einen Dino-Namen ein und schau zu, was passiert.

+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/dinos/upload_handler.php b/dinos/upload_handler.php new file mode 100644 index 0000000..b73da7c --- /dev/null +++ b/dinos/upload_handler.php @@ -0,0 +1,56 @@ +isAuthenticated()) { + echo json_encode(['success' => false, 'error' => 'Server nicht mit Google verbunden.']); + exit; +} + +// 2. Daten empfangen +$input = json_decode(file_get_contents('php://input'), true); +if (!isset($input['image']) || !isset($input['name'])) { + echo json_encode(['success' => false, 'error' => 'Keine Bilddaten empfangen.']); + exit; +} + +// 3. Base64 bereinigen (Das "data:image/png;base64," entfernen) +$imgParts = explode(',', $input['image']); +$base64Data = end($imgParts); +$binaryImage = base64_decode($base64Data); + +if (!$binaryImage) { + echo json_encode(['success' => false, 'error' => 'Bild konnte nicht verarbeitet werden.']); + exit; +} + +// 4. Beschreibung bauen +$desc = "🦖 " . $input['name'] . "\n" . + "💡 " . ($input['fun_fact'] ?? '') . "\n" . + "📅 " . ($input['years'] ?? ''); + +// 5. Hochladen +$result = $google->uploadImage($binaryImage, $desc); + +if (isset($result['newMediaItemsResult'])) { // Google Success Structure + echo json_encode(['success' => true]); +} else { + echo json_encode(['success' => false, 'error' => 'Google Fehler', 'details' => $result]); +} +?> \ No newline at end of file diff --git a/home/.env.example b/home/.env.example new file mode 100644 index 0000000..c51b5da --- /dev/null +++ b/home/.env.example @@ -0,0 +1,13 @@ +# App Einstellungen +APP_NAME=philippurbschat.de +APP_ENV=production +BASE_URL=https://philippurbschat.de/ + +# Datenbank (Optional - leer lassen, wenn keine DB benötigt wird) +DB_HOST=localhost +DB_NAME=your_db_name +DB_USER=your_db_user +DB_PASS=your_db_password + +# APIs & Externe Dienste +GEMINI_API_KEY=your_gemini_api_key_here diff --git a/home/app/controllers/AdminController.php b/home/app/controllers/AdminController.php new file mode 100644 index 0000000..c51ba2c --- /dev/null +++ b/home/app/controllers/AdminController.php @@ -0,0 +1,89 @@ +view('admin/dashboard', [ + 'title' => 'Admin Dashboard', + 'health' => $report + ]); + } + // Settings + public function settings() { + $this->view('admin/settings', [ + 'title' => 'Einstellungen' + ]); + } + // User List + public function users() { + $userModel = $this->model('User'); + $users = $userModel->getAll(); + $this->view('admin/users', [ + 'title' => 'Benutzerverwaltung', + 'users' => $users + ]); + } + // User Form + public function user_form($id = null) { + $user = null; + if ($id) { + $userModel = $this->model('User'); + $user = $userModel->getById($id); + if (!$user) { + Flash::set('Nutzer nicht gefunden.', 'error'); + header('Location: ' . Config::get('BASE_URL') . 'admin/users'); + exit; + } + } + $this->view('admin/user_form', [ + 'title' => $id ? 'Benutzer bearbeiten' : 'Neuen Benutzer anlegen', + 'user' => $user + ]); + } + // Save User + public function user_save() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: ' . Config::get('BASE_URL') . 'admin/users'); + exit; + } + try { + Security::checkCsrf($_POST['csrf_token']); + $id = !empty($_POST['id']) ? $_POST['id'] : null; + $userModel = $this->model('User'); + if (empty($_POST['name']) || empty($_POST['email'])) { + throw new Exception("Name und E-Mail sind Pflichtfelder."); + } + $userModel->save($_POST, $id); + Flash::set('Nutzer erfolgreich gespeichert!', 'success'); + } catch (Exception $e) { + Flash::set($e->getMessage(), 'error'); + } + header('Location: ' . Config::get('BASE_URL') . 'admin/users'); + exit; + } + // Delete User + public function user_delete($id) { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + try { + Security::checkCsrf($_POST['csrf_token']); + $userModel = $this->model('User'); + $userModel->delete($id); + Flash::set('Nutzer wurde gelöscht.', 'success'); + } catch (Exception $e) { + Flash::set('Fehler beim Löschen: ' . $e->getMessage(), 'error'); + } + } + header('Location: ' . Config::get('BASE_URL') . 'admin/users'); + exit; + } +} \ No newline at end of file diff --git a/home/app/controllers/HomeController.php b/home/app/controllers/HomeController.php new file mode 100644 index 0000000..7c45293 --- /dev/null +++ b/home/app/controllers/HomeController.php @@ -0,0 +1,28 @@ +{$routeName}(); + } + if ($routeName !== 'home' && $routeName !== '') { + throw new Exception("Route not found."); + } + $this->view('home', [ + 'name' => $name + ]); + } + // Static Views + public function about() { + $this->view('about'); + } + public function imprint() { + $this->view('imprint'); + } + public function privacy() { + $this->view('privacy'); + } +} \ No newline at end of file diff --git a/home/app/controllers/LoginController.php b/home/app/controllers/LoginController.php new file mode 100644 index 0000000..d6837fa --- /dev/null +++ b/home/app/controllers/LoginController.php @@ -0,0 +1,43 @@ +model('User'); + $user = $userModel->authenticate($email, $password); + if ($user) { + $_SESSION['user_email'] = $email; + $_SESSION['user_projects'] = $user['projects'] ?? []; + $_SESSION['is_admin'] = $user['is_admin'] ?? false; + header('Location: ' . Config::get('BASE_URL', '/')); + } else { + Flash::set('Access denied: Invalid credentials.'); + header('Location: ' . Config::get('BASE_URL', '/')); + } + } catch (Exception $e) { + Flash::set('System error: ' . $e->getMessage()); + header('Location: ' . Config::get('BASE_URL', '/')); + } + exit; + } + // Logout Handler + public function logout() { + session_destroy(); + header('Location: ' . Config::get('BASE_URL', '/')); + exit; + } +} \ No newline at end of file diff --git a/home/app/controllers/SeoController.php b/home/app/controllers/SeoController.php new file mode 100644 index 0000000..579661d --- /dev/null +++ b/home/app/controllers/SeoController.php @@ -0,0 +1,40 @@ +'; + $xml .= ''; + foreach ($pages as $page) { + $priority = ($page === '') ? '1.0' : '0.8'; + $xml .= ''; + $xml .= '' . $baseUrl . $page . ''; + $xml .= '' . $lastMod . ''; + $xml .= '' . $priority . ''; + $xml .= ''; + } + $xml .= ''; + return $this->xml($xml); + } + // Robots.txt Generation + public function robots() { + $baseUrl = Config::get('BASE_URL', 'https://philippurbschat.de/'); + $txt = "User-agent: *\n"; + $txt .= "Allow: /\n\n"; + $txt .= "Disallow: /admin\n"; + $txt .= "Disallow: /admin/\n"; + $txt .= "Disallow: /admin/users\n"; + $txt .= "Disallow: /admin/settings\n"; + $txt .= "Disallow: /login\n"; + $txt .= "Disallow: /login/\n"; + $txt .= "Disallow: /core/\n"; + $txt .= "Disallow: /app/\n"; + $txt .= "Disallow: /views/\n"; + $txt .= "Disallow: /src/\n\n"; + $txt .= "Sitemap: " . $baseUrl . "sitemap.xml\n"; + return $this->text($txt); + } +} \ No newline at end of file diff --git a/home/app/init.php b/home/app/init.php new file mode 100644 index 0000000..a6c9648 --- /dev/null +++ b/home/app/init.php @@ -0,0 +1,32 @@ + '/', + 'httponly' => true, + 'samesite' => 'Lax' + ]); + session_start(); +} +date_default_timezone_set('Europe/Berlin'); +// Autoloader +spl_autoload_register(function($className) { + $paths = [ + __DIR__ . '/../core/', + __DIR__ . '/models/', + __DIR__ . '/services/' + ]; + foreach ($paths as $path) { + $file = $path . $className . '.php'; + $fileLower = $path . strtolower($className) . '.php'; + if (file_exists($file)) { + require_once $file; + return; + } elseif (file_exists($fileLower)) { + require_once $fileLower; + return; + } + } +}); +// Environment +Config::load(__DIR__ . '/../.env'); \ No newline at end of file diff --git a/home/app/models/User.php b/home/app/models/User.php new file mode 100644 index 0000000..1052d6f --- /dev/null +++ b/home/app/models/User.php @@ -0,0 +1,67 @@ +db = new Database(); + } + // Auth-Logic + public function authenticate($email, $password) { + $this->db->query("SELECT * FROM home_users WHERE email = :email AND status = 'Active'"); + $this->db->bind(':email', $email); + $user = $this->db->single(); + if ($user && password_verify($password, $user['password'])) { + $user['projects'] = json_decode($user['projects'], true) ?? []; + $user['is_admin'] = (bool)$user['is_admin']; + return $user; + } + return false; + } + // Data-Retrieval + public function getAll() { + $this->db->query("SELECT id, name, email, is_admin, status, projects FROM home_users ORDER BY id DESC"); + $users = $this->db->resultSet(); + foreach ($users as &$user) { + $user['projects'] = json_decode($user['projects'], true) ?? []; + } + return $users; + } + public function getById($id) { + $this->db->query("SELECT * FROM home_users WHERE id = :id"); + $this->db->bind(':id', $id); + $user = $this->db->single(); + if ($user) { + $user['projects'] = json_decode($user['projects'], true) ?? []; + } + return $user; + } + // Persistence + public function save($data, $id = null) { + $projectsJson = json_encode(array_map('trim', explode(',', $data['projects']))); + if ($id) { + $sql = "UPDATE home_users SET name = :name, email = :email, projects = :projects, is_admin = :is_admin, status = :status "; + if (!empty($data['password'])) { + $sql .= ", password = :password "; + } + $sql .= "WHERE id = :id"; + $this->db->query($sql); + $this->db->bind(':id', $id); + } else { + $sql = "INSERT INTO home_users (name, email, password, projects, is_admin, status) VALUES (:name, :email, :password, :projects, :is_admin, :status)"; + $this->db->query($sql); + } + $this->db->bind(':name', $data['name']); + $this->db->bind(':email', $data['email']); + $this->db->bind(':projects', $projectsJson); + $this->db->bind(':is_admin', isset($data['is_admin']) ? 1 : 0); + $this->db->bind(':status', $data['status']); + if (!$id || !empty($data['password'])) { + $this->db->bind(':password', password_hash($data['password'], PASSWORD_DEFAULT)); + } + return $this->db->execute(); + } + public function delete($id) { + $this->db->query("DELETE FROM home_users WHERE id = :id"); + $this->db->bind(':id', $id); + return $this->db->execute(); + } +} \ No newline at end of file diff --git a/home/app/services/HealthService.php b/home/app/services/HealthService.php new file mode 100644 index 0000000..acd1a17 --- /dev/null +++ b/home/app/services/HealthService.php @@ -0,0 +1,72 @@ + PHP_VERSION, + 'disk_free' => self::getDiskStatus(), + 'memory' => self::formatBytes(self::getMemoryLimit()), + 'env_status' => self::checkEnvFile(), + 'db_status' => self::checkDatabase(), + 'writable' => self::checkWritableFolders(['public', 'app']) + ]; + } + // Memory Helpers + private static function getMemoryLimit() { + $limit = ini_get('memory_limit'); + if (preg_match('/^(\d+)(.)$/', $limit, $matches)) { + $val = (int)$matches[1]; + switch (strtoupper($matches[2])) { + case 'G': $val *= 1024 * 1024 * 1024; break; + case 'M': $val *= 1024 * 1024; break; + case 'K': $val *= 1024; break; + } + return $val; + } + return (int)$limit; + } + private static function formatBytes($bytes, $precision = 2) { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow]; + } + // System Status + private static function getDiskStatus() { + if (function_exists('disk_free_space') && function_exists('disk_total_space')) { + try { + $free = @disk_free_space("/"); + $total = @disk_total_space("/"); + if ($free !== false && $total > 0) { + return round(($free / $total) * 100) . "% frei"; + } + } catch (Exception $e) {} + } + return "Eingeschränkt (Hosting)"; + } + private static function checkEnvFile() { + return file_exists(__DIR__ . '/../../.env') ? 'Bereit' : 'Fehlt!'; + } + private static function checkDatabase() { + try { + if (empty(Config::get('DB_NAME'))) { + return 'Deaktiviert'; + } + $db = new Database(); + $db->query("SELECT VERSION() as v"); + $res = $db->single(); + return $res['v'] ?? 'Online'; + } catch (Exception $e) { + return 'Offline / Fehler'; + } + } + private static function checkWritableFolders($folders) { + foreach ($folders as $folder) { + $path = realpath(__DIR__ . '/../../' . $folder); + if (!$path || !is_dir($path)) return "Fehlt: $folder"; + if (!is_writable($path)) return "Rechte: $folder"; + } + return "OK"; + } +} \ No newline at end of file diff --git a/home/core/Auth.php b/home/core/Auth.php new file mode 100644 index 0000000..c2c8b5a --- /dev/null +++ b/home/core/Auth.php @@ -0,0 +1,19 @@ + true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC + ]; + + try { + $this->dbh = new PDO($dsn, $user, $pass, $options); + } catch (PDOException $e) { + $this->error = $e->getMessage(); + // Exception werfen statt sterben! + throw new Exception("Datenbank-Verbindungsfehler."); + } + } + + public function query($sql) { + $this->stmt = $this->dbh->prepare($sql); + } + + public function bind($param, $value, $type = null) { + if (is_null($type)) { + switch (true) { + case is_int($value): $type = PDO::PARAM_INT; break; + case is_bool($value): $type = PDO::PARAM_BOOL; break; + case is_null($value): $type = PDO::PARAM_NULL; break; + default: $type = PDO::PARAM_STR; + } + } + $this->stmt->bindValue($param, $value, $type); + } + + public function execute() { + return $this->stmt->execute(); + } + + public function resultSet() { + $this->execute(); + return $this->stmt->fetchAll(); + } + + public function single() { + $this->execute(); + return $this->stmt->fetch(); + } + + public function rowCount() { + return $this->stmt->rowCount(); + } +} \ No newline at end of file diff --git a/home/core/Flash.php b/home/core/Flash.php new file mode 100644 index 0000000..8ae772a --- /dev/null +++ b/home/core/Flash.php @@ -0,0 +1,42 @@ + $message, + 'type' => $type + ]; + } + public static function display() { + if (isset($_SESSION['flash_messages'])) { + foreach ($_SESSION['flash_messages'] as $flash) { + // Style-Mapping + $styles = [ + 'error' => ['class' => 'border-rose-500/50 text-rose-400 shadow-[0_0_20px_rgba(244,63,94,0.25)]', 'prefix' => '[ ERR ]'], + 'success' => ['class' => 'border-emerald-500/50 text-emerald-400 shadow-[0_0_20px_rgba(16,185,129,0.25)]', 'prefix' => '[ OK ]'], + 'warning' => ['class' => 'border-amber-500/50 text-amber-400 shadow-[0_0_20px_rgba(245,158,11,0.25)]', 'prefix' => '[ WARN ]'], + 'info' => ['class' => 'border-cyan-500/50 text-cyan-400 shadow-[0_0_20px_rgba(6,182,212,0.25)]', 'prefix' => '[ INFO ]'] + ]; + $currentStyle = $styles[$flash['type']] ?? $styles['error']; + $cssClasses = $currentStyle['class']; + $prefix = $currentStyle['prefix']; + // Output HUD Element + echo "
+ {$prefix} + " . htmlspecialchars($flash['message']) . " +
"; + } + unset($_SESSION['flash_messages']); + // Auto-Hide Script + echo ""; + } + } +} \ No newline at end of file diff --git a/home/core/Router.php b/home/core/Router.php new file mode 100644 index 0000000..c6f0f67 --- /dev/null +++ b/home/core/Router.php @@ -0,0 +1,35 @@ +parseUrl(); + $controllerPath = __DIR__ . '/../app/controllers/'; + // Controller Check + if (isset($url[0]) && file_exists($controllerPath . ucfirst($url[0]) . 'Controller.php')) { + $this->controller = ucfirst($url[0]) . 'Controller'; + unset($url[0]); + } + require_once $controllerPath . $this->controller . '.php'; + $this->controller = new $this->controller; + // Method Check + if (isset($url[1])) { + if (method_exists($this->controller, $url[1])) { + $this->method = $url[1]; + unset($url[1]); + } + } + // Params & Execution + $this->params = $url ? array_values($url) : []; + call_user_func_array([$this->controller, $this->method], $this->params); + } + // URL Parser + private function parseUrl() { + if (isset($_GET['url'])) { + return explode('/', filter_var(rtrim($_GET['url'], '/'), FILTER_SANITIZE_URL)); + } + return []; + } +} \ No newline at end of file diff --git a/home/core/Security.php b/home/core/Security.php new file mode 100644 index 0000000..b23dcb6 --- /dev/null +++ b/home/core/Security.php @@ -0,0 +1,17 @@ +=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz", + "integrity": "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "enhanced-resolve": "^5.19.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.2.1" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + } + } +} diff --git a/home/package.json b/home/package.json new file mode 100644 index 0000000..5733383 --- /dev/null +++ b/home/package.json @@ -0,0 +1,17 @@ +{ + "name": "home", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "dev": "npx @tailwindcss/cli -i ./src/input.css -o ./public/css/app.css --watch", + "build": "npx @tailwindcss/cli -i ./src/input.css -o ./public/css/app.css --minify" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@tailwindcss/cli": "^4.2.1", + "tailwindcss": "^4.2.1" + } +} \ No newline at end of file diff --git a/home/public/.htaccess b/home/public/.htaccess new file mode 100644 index 0000000..1eb8b95 --- /dev/null +++ b/home/public/.htaccess @@ -0,0 +1,9 @@ +RewriteEngine On + +# Falls die angefragte Datei oder das Verzeichnis wirklich existiert, nimm sie direkt +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Ansonsten schicke alles an die index.php +# Der Router im HomeController kümmert sich um den Rest +RewriteRule ^(.*)$ index.php?url=$1 [L,QSA] \ No newline at end of file diff --git a/home/public/android-chrome-192x192.png b/home/public/android-chrome-192x192.png new file mode 100644 index 0000000..55f8ec9 Binary files /dev/null and b/home/public/android-chrome-192x192.png differ diff --git a/home/public/android-chrome-512x512.png b/home/public/android-chrome-512x512.png new file mode 100644 index 0000000..5cfe25c Binary files /dev/null and b/home/public/android-chrome-512x512.png differ diff --git a/home/public/apple-touch-icon.png b/home/public/apple-touch-icon.png new file mode 100644 index 0000000..6618289 Binary files /dev/null and b/home/public/apple-touch-icon.png differ diff --git a/home/public/css/app.css b/home/public/css/app.css new file mode 100644 index 0000000..e07e815 --- /dev/null +++ b/home/public/css/app.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-900:oklch(37.8% .077 168.94);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-900:oklch(39.8% .07 227.392);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-900:oklch(35.9% .144 278.697);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-900:oklch(38.1% .176 304.987);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--font-weight-light:300;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-normal:1.5;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.-inset-4{inset:calc(var(--spacing) * -4)}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.top-4{top:calc(var(--spacing) * 4)}.top-6{top:calc(var(--spacing) * 6)}.top-\[-20\%\]{top:-20%}.top-\[10\%\]{top:10%}.-right-10{right:calc(var(--spacing) * -10)}.right-4{right:calc(var(--spacing) * 4)}.right-6{right:calc(var(--spacing) * 6)}.right-\[-10\%\]{right:-10%}.right-\[10\%\]{right:10%}.-bottom-10{bottom:calc(var(--spacing) * -10)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-4{bottom:calc(var(--spacing) * 4)}.bottom-\[-20\%\]{bottom:-20%}.left-0{left:calc(var(--spacing) * 0)}.left-4{left:calc(var(--spacing) * 4)}.left-\[-10\%\]{left:-10%}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.z-100{z-index:100}.z-9999{z-index:9999}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.mb-20{margin-bottom:calc(var(--spacing) * 20)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-24{height:calc(var(--spacing) * 24)}.h-32{height:calc(var(--spacing) * 32)}.h-\[50\%\]{height:50%}.h-\[60\%\]{height:60%}.h-\[70\%\]{height:70%}.h-fit{height:fit-content}.h-full{height:100%}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing) * 0)}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-\[50\%\]{width:50%}.w-\[60\%\]{width:60%}.w-\[70\%\]{width:70%}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-lg{max-width:var(--container-lg)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.rotate-3{rotate:3deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-800>:not(:last-child)){border-color:var(--color-slate-800)}:where(.divide-slate-800\/50>:not(:last-child)){border-color:#1d293d80}@supports (color:color-mix(in lab, red, red)){:where(.divide-slate-800\/50>:not(:last-child)){border-color:color-mix(in oklab, var(--color-slate-800) 50%, transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-500\/50{border-color:#f99c0080}@supports (color:color-mix(in lab, red, red)){.border-amber-500\/50{border-color:color-mix(in oklab, var(--color-amber-500) 50%, transparent)}}.border-cyan-500{border-color:var(--color-cyan-500)}.border-cyan-500\/20{border-color:#00b7d733}@supports (color:color-mix(in lab, red, red)){.border-cyan-500\/20{border-color:color-mix(in oklab, var(--color-cyan-500) 20%, transparent)}}.border-cyan-500\/50{border-color:#00b7d780}@supports (color:color-mix(in lab, red, red)){.border-cyan-500\/50{border-color:color-mix(in oklab, var(--color-cyan-500) 50%, transparent)}}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.border-emerald-500\/20{border-color:color-mix(in oklab, var(--color-emerald-500) 20%, transparent)}}.border-emerald-500\/50{border-color:#00bb7f80}@supports (color:color-mix(in lab, red, red)){.border-emerald-500\/50{border-color:color-mix(in oklab, var(--color-emerald-500) 50%, transparent)}}.border-fuchsia-500{border-color:var(--color-fuchsia-500)}.border-fuchsia-500\/20{border-color:#e12afb33}@supports (color:color-mix(in lab, red, red)){.border-fuchsia-500\/20{border-color:color-mix(in oklab, var(--color-fuchsia-500) 20%, transparent)}}.border-fuchsia-500\/40{border-color:#e12afb66}@supports (color:color-mix(in lab, red, red)){.border-fuchsia-500\/40{border-color:color-mix(in oklab, var(--color-fuchsia-500) 40%, transparent)}}.border-red-500{border-color:var(--color-red-500)}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-500\/20{border-color:#ff235733}@supports (color:color-mix(in lab, red, red)){.border-rose-500\/20{border-color:color-mix(in oklab, var(--color-rose-500) 20%, transparent)}}.border-rose-500\/30{border-color:#ff23574d}@supports (color:color-mix(in lab, red, red)){.border-rose-500\/30{border-color:color-mix(in oklab, var(--color-rose-500) 30%, transparent)}}.border-rose-500\/50{border-color:#ff235780}@supports (color:color-mix(in lab, red, red)){.border-rose-500\/50{border-color:color-mix(in oklab, var(--color-rose-500) 50%, transparent)}}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-700\/50{border-color:#31415880}@supports (color:color-mix(in lab, red, red)){.border-slate-700\/50{border-color:color-mix(in oklab, var(--color-slate-700) 50%, transparent)}}.border-slate-700\/60{border-color:#31415899}@supports (color:color-mix(in lab, red, red)){.border-slate-700\/60{border-color:color-mix(in oklab, var(--color-slate-700) 60%, transparent)}}.border-slate-700\/80{border-color:#314158cc}@supports (color:color-mix(in lab, red, red)){.border-slate-700\/80{border-color:color-mix(in oklab, var(--color-slate-700) 80%, transparent)}}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-800\/60{border-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.border-slate-800\/60{border-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.border-slate-800\/80{border-color:#1d293dcc}@supports (color:color-mix(in lab, red, red)){.border-slate-800\/80{border-color:color-mix(in oklab, var(--color-slate-800) 80%, transparent)}}.bg-\[\#0b1120\]{background-color:#0b1120}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black{background-color:var(--color-black)}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab, red, red)){.bg-black\/40{background-color:color-mix(in oklab, var(--color-black) 40%, transparent)}}.bg-blue-900{background-color:var(--color-blue-900)}.bg-blue-900\/5{background-color:#1c398e0d}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/5{background-color:color-mix(in oklab, var(--color-blue-900) 5%, transparent)}}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-cyan-500\/5{background-color:#00b7d70d}@supports (color:color-mix(in lab, red, red)){.bg-cyan-500\/5{background-color:color-mix(in oklab, var(--color-cyan-500) 5%, transparent)}}.bg-cyan-900{background-color:var(--color-cyan-900)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\/5{background-color:#00bb7f0d}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/5{background-color:color-mix(in oklab, var(--color-emerald-500) 5%, transparent)}}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-emerald-500\/20{background-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/20{background-color:color-mix(in oklab, var(--color-emerald-500) 20%, transparent)}}.bg-emerald-900{background-color:var(--color-emerald-900)}.bg-fuchsia-500{background-color:var(--color-fuchsia-500)}.bg-fuchsia-500\/5{background-color:#e12afb0d}@supports (color:color-mix(in lab, red, red)){.bg-fuchsia-500\/5{background-color:color-mix(in oklab, var(--color-fuchsia-500) 5%, transparent)}}.bg-indigo-900{background-color:var(--color-indigo-900)}.bg-indigo-900\/10{background-color:#312c851a}@supports (color:color-mix(in lab, red, red)){.bg-indigo-900\/10{background-color:color-mix(in oklab, var(--color-indigo-900) 10%, transparent)}}.bg-purple-900{background-color:var(--color-purple-900)}.bg-red-500{background-color:var(--color-red-500)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-500\/5{background-color:#ff23570d}@supports (color:color-mix(in lab, red, red)){.bg-rose-500\/5{background-color:color-mix(in oklab, var(--color-rose-500) 5%, transparent)}}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-800\/80{background-color:#1d293dcc}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/80{background-color:color-mix(in oklab, var(--color-slate-800) 80%, transparent)}}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-900\/30{background-color:#0f172b4d}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/30{background-color:color-mix(in oklab, var(--color-slate-900) 30%, transparent)}}.bg-slate-900\/50{background-color:#0f172b80}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/50{background-color:color-mix(in oklab, var(--color-slate-900) 50%, transparent)}}.bg-slate-900\/80{background-color:#0f172bcc}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/80{background-color:color-mix(in oklab, var(--color-slate-900) 80%, transparent)}}.bg-slate-950{background-color:var(--color-slate-950)}.bg-slate-950\/50{background-color:#02061880}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/50{background-color:color-mix(in oklab, var(--color-slate-950) 50%, transparent)}}.bg-slate-950\/80{background-color:#020618cc}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/80{background-color:color-mix(in oklab, var(--color-slate-950) 80%, transparent)}}.bg-slate-950\/90{background-color:#020618e6}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/90{background-color:color-mix(in oklab, var(--color-slate-950) 90%, transparent)}}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-cyan-400{--tw-gradient-from:var(--color-cyan-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-400{--tw-gradient-from:var(--color-emerald-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-500{--tw-gradient-from:var(--color-emerald-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-emerald-500\/20{--tw-gradient-from:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.from-emerald-500\/20{--tw-gradient-from:color-mix(in oklab, var(--color-emerald-500) 20%, transparent)}}.from-emerald-500\/20{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-fuchsia-400{--tw-gradient-from:var(--color-fuchsia-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-500{--tw-gradient-from:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-cyan-500{--tw-gradient-via:var(--color-cyan-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-cyan-500\/40{--tw-gradient-via:#00b7d766}@supports (color:color-mix(in lab, red, red)){.via-cyan-500\/40{--tw-gradient-via:color-mix(in oklab, var(--color-cyan-500) 40%, transparent)}}.via-cyan-500\/40{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-emerald-500{--tw-gradient-via:var(--color-emerald-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-emerald-500\/40{--tw-gradient-via:#00bb7f66}@supports (color:color-mix(in lab, red, red)){.via-emerald-500\/40{--tw-gradient-via:color-mix(in oklab, var(--color-emerald-500) 40%, transparent)}}.via-emerald-500\/40{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-fuchsia-500{--tw-gradient-via:var(--color-fuchsia-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-fuchsia-500\/40{--tw-gradient-via:#e12afb66}@supports (color:color-mix(in lab, red, red)){.via-fuchsia-500\/40{--tw-gradient-via:color-mix(in oklab, var(--color-fuchsia-500) 40%, transparent)}}.via-fuchsia-500\/40{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-400{--tw-gradient-via:var(--color-purple-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500{--tw-gradient-via:var(--color-purple-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-purple-500\/20{--tw-gradient-via:#ac4bff33}@supports (color:color-mix(in lab, red, red)){.via-purple-500\/20{--tw-gradient-via:color-mix(in oklab, var(--color-purple-500) 20%, transparent)}}.via-purple-500\/20{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-rose-500{--tw-gradient-via:var(--color-rose-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-rose-500\/40{--tw-gradient-via:#ff235766}@supports (color:color-mix(in lab, red, red)){.via-rose-500\/40{--tw-gradient-via:color-mix(in oklab, var(--color-rose-500) 40%, transparent)}}.via-rose-500\/40{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-cyan-400{--tw-gradient-to:var(--color-cyan-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-cyan-500{--tw-gradient-to:var(--color-cyan-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-cyan-500\/20{--tw-gradient-to:#00b7d733}@supports (color:color-mix(in lab, red, red)){.to-cyan-500\/20{--tw-gradient-to:color-mix(in oklab, var(--color-cyan-500) 20%, transparent)}}.to-cyan-500\/20{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-fuchsia-400{--tw-gradient-to:var(--color-fuchsia-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-700{--tw-gradient-to:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-500{color:var(--color-cyan-500)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-500{color:var(--color-emerald-500)}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-fuchsia-400\/80{color:#ec6cffcc}@supports (color:color-mix(in lab, red, red)){.text-fuchsia-400\/80{color:color-mix(in oklab, var(--color-fuchsia-400) 80%, transparent)}}.text-fuchsia-500{color:var(--color-fuchsia-500)}.text-fuchsia-500\/50{color:#e12afb80}@supports (color:color-mix(in lab, red, red)){.text-fuchsia-500\/50{color:color-mix(in oklab, var(--color-fuchsia-500) 50%, transparent)}}.text-indigo-400{color:var(--color-indigo-400)}.text-purple-400{color:var(--color-purple-400)}.text-red-400{color:var(--color-red-400)}.text-rose-400{color:var(--color-rose-400)}.text-rose-500{color:var(--color-rose-500)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.text-transparent{color:#0000}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.accent-emerald-500{accent-color:var(--color-emerald-500)}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(6\,182\,212\,0\.8\)\]{--tw-shadow:0 0 5px var(--tw-shadow-color,#06b6d4cc);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_5px_rgba\(244\,63\,94\,0\.8\)\]{--tw-shadow:0 0 5px var(--tw-shadow-color,#f43f5ecc);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(217\,70\,239\,0\.05\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#d946ef0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(6\,182\,212\,0\.25\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#06b6d440);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(16\,185\,129\,0\.25\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#10b98140);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(244\,63\,94\,0\.25\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#f43f5e40);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(245\,158\,11\,0\.25\)\]{--tw-shadow:0 0 20px var(--tw-shadow-color,#f59e0b40);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(6\,182\,212\,0\.15\)\]{--tw-shadow:0 0 30px var(--tw-shadow-color,#06b6d426);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(16\,185\,129\,0\.15\)\]{--tw-shadow:0 0 30px var(--tw-shadow-color,#10b98126);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(217\,70\,239\,0\.15\)\]{--tw-shadow:0 0 30px var(--tw-shadow-color,#d946ef26);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(244\,63\,94\,0\.15\)\]{--tw-shadow:0 0 30px var(--tw-shadow-color,#f43f5e26);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(16\,185\,129\,0\.8\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#10b981cc));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(34\,211\,238\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#22d3ee4d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(52\,211\,153\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#34d3994d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(129\,140\,248\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#818cf84d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(192\,132\,252\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#c084fc4d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(251\,113\,133\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#fb71854d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_8px_rgba\(251\,191\,36\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 8px var(--tw-drop-shadow-color,#fbbf244d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_10px_rgba\(236\,72\,153\,0\.8\)\]{--tw-drop-shadow-size:drop-shadow(0 0 10px var(--tw-drop-shadow-color,#ec4899cc));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_12px_rgba\(232\,121\,249\,0\.6\)\]{--tw-drop-shadow-size:drop-shadow(0 0 12px var(--tw-drop-shadow-color,#e879f999));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_15px_rgba\(168\,85\,247\,0\.8\)\]{--tw-drop-shadow-size:drop-shadow(0 0 15px var(--tw-drop-shadow-color,#a855f7cc));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:w-full:is(:where(.group):hover *){width:100%}.group-hover\:scale-125:is(:where(.group):hover *){--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:bg-cyan-400:is(:where(.group):hover *){background-color:var(--color-cyan-400)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/btn\:translate-x-1:is(:where(.group\/btn):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\/btn\:text-emerald-400:is(:where(.group\/btn):hover *){color:var(--color-emerald-400)}}.placeholder\:text-slate-500::placeholder{color:var(--color-slate-500)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-emerald-500\/50:hover{border-color:#00bb7f80}@supports (color:color-mix(in lab, red, red)){.hover\:border-emerald-500\/50:hover{border-color:color-mix(in oklab, var(--color-emerald-500) 50%, transparent)}}.hover\:bg-emerald-500\/40:hover{background-color:#00bb7f66}@supports (color:color-mix(in lab, red, red)){.hover\:bg-emerald-500\/40:hover{background-color:color-mix(in oklab, var(--color-emerald-500) 40%, transparent)}}.hover\:bg-fuchsia-500\/10:hover{background-color:#e12afb1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-fuchsia-500\/10:hover{background-color:color-mix(in oklab, var(--color-fuchsia-500) 10%, transparent)}}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-800\/50:hover{background-color:#1d293d80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-slate-800\/50:hover{background-color:color-mix(in oklab, var(--color-slate-800) 50%, transparent)}}.hover\:text-cyan-300:hover{color:var(--color-cyan-300)}.hover\:text-cyan-400:hover{color:var(--color-cyan-400)}.hover\:text-emerald-300:hover{color:var(--color-emerald-300)}.hover\:text-emerald-400:hover{color:var(--color-emerald-400)}.hover\:text-fuchsia-300:hover{color:var(--color-fuchsia-300)}.hover\:text-fuchsia-400:hover{color:var(--color-fuchsia-400)}.hover\:text-rose-300:hover{color:var(--color-rose-300)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}.hover\:shadow-\[0_0_15px_rgba\(217\,70\,239\,0\.2\)\]:hover{--tw-shadow:0 0 15px var(--tw-shadow-color,#d946ef33);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-emerald-500\/20:hover{--tw-shadow-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.hover\:shadow-emerald-500\/20:hover{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-emerald-500) 20%, transparent) var(--tw-shadow-alpha), transparent)}}}.focus\:border-emerald-500\/50:focus{border-color:#00bb7f80}@supports (color:color-mix(in lab, red, red)){.focus\:border-emerald-500\/50:focus{border-color:color-mix(in oklab, var(--color-emerald-500) 50%, transparent)}}.focus\:border-fuchsia-500\/50:focus{border-color:#e12afb80}@supports (color:color-mix(in lab, red, red)){.focus\:border-fuchsia-500\/50:focus{border-color:color-mix(in oklab, var(--color-fuchsia-500) 50%, transparent)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-fuchsia-500\/50:focus{--tw-ring-color:#e12afb80}@supports (color:color-mix(in lab, red, red)){.focus\:ring-fuchsia-500\/50:focus{--tw-ring-color:color-mix(in oklab, var(--color-fuchsia-500) 50%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}@media (min-width:40rem){.sm\:mx-0{margin-inline:calc(var(--spacing) * 0)}.sm\:mt-8{margin-top:calc(var(--spacing) * 8)}.sm\:mt-12{margin-top:calc(var(--spacing) * 12)}.sm\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.sm\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.sm\:mb-10{margin-bottom:calc(var(--spacing) * 10)}.sm\:mb-20{margin-bottom:calc(var(--spacing) * 20)}.sm\:ml-2{margin-left:calc(var(--spacing) * 2)}.sm\:block{display:block}.sm\:w-auto{width:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:gap-10{gap:calc(var(--spacing) * 10)}:where(.sm\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 12) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 12) * calc(1 - var(--tw-space-y-reverse)))}.sm\:rounded-\[2\.5rem\]{border-radius:2.5rem}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:pt-8{padding-top:calc(var(--spacing) * 8)}.sm\:pt-10{padding-top:calc(var(--spacing) * 10)}.sm\:pb-6{padding-bottom:calc(var(--spacing) * 6)}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media (min-width:48rem){.md\:mt-0{margin-top:calc(var(--spacing) * 0)}.md\:mt-12{margin-top:calc(var(--spacing) * 12)}.md\:mt-24{margin-top:calc(var(--spacing) * 24)}.md\:h-32{height:calc(var(--spacing) * 32)}.md\:w-32{width:calc(var(--spacing) * 32)}.md\:w-64{width:calc(var(--spacing) * 64)}.md\:max-w-none{max-width:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:items-start{align-items:flex-start}.md\:justify-start{justify-content:flex-start}.md\:p-12{padding:calc(var(--spacing) * 12)}.md\:text-left{text-align:left}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.md\:whitespace-normal{white-space:normal}}}@font-face{font-family:Outfit;font-style:normal;font-weight:300 700;font-display:swap;src:url(../fonts/outfit-v15-latin-regular.woff2)format("woff2")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(../fonts/jetbrains-mono-v24-latin-regular.woff2)format("woff2")}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} \ No newline at end of file diff --git a/home/public/css/style.css b/home/public/css/style.css new file mode 100644 index 0000000..3a1f546 --- /dev/null +++ b/home/public/css/style.css @@ -0,0 +1,125 @@ +@import "app.css"; +:root { + --bg-base: #020617; +} +body { + background-color: var(--bg-base); + overflow-x: hidden; + font-family: 'Outfit', sans-serif; +} +.font-mono { + font-family: 'JetBrains Mono', monospace !important; +} +/* CRT Overlay */ +.crt-overlay { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), + linear-gradient(90deg, rgba(255, 0, 0, 0.02), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.02)); + background-size: 100% 3px, 3px 100%; + pointer-events: none; + z-index: 50; +} +/* Noise Texture */ +.noise { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); + opacity: 0.025; + pointer-events: none; + z-index: 40; +} +/* Background Glow */ +.bg-glow { + position: fixed; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + width: 120vw; height: 120vh; + background: radial-gradient(circle at center, rgba(99, 102, 241, 0.08) 0%, rgba(0, 0, 0, 0) 70%); + pointer-events: none; + z-index: -1; +} +.selection-pink::selection { + background: #f43f5e; + color: white; +} +/* Animations */ +@keyframes blob-bounce { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(30px, -50px) scale(1.1); } + 66% { transform: translate(-20px, 20px) scale(0.9); } +} +@keyframes particle-drift { + 0% { transform: translateY(0) rotate(0deg); opacity: 0; } + 50% { opacity: 0.5; } + 100% { transform: translateY(-100vh) rotate(360deg); opacity: 0; } +} +@keyframes invader-patrol { + 0%, 100% { transform: translateX(-12px); } + 50% { transform: translateX(12px); } +} +.animate-blob { + animation: blob-bounce 25s infinite alternate ease-in-out; + filter: blur(80px); +} +.animate-invader { + animation: invader-patrol 4s infinite ease-in-out; +} +.particle { + position: fixed; + background: white; + border-radius: 50%; + pointer-events: none; + z-index: 1; + animation: particle-drift var(--p-duration) infinite linear; + animation-delay: var(--p-delay); +} +/* UI Components */ +.bullshit-terminal { + background: rgba(15, 23, 42, 0.6); + border: 1px dashed rgba(100, 116, 139, 0.3); + border-left: 4px solid #f0abfc; + position: relative; +} +.bullshit-terminal::before { + content: "CORPORATE_BULLSHIT_V1.0"; + position: absolute; + top: -10px; + left: 20px; + background: #0f172a; + padding: 0 10px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: #64748b; + letter-spacing: 0.1em; +} +.btn-slim { + display: inline-flex; + align-items: center; + border: 1px solid rgba(240, 171, 252, 0.3); + color: #f0abfc; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 0.75rem 1.5rem; + border-radius: 12px; + transition: all 0.3s ease; +} +.btn-slim:hover { + background: rgba(240, 171, 252, 0.1); + border-color: #f0abfc; + transform: translateY(-2px); + box-shadow: 0 0 20px rgba(240, 171, 252, 0.1); +} +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border-width: 0 !important; +} \ No newline at end of file diff --git a/home/public/favicon-16x16.png b/home/public/favicon-16x16.png new file mode 100644 index 0000000..c3c3627 Binary files /dev/null and b/home/public/favicon-16x16.png differ diff --git a/home/public/favicon-32x32.png b/home/public/favicon-32x32.png new file mode 100644 index 0000000..3e24283 Binary files /dev/null and b/home/public/favicon-32x32.png differ diff --git a/home/public/favicon.ico b/home/public/favicon.ico new file mode 100644 index 0000000..7e2bcdd Binary files /dev/null and b/home/public/favicon.ico differ diff --git a/home/public/fonts/jetbrains-mono-v24-latin-700.woff2 b/home/public/fonts/jetbrains-mono-v24-latin-700.woff2 new file mode 100644 index 0000000..3a4e333 Binary files /dev/null and b/home/public/fonts/jetbrains-mono-v24-latin-700.woff2 differ diff --git a/home/public/fonts/jetbrains-mono-v24-latin-regular.woff2 b/home/public/fonts/jetbrains-mono-v24-latin-regular.woff2 new file mode 100644 index 0000000..5858873 Binary files /dev/null and b/home/public/fonts/jetbrains-mono-v24-latin-regular.woff2 differ diff --git a/home/public/fonts/outfit-v15-latin-300.woff2 b/home/public/fonts/outfit-v15-latin-300.woff2 new file mode 100644 index 0000000..24dc096 Binary files /dev/null and b/home/public/fonts/outfit-v15-latin-300.woff2 differ diff --git a/home/public/fonts/outfit-v15-latin-700.woff2 b/home/public/fonts/outfit-v15-latin-700.woff2 new file mode 100644 index 0000000..8674e73 Binary files /dev/null and b/home/public/fonts/outfit-v15-latin-700.woff2 differ diff --git a/home/public/fonts/outfit-v15-latin-regular.woff2 b/home/public/fonts/outfit-v15-latin-regular.woff2 new file mode 100644 index 0000000..2c401f5 Binary files /dev/null and b/home/public/fonts/outfit-v15-latin-regular.woff2 differ diff --git a/home/public/index.php b/home/public/index.php new file mode 100644 index 0000000..43dec3d --- /dev/null +++ b/home/public/index.php @@ -0,0 +1,23 @@ +route(); +} catch (Exception $e) { + // Error Handling + http_response_code(404); + $errorMessage = Config::get('APP_ENV') === 'development' ? $e->getMessage() : 'Page not found or access denied.'; + $errorView = __DIR__ . '/../views/error.php'; + if(file_exists($errorView)) { + require_once $errorView; + } else { + echo "

System Error

" . htmlspecialchars($errorMessage) . "

"; + } +} \ No newline at end of file diff --git a/home/public/js/main.js b/home/public/js/main.js new file mode 100644 index 0000000..fc83d51 --- /dev/null +++ b/home/public/js/main.js @@ -0,0 +1,53 @@ +let clickCount = 0; +let resetTimer = null; +let isNaughtyMode = false; +function triggerGimmick(e) { + const icon = document.getElementById('invader-icon'); + clearTimeout(resetTimer); + // Counter & Mode + clickCount++; + if (clickCount >= 10 && !isNaughtyMode) { + isNaughtyMode = true; + icon.innerText = '🍆'; + } + // Icon Animation + icon.style.transition = 'transform 0.5s ease-out'; + icon.style.transform = `rotate(${Math.random() > 0.5 ? 360 : -360}deg) scale(1.4)`; + setTimeout(() => { + icon.style.transition = 'transform 0.3s ease-in'; + icon.style.transform = 'rotate(0deg) scale(1)'; + }, 500); + // Particle Explosion + for(let i = 0; i < 12; i++) { + let bit = document.createElement('div'); + if (isNaughtyMode) { + bit.innerText = Math.random() > 0.5 ? '' : '💕'; + bit.className = 'fixed pointer-events-none text-2xl z-50 drop-shadow-[0_0_10px_rgba(236,72,153,0.8)]'; + } else { + bit.innerText = Math.random() > 0.5 ? '1' : '0'; + bit.className = 'fixed font-mono text-emerald-500 pointer-events-none text-xl z-50 font-bold drop-shadow-[0_0_8px_rgba(16,185,129,0.8)]'; + } + bit.style.left = e.clientX + 'px'; + bit.style.top = e.clientY + 'px'; + bit.style.transition = 'all 0.8s cubic-bezier(0.1, 0.8, 0.2, 1)'; + document.body.appendChild(bit); + setTimeout(() => { + const angle = Math.random() * Math.PI * 2; + const dist = 60 + Math.random() * 100; + bit.style.transform = `translate(${Math.cos(angle)*dist}px, ${Math.sin(angle)*dist}px) scale(${Math.random() + 0.5})`; + bit.style.opacity = '0'; + }, 10); + setTimeout(() => bit.remove(), 800); + } + // Auto-Reset + resetTimer = setTimeout(() => { + clickCount = 0; + if (isNaughtyMode) { + isNaughtyMode = false; + icon.innerText = '👾'; + icon.style.transition = 'transform 0.3s ease'; + icon.style.transform = 'scale(1.2)'; + setTimeout(() => icon.style.transform = 'scale(1)', 300); + } + }, 5000); +} \ No newline at end of file diff --git a/home/src/input.css b/home/src/input.css new file mode 100644 index 0000000..d6cf66b --- /dev/null +++ b/home/src/input.css @@ -0,0 +1,19 @@ +/* DATEI: ./src/input.css */ +@import "tailwindcss"; + +/* --- FONTS (Pfade korrigiert) --- */ +@font-face { + font-family: 'Outfit'; + font-style: normal; + font-weight: 300 700; + font-display: swap; + src: url('../fonts/outfit-v15-latin-regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url('../fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2'); +} \ No newline at end of file diff --git a/home/views/about.php b/home/views/about.php new file mode 100644 index 0000000..46bf4dd --- /dev/null +++ b/home/views/about.php @@ -0,0 +1,43 @@ + +
+
+

+ _Über mich +

+ + < Back to Root + +
+
+
+
+
+
+

+ „Als ergebnisorientierter Game-Changer mit einem disruptiven Mindset fokussiere ich mich auf die Maximierung holistischer Synergien im agilen Deep-Dive, um Low-Hanging Fruits in skalierbare Next-Level-Blueprints zu transformieren.“ +

+
+

Merkst du selbst, oder?

+
+

Wenn du jemanden suchst, der dich mit Bullshit-Bingo einlullt, bist du hier falsch.

+
    +
  • Klares Design & durchdachtes Frontend statt „disruptiver Interface-Paradigmen“ oder „User-Experience-Visionen“.
  • +
  • solides Backend, das einfach läuft, statt „hyper-skalierbarer Microservice-Ökosysteme“.
  • +
  • Echte Sichtbarkeit statt „Performance-Driven Reach Orchestration“.
  • +
  • Sinnvolle KI-Integrationen statt „Big-Data-Orchestrierung“ im luftleeren Raum.
  • +
  • Starke Konzepte, die alles zusammenhalten, statt „holistischer Transformations-Roadmaps“.
  • +
+

Ich baue Systeme, die funktionieren, und löse Probleme, anstatt sie hinter Fachbegriffen zu verstecken.

+
+
+
+
+

Willst du wissen, wie ich arbeite, wer ich bin oder welche Musik ich höre?

+ +
+
+
+
+
+
+ \ No newline at end of file diff --git a/home/views/admin/dashboard.php b/home/views/admin/dashboard.php new file mode 100644 index 0000000..e83b344 --- /dev/null +++ b/home/views/admin/dashboard.php @@ -0,0 +1,56 @@ + +
+ +
+
+
+
+
+
philcore-dashboard.sh
+
terminal_session_main
+
+
+
+

> Dashboard_

+

System-Status für

+
+
+
+

> db_connection

+

+ + +

+
+
+

> php_engine

+

v ()

+
+
+

> disk_space

+

+
+
+

> write_permissions

+

+
+
+
+

> [] SYS_LOAD: OPTIMAL

+

> [] ENV_STATUS:

+

> [] Alle Systeme bereit.

+
+
+
+
+ \ No newline at end of file diff --git a/home/views/admin/settings.php b/home/views/admin/settings.php new file mode 100644 index 0000000..d56249b --- /dev/null +++ b/home/views/admin/settings.php @@ -0,0 +1,45 @@ + +
+ +
+
+
+
+
+
philcore-settings.sh
+
+
+
+

> _

+

Systemkonfiguration anpassen.

+
+
+
+

> env_variables

+
+
+ + +
+
+ + +

// Hinweis: Änderungen direkt in der .env vornehmen.

+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/home/views/admin/user_form.php b/home/views/admin/user_form.php new file mode 100644 index 0000000..eec5160 --- /dev/null +++ b/home/views/admin/user_form.php @@ -0,0 +1,50 @@ + +
+
+

+ > +

+ < Zurück zur Liste +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors"> +
+
+ + +
+
+
+ + +

Verzeichnisse im Root für Zugriffsberechtigung.

+
+
+ class="w-4 h-4 accent-emerald-500 bg-slate-900 border-slate-700 rounded"> + +
+
+ +
+
+
+
+ \ No newline at end of file diff --git a/home/views/admin/users.php b/home/views/admin/users.php new file mode 100644 index 0000000..6ae1920 --- /dev/null +++ b/home/views/admin/users.php @@ -0,0 +1,70 @@ + +
+ +
+
+
+ + + +
philcore-users.sh
+
+ + NEW_USER +
+
+
+

> _

+

Registrierte System-Accounts.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNameEmailRolleProjekteStatusAction
Keine Benutzer gefunden.
#Admin' : 'User' ?> +
+ Edit +
+ + +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/home/views/error.php b/home/views/error.php new file mode 100644 index 0000000..742cbec --- /dev/null +++ b/home/views/error.php @@ -0,0 +1,27 @@ + +
+
+ 👾 +
+

+ GAME OVER +

+
+
+
+

+ / system_failure_404 +

+

+ > +

+
+
+
+ +
+ \ No newline at end of file diff --git a/home/views/home.php b/home/views/home.php new file mode 100644 index 0000000..8eaa5d4 --- /dev/null +++ b/home/views/home.php @@ -0,0 +1,100 @@ + + +
+
+

+ Philipp Urbschat – Webentwickler für Individualsoftware, Webdesign, SEO, Datenanalyse und KI-Lösungen in Detmold. Fullstack-Entwicklung für Frontend und Backend. + PhilippUrbschat +

+
+
+
+
+
+ 👾 +
+
+
+
+

Developer, Designer, Punkrocker, Problem Solver.

+

Digital with zero Bullshit. Whether it's a thoughtful frontend, a solid backend, hands-on SEO, deep data analysis, smart AI integrations, or a sharp concept tying it all together – I build systems that just work.

+

Active staging ground for raw code experiments and live test environments.

+
+ +
+
+
+ +
+
+
+

Projects

+ Logout +
+

/ access_granted

+
+ +

Keine aktiven Projekte gefunden.

+ + + + + + +
+
+
+ +
+
+

Login

+

/ access_restricted

+
+ + + + +
+
+
+ +
+
+
+

Directory

+ +
+
+ + + Kernel Access + + + Kernel Access (Locked) + +
+
+
+
+
+ + \ No newline at end of file diff --git a/home/views/imprint.php b/home/views/imprint.php new file mode 100644 index 0000000..ed0965a --- /dev/null +++ b/home/views/imprint.php @@ -0,0 +1,34 @@ + +
+
+

+ _Impressum +

+ + < Back to Root + +
+
+
+
+
+

Angaben gemäß § 5 TMG

+

Philipp Urbschat
Leopoldstr. 32
32756 Detmold

+
+
+

Kontakt

+

E-Mail: hi@philippurbschat.de

+
+
+

Haftung für Inhalte

+

Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.

+
+
+

Urheberrecht

+

Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.

+
+
+
+
+
+ \ No newline at end of file diff --git a/home/views/inc/admin_footer.php b/home/views/inc/admin_footer.php new file mode 100644 index 0000000..23dec52 --- /dev/null +++ b/home/views/inc/admin_footer.php @@ -0,0 +1,6 @@ + +
+ PHILCORE_SYSTEM_OS [v1.0.4] +
+ + \ No newline at end of file diff --git a/home/views/inc/admin_header.php b/home/views/inc/admin_header.php new file mode 100644 index 0000000..86ef22f --- /dev/null +++ b/home/views/inc/admin_header.php @@ -0,0 +1,25 @@ + + + + + + + Admin | <?= htmlspecialchars(Config::get('APP_NAME', 'PhilCore')) ?> + + + + + + + +
+
+
+ 👾 + / Admin +
+ Home +
+
+ +
\ No newline at end of file diff --git a/home/views/inc/footer.php b/home/views/inc/footer.php new file mode 100644 index 0000000..74249ae --- /dev/null +++ b/home/views/inc/footer.php @@ -0,0 +1,15 @@ +
+
+ © . Made with 🍺 and 🍕. +
+ +
+ + + + + ENV: +
+ + + \ No newline at end of file diff --git a/home/views/inc/header.php b/home/views/inc/header.php new file mode 100644 index 0000000..033307f --- /dev/null +++ b/home/views/inc/header.php @@ -0,0 +1,62 @@ + + + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
\n"; + } + ?> + + +
\ No newline at end of file diff --git a/home/views/privacy.php b/home/views/privacy.php new file mode 100644 index 0000000..114a1f9 --- /dev/null +++ b/home/views/privacy.php @@ -0,0 +1,38 @@ + +
+
+

+ _Datenschutz +

+ + < Back to Root + +
+
+
+
+
+

1. Datenschutz auf einen Blick

+

Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit deinen personenbezogenen Daten passiert, wenn du diese Website besuchst.

+

Verantwortliche Stelle

+

Philipp Urbschat
Leopoldstr. 32
32756 Detmold
E-Mail: hi@philippurbschat.de

+
+
+

2. Datenerfassung auf dieser Website

+

Server-Log-Dateien

+

Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die dein Browser automatisch an uns übermittelt. Dies sind: Browsertyp/Browserversion, verwendetes Betriebssystem, Referrer URL, Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage und IP-Adresse.

+

Technisch notwendige Cookies (Session)

+

Diese Website verwendet ausschließlich sogenannte "Session-Cookies". Diese sind kleine Textdateien, die nur für die Dauer deines Besuchs im Speicher deines Browsers abgelegt werden. Sie sind technisch notwendig, um Kernfunktionen wie den internen Login-Bereich oder die Darstellung von Animationen über Seitenwechsel hinweg bereitzustellen. Session-Cookies werden nach dem Schließen des Browsers automatisch gelöscht. Ein Tracking oder eine Analyse deines Nutzerverhaltens findet dadurch nicht statt.

+

Login und Authentifizierung

+

Wenn du den internen Login-Bereich nutzt, speichern wir die von dir eingegebenen Anmeldedaten (E-Mail, Passwort in verschlüsselter Form) zur Bereitstellung der Systemfunktionen und zum Schutz vor unbefugtem Zugriff.

+
+
+

3. Lokale Assets

+

Schriftarten

+

Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten Web Fonts (Outfit, JetBrains Mono), die lokal auf unserem Server bereitgestellt werden. Es findet keine Verbindung zu externen Servern von Drittanbietern statt.

+
+
+
+
+
+ \ No newline at end of file diff --git a/philcore/.htaccess b/philcore/.htaccess new file mode 100644 index 0000000..8123b8a --- /dev/null +++ b/philcore/.htaccess @@ -0,0 +1,13 @@ +Options -Indexes +RewriteEngine On + +# Leitet den Aufruf ohne Pfad direkt in den public-Ordner +RewriteRule ^$ public/index.php [L] + +# GANZ WICHTIG: Stoppt die Umleitung, wenn eine echte Datei (Bilder, CSS) oder ein echter Ordner aufgerufen wird +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Leitet alle anderen Aufrufe an den Router um +RewriteCond %{REQUEST_URI} !^/public/ +RewriteRule ^(.*)$ public/index.php?url=$1 [L,QSA] \ No newline at end of file diff --git a/philcore/app/controllers/AdminController.php b/philcore/app/controllers/AdminController.php new file mode 100644 index 0000000..9c995b2 --- /dev/null +++ b/philcore/app/controllers/AdminController.php @@ -0,0 +1,36 @@ +view('admin/dashboard', [ + 'title' => 'Admin Dashboard', + 'health' => $report + ]); + } + + // 2. Die Einstellungen + public function settings() { + $this->view('admin/settings', [ + 'title' => 'Einstellungen' + ]); + } + + // 3. Die Benutzerverwaltung + public function users() { + // Erstmal mit Dummy-Daten, bis wir das Model anbinden + $dummyUsers = [ + ['id' => 1, 'name' => 'Admin Phili', 'email' => 'admin@philcore.local', 'status' => 'Active'] + ]; + + $this->view('admin/users', [ + 'title' => 'Benutzerverwaltung', + 'users' => $dummyUsers + ]); + } +} \ No newline at end of file diff --git a/philcore/app/controllers/HomeController.php b/philcore/app/controllers/HomeController.php new file mode 100644 index 0000000..e7ba2db --- /dev/null +++ b/philcore/app/controllers/HomeController.php @@ -0,0 +1,11 @@ + philcore), dann in core +require_once __DIR__ . '/../../core/Controller.php'; + +class HomeController extends Controller { + public function index($name = 'Phili') { + $this->view('home', [ + 'name' => $name + ]); + } +} \ No newline at end of file diff --git a/philcore/app/init.php b/philcore/app/init.php new file mode 100644 index 0000000..ee4e744 --- /dev/null +++ b/philcore/app/init.php @@ -0,0 +1,44 @@ + PHP_VERSION, + 'disk_free' => self::getDiskStatus(), + 'memory' => self::formatBytes(self::getMemoryLimit()), + 'env_status' => self::checkEnvFile(), + 'db_status' => self::checkDatabase(), + 'writable' => self::checkWritableFolders(['public', 'app']) + ]; + } + + private static function getMemoryLimit() { + $limit = ini_get('memory_limit'); + if (preg_match('/^(\d+)(.)$/', $limit, $matches)) { + $val = (int)$matches[1]; + switch (strtoupper($matches[2])) { + case 'G': $val *= 1024 * 1024 * 1024; break; + case 'M': $val *= 1024 * 1024; break; + case 'K': $val *= 1024; break; + } + return $val; + } + return (int)$limit; + } + + private static function formatBytes($bytes, $precision = 2) { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow]; + } + + private static function getDiskStatus() { + if (function_exists('disk_free_space') && function_exists('disk_total_space')) { + try { + $free = @disk_free_space("/"); + $total = @disk_total_space("/"); + if ($free !== false && $total > 0) { + return round(($free / $total) * 100) . "% frei"; + } + } catch (Exception $e) {} + } + return "Eingeschränkt (Hosting)"; + } + + private static function checkEnvFile() { + // Springt aus app/services/ zwei Ebenen hoch zum Hauptverzeichnis + return file_exists(__DIR__ . '/../../.env') ? 'Bereit' : 'Fehlt!'; + } + + private static function checkDatabase() { + try { + // Checkt, ob überhaupt ein Name gesetzt ist + if (empty(Config::get('DB_NAME'))) { + return 'Deaktiviert'; + } + + $db = new Database(); + $db->query("SELECT VERSION() as v"); + $res = $db->single(); + return $res['v'] ?? 'Online'; + } catch (Exception $e) { + return 'Offline / Fehler'; + } + } + + private static function checkWritableFolders($folders) { + foreach ($folders as $folder) { + // Geht vom aktuellen Verzeichnis (app/services/) zwei Ebenen hoch + $path = realpath(__DIR__ . '/../../' . $folder); + if (!$path || !is_dir($path)) return "Fehlt: $folder"; + if (!is_writable($path)) return "Rechte: $folder"; + } + return "OK"; + } +} \ No newline at end of file diff --git a/philcore/core/Auth.php b/philcore/core/Auth.php new file mode 100644 index 0000000..0aa0ca0 --- /dev/null +++ b/philcore/core/Auth.php @@ -0,0 +1,25 @@ + true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC + ]; + + try { + $this->dbh = new PDO($dsn, $user, $pass, $options); + } catch (PDOException $e) { + $this->error = $e->getMessage(); + // Exception werfen statt sterben! + throw new Exception("Datenbank-Verbindungsfehler."); + } + } + + public function query($sql) { + $this->stmt = $this->dbh->prepare($sql); + } + + public function bind($param, $value, $type = null) { + if (is_null($type)) { + switch (true) { + case is_int($value): $type = PDO::PARAM_INT; break; + case is_bool($value): $type = PDO::PARAM_BOOL; break; + case is_null($value): $type = PDO::PARAM_NULL; break; + default: $type = PDO::PARAM_STR; + } + } + $this->stmt->bindValue($param, $value, $type); + } + + public function execute() { + return $this->stmt->execute(); + } + + public function resultSet() { + $this->execute(); + return $this->stmt->fetchAll(); + } + + public function single() { + $this->execute(); + return $this->stmt->fetch(); + } + + public function rowCount() { + return $this->stmt->rowCount(); + } +} \ No newline at end of file diff --git a/philcore/core/Flash.php b/philcore/core/Flash.php new file mode 100644 index 0000000..3dde680 --- /dev/null +++ b/philcore/core/Flash.php @@ -0,0 +1,26 @@ + $message, + 'type' => $type + ]; + } + + // Nachrichten ausgeben und danach löschen + public static function display() { + if (isset($_SESSION['flash_messages'])) { + foreach ($_SESSION['flash_messages'] as $flash) { + // Farben je nach Typ + $color = $flash['type'] === 'error' ? 'red' : 'emerald'; + + echo "
+ " . htmlspecialchars($flash['message']) . " +
"; + } + // Nach dem Anzeigen direkt aufräumen! + unset($_SESSION['flash_messages']); + } + } +} \ No newline at end of file diff --git a/philcore/core/Router.php b/philcore/core/Router.php new file mode 100644 index 0000000..2050d09 --- /dev/null +++ b/philcore/core/Router.php @@ -0,0 +1,43 @@ +parseUrl(); + + // Absoluter Pfad zu den Controllern (geht von core/ einen Ordner hoch und in app/controllers) + $controllerPath = __DIR__ . '/../app/controllers/'; + + // 1. Controller prüfen (z.B. /user/ -> UserController) + if (isset($url[0]) && file_exists($controllerPath . ucfirst($url[0]) . 'Controller.php')) { + $this->controller = ucfirst($url[0]) . 'Controller'; + unset($url[0]); + } + + require_once $controllerPath . $this->controller . '.php'; + $this->controller = new $this->controller; + + // 2. Methode prüfen (z.B. /user/edit -> Methode edit()) + if (isset($url[1])) { + if (method_exists($this->controller, $url[1])) { + $this->method = $url[1]; + unset($url[1]); + } + } + + // 3. Restliche Parameter sammeln + $this->params = $url ? array_values($url) : []; + + // 4. Controller-Methode mit den Parametern aufrufen + call_user_func_array([$this->controller, $this->method], $this->params); + } + + private function parseUrl() { + if (isset($_GET['url'])) { + return explode('/', filter_var(rtrim($_GET['url'], '/'), FILTER_SANITIZE_URL)); + } + return []; + } +} \ No newline at end of file diff --git a/philcore/core/Security.php b/philcore/core/Security.php new file mode 100644 index 0000000..bdf8548 --- /dev/null +++ b/philcore/core/Security.php @@ -0,0 +1,18 @@ +route(); +} catch (Exception $e) { + // Fehler abfangen und hübsche Error-View laden + http_response_code(404); + + // In Produktion zeigen wir keine genauen Fehlermeldungen an + $errorMessage = Config::get('APP_ENV') === 'development' ? $e->getMessage() : 'Seite nicht gefunden oder Zugriff verweigert.'; + + $errorView = __DIR__ . '/../views/error.php'; + if(file_exists($errorView)) { + require_once $errorView; + } else { + echo "

Systemfehler

" . htmlspecialchars($errorMessage) . "

"; + } +} \ No newline at end of file diff --git a/philcore/readme.md b/philcore/readme.md new file mode 100644 index 0000000..1d02ba0 --- /dev/null +++ b/philcore/readme.md @@ -0,0 +1,27 @@ +# 🍕 PhilCore MVC + +Ein leichtgewichtiges, schnelles und sicheres PHP MVC-Grundgerüst für neue Projekte. C&P ready! + +## 🚀 Features +* **Routing:** Automatisches URL-Routing (`/controller/method/params`). +* **Datenbank:** Sichere PDO-Klasse mit Prepared Statements (optional nutzbar). +* **Sicherheit:** Integrierter CSRF-Schutz & globale `.env`-Konfiguration. +* **Autoloader:** Kein nerviges `require_once` mehr für Klassen. +* **Admin-Dashboard:** Integrierter `HealthService` für System-Checks. + +## 🛠 Installation + +1. **Dateien kopieren:** Lade das komplette Projektverzeichnis auf deinen Server oder in deine lokale Entwicklungsumgebung. +2. **Umgebungsvariablen:** Kopiere die `.env.example`, benenne sie in `.env` um und trage deine Daten ein (Base-URL, DB-Credentials etc.). +3. **Routing aktivieren:** Stelle sicher, dass `mod_rewrite` auf deinem Apache-Server aktiv ist. Die `.htaccess`-Dateien leiten alle Anfragen auf `public/index.php` um. +4. **(Optional) DocumentRoot:** Für maximale Sicherheit beim Hosting das Web-Stammverzeichnis direkt auf den Ordner `/public` legen. + +## 📂 Struktur +* `/app`: Deine Anwendungslogik (Controllers, Models, Services). +* `/core`: Das Herzstück (Router, Database, Config, Auth, Security). +* `/public`: Der Einstiegspunkt (`index.php`) und statische Assets. +* `/views`: Deine HTML/PHP-Templates (TailwindCSS vorbereitet). + +## 💡 Nutzung +Neue Seiten erstellst du einfach durch neue Controller in `/app/controllers/`. +Eine Methode `profile()` im `UserController.php` ist direkt über `deinedomain.de/user/profile` aufrufbar. \ No newline at end of file diff --git a/philcore/views/admin/dashboard.php b/philcore/views/admin/dashboard.php new file mode 100644 index 0000000..b7d4082 --- /dev/null +++ b/philcore/views/admin/dashboard.php @@ -0,0 +1,87 @@ + + +
+ + + +
+
+
+
+
+
philcore-dashboard.sh
+
terminal_session_main
+
+ +
+
+

+ > Dashboard_ +

+

System-Status für

+
+ +
+ +
+

> db_connection

+

+ + +

+
+ +
+

> php_engine

+

+ v () +

+
+ +
+

> disk_space

+

+ +

+
+ +
+

> write_permissions

+

+ +

+
+ +
+ +
+

> [] SYS_LOAD: OPTIMAL

+

> [] ENV_STATUS:

+

> [] Alle Systeme bereit.

+
+
+
+
+ + \ No newline at end of file diff --git a/philcore/views/admin/settings.php b/philcore/views/admin/settings.php new file mode 100644 index 0000000..bb9e927 --- /dev/null +++ b/philcore/views/admin/settings.php @@ -0,0 +1,66 @@ + + +
+ + + +
+
+
+
+
+
philcore-settings.sh
+
+ +
+
+

+ > _ +

+

Systemkonfiguration anpassen.

+
+ +
+
+

> env_variables

+ +
+
+ + +
+ +
+ + +

+ // Hinweis: Änderungen direkt in der .env vornehmen. +

+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/philcore/views/admin/users.php b/philcore/views/admin/users.php new file mode 100644 index 0000000..b0ee14e --- /dev/null +++ b/philcore/views/admin/users.php @@ -0,0 +1,66 @@ + + +
+ + + +
+
+
+
+
+
philcore-users.sh
+
+ +
+
+

+ > _ +

+

Registrierte System-Accounts.

+
+ +
+ + + + + + + + + + + + + + + + + + + +
IDNameEmailStatus
#
+
+
+
+
+ + \ No newline at end of file diff --git a/philcore/views/error.php b/philcore/views/error.php new file mode 100644 index 0000000..b98ef4e --- /dev/null +++ b/philcore/views/error.php @@ -0,0 +1,26 @@ + + +
+
+ 🍕 +
+ +

+ Fehler 404 +

+ +
+

+ > +

+
+ + +
+ + \ No newline at end of file diff --git a/philcore/views/home.php b/philcore/views/home.php new file mode 100644 index 0000000..e59a89f --- /dev/null +++ b/philcore/views/home.php @@ -0,0 +1,24 @@ + + +
+
+ 🍕 +
+ +

+ Hallo, . +

+ +

+ Das Fundament steht. Zeit, etwas Großartiges zu bauen. +

+ + +
+ + \ No newline at end of file diff --git a/philcore/views/inc/admin_footer.php b/philcore/views/inc/admin_footer.php new file mode 100644 index 0000000..d0b4f4f --- /dev/null +++ b/philcore/views/inc/admin_footer.php @@ -0,0 +1,6 @@ +
+
+ PHILCORE_SYSTEM_OS [v1.0.4] +
+ + \ No newline at end of file diff --git a/philcore/views/inc/admin_header.php b/philcore/views/inc/admin_header.php new file mode 100644 index 0000000..02e737e --- /dev/null +++ b/philcore/views/inc/admin_header.php @@ -0,0 +1,28 @@ + + + + + + Admin | <?= htmlspecialchars(Config::get('APP_NAME', 'PhilCore')) ?> + + + + + +
+
+
+ 🍕 + / Admin +
+ Home +
+
+ +
\ No newline at end of file diff --git a/philcore/views/inc/footer.php b/philcore/views/inc/footer.php new file mode 100644 index 0000000..0b65304 --- /dev/null +++ b/philcore/views/inc/footer.php @@ -0,0 +1,16 @@ +
+
+ © . Made with ❤️ and Pizza. +
+ + +
+ + + + + ENV: +
+ + + \ No newline at end of file diff --git a/philcore/views/inc/header.php b/philcore/views/inc/header.php new file mode 100644 index 0000000..cc84f0d --- /dev/null +++ b/philcore/views/inc/header.php @@ -0,0 +1,29 @@ + + + + + + <?= htmlspecialchars(Config::get('APP_NAME', 'PhilCore')) ?> + + + + + +
+
+
+ 🍕 +
+ +
+
+ +
\ No newline at end of file diff --git a/plants/.env.example b/plants/.env.example new file mode 100644 index 0000000..810d64f --- /dev/null +++ b/plants/.env.example @@ -0,0 +1,2 @@ +DB_PASSWORD="your_db_password_here" +GEMINI_API_KEY="your_gemini_api_key_here" diff --git a/plants/.gitignore b/plants/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/plants/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/plants/api.js b/plants/api.js new file mode 100644 index 0000000..ed5fc9a --- /dev/null +++ b/plants/api.js @@ -0,0 +1,173 @@ +// Version: 01.10.2025 11:45 (FINAL FIX) +const API_URL = 'api.php'; + +// --- Hilfsfunktion für Fehlerbehandlung --- +async function handleResponse(response) { + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unbekannter API-Fehler' })); + throw new Error(errorData.error || `HTTP-Fehler! Status: ${response.status}`); + } + return response.json(); +} + +// --- Nutzer-Funktionen --- +export async function getUserSettings() { + const response = await fetch(`${API_URL}?action=get_user_settings`); + return handleResponse(response); +} + +export async function updateUserSettings(settings) { + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ request_type: 'update_user_settings', settings: settings }), + }); + return handleResponse(response); +} + +// --- Lexikon-Funktionen --- +export async function getLexiconEntries() { + const response = await fetch(`${API_URL}?action=get_lexicon_entries`); + return handleResponse(response); +} + +export async function getLexiconEntryDetails(latinName) { + const response = await fetch(`${API_URL}?action=get_lexicon_entry_details&latin_name=${encodeURIComponent(latinName)}`); + return handleResponse(response); +} + + +// --- Pflanzen-Funktionen --- +export async function getPlants() { + const response = await fetch(`${API_URL}?action=get_plants`); + return handleResponse(response); +} + +export async function savePlant(formData) { + const response = await fetch(API_URL, { method: 'POST', body: formData }); + return handleResponse(response); +} + +export async function deletePlant(plantId) { + const response = await fetch(`${API_URL}?id=${plantId}`, { method: 'DELETE' }); + return handleResponse(response); +} + +export async function recordCareAction(plantId, action) { + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ request_type: 'care_action', id: plantId, action }), + }); + return handleResponse(response); +} + +export async function setProfilePicture(plantId, photoPath) { + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ request_type: 'set_profile_picture', plant_id: plantId, photo_path: photoPath }), + }); + return handleResponse(response); +} + +// --- Tagebuch-Funktionen --- +export async function getDiaryEntries(plantId) { + const response = await fetch(`${API_URL}?action=get_diary&plant_id=${plantId}`); + return handleResponse(response); +} + +export async function addDiaryEntry(formData) { + formData.append('action', 'add_diary_entry'); + const response = await fetch(API_URL, { method: 'POST', body: formData }); + return handleResponse(response); +} + +export async function deleteDiaryEntry(entryId) { + const response = await fetch(`${API_URL}?diary_entry_id=${entryId}`, { method: 'DELETE' }); + return handleResponse(response); +} + +// --- KI-Funktionen --- + +// Schritt 1: Identifikation & Foto-Analyse +export async function performInitialAnalysis(base64Image, excludedLatinNames = []) { + let prompt = ` + Du bist ein Botaniker und Foto-Analyst. Führe ZWEI Aufgaben aus und gib das Ergebnis als EINEN einzigen, sauberen JSON-String zurück. Das JSON-Objekt muss zwei Hauptschlüssel haben: "identification" und "photo_analysis". + + 1. **Im "identification"-Objekt:** Identifiziere die Pflanze. + - "name": Der gebräuchlichste Name, so wie er im deutschen Einzelhandel genutzt wird. + - "latin_name": Der korrekte, zweiteilige botanische Name. + - "confidence_score": Bewerte deine Sicherheit KRITISCH (0-100). + - "identification_notes": Kurze Notiz zur Identifizierung. + - REGEL: Falls dein confidence_score < 75 ist, setze alle Werte auf "Unbekannt" oder 0. + + 2. **Im "photo_analysis"-Objekt:** Analysiere die Fotoqualität. + - "photo_quality_score" (integer): Bewerte die Qualität KRITISCH (0-100). + - "is_high_quality" (boolean): true, wenn photo_quality_score > 70 ist. + - "rejection_reason" (string): Kurzer Grund, falls is_high_quality false ist, sonst null. + - "photo_type" (string): 'whole_plant', 'leaf_detail', 'flower_detail', oder 'other'. + - "focal_point" (object): Ein Objekt mit "x"- und "y"-Koordinaten (Prozent von 0-100) des visuellen Mittelpunkts. + - "dominant_color_hex" (string): Der dominante Farbton des Hauptmotivs als HEX-Code. + `; + if (excludedLatinNames.length > 0) { + prompt += ` Ignoriere dabei diese falschen Vorschläge: ${excludedLatinNames.join(', ')}.`; + } + const payload = { + request_type: 'ai_identification', + payload: { "contents": [{ "parts": [ { "text": prompt }, { "inline_data": { "mime_type": "image/jpeg", "data": base64Image } } ] }] } + }; + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const result = await handleResponse(response); + const resultText = result.candidates[0].content.parts[0].text; + return JSON.parse(resultText.replace(/```json|```/g, '').trim()); +} + +// Schritt 2: Detaillierte Anreicherung +export async function aiEnrich(latinName) { + const prompt = ` + Du bist ein Botanik-Experte. Gib detaillierte Pflegedaten für "${latinName}" als sauberen JSON-String zurück. + + ANWEISUNGEN FÜR DEN INHALT: + - Für "description_full": Erstelle einen ausführlichen, enzyklopädischen Text. **Verwende KEINE einleitenden Grußformeln, Willkommenssätze oder persönlichen Anreden wie "Du"**. Gliedere den Text in Abschnitte mit Titeln (z.B. 'Standort & Licht:') und spreche den Leser neutral an (z.B. "Diese Pflanze benötigt..."). Die Titel dürfen KEIN Markdown enthalten. + - Für "care_tips": Erstelle genau drei kurze, prägnante Tipps aus der Ich-Perspektive der Pflanze. + - Für "tags": Gib 3-5 passende Schlagwörter als kommaseparierten String an (z.B. "Anfängerfreundlich, Haustier-sicher, Buntblatt"). + + Das JSON-Objekt MUSS die folgenden Schlüssel enthalten, auch wenn der Wert null ist: "plant_family", "plant_genus", "description_full", "watering_interval_days", "fertilizing_interval_days", "care_tips", "light_requirement", "water_requirement", "humidity_requirement", "care_difficulty", "growth_habit", "mature_size", "toxicity", "edibility", "temperature_preference", "flowering_period", "substrate_recommendation", "native_to", "tags". + + WICHTIGE REGELN FÜR KATEGORIEN: + - Für "substrate_recommendation": Wähle einen oder MEHRERE passende Werte aus der Liste und gib sie als kommaseparierten String zurück: 'Universal-Erde', 'Sukkulenten/Kakteen-Erde', 'Anzucht-Erde', 'Kräuter-Erde', 'Saurer Boden (Moorbeet-Erde)', 'Orchideen-Substrat', 'Hydrokultur/Pon', 'Erde-frei (Aroid-Mix)'. + - Für die folgenden Felder, wähle EXAKT EINEN der angegebenen Werte in Kleinbuchstaben: + - "light_requirement": 'wenig', 'mittel', 'viel'. + - "water_requirement": 'niedrig', 'mittel', 'hoch'. + - "humidity_requirement": 'normal', 'erhöht'. + - "care_difficulty": 'einfach', 'mittel', 'schwer'. + - "growth_habit": 'kletternd', 'hängend', 'aufrecht', 'buschig', 'rosette'. + - "mature_size": 'klein', 'mittel', 'groß'. + - "toxicity": 'ungiftig', 'leicht giftig', 'giftig'. + - "edibility": 'essbar', 'nicht essbar', 'zierfrüchte'. + - "temperature_preference": 'zimmerwarm', 'kühl', 'winterhart'. + - "flowering_period": 'frühling', 'sommer', 'herbst', 'winter', 'ganzjährig', 'selten', oder null. + + FORMATIERUNGSREGELN für "care_tips": + 1. Jeder Tipp MUSS mit einem passenden Emoji und einem Titel beginnen (z.B. "☀️ Licht:"). + 2. Zwischen den Tipps MUSS sich ein doppelter Zeilenumbruch (\\n\\n) befinden.`; + + const payload = { + request_type: 'ai_enrichment', + payload: { "contents": [{ "parts": [ { "text": prompt } ] }] } + }; + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const result = await handleResponse(response); + const resultText = result.candidates[0].content.parts[0].text; + return JSON.parse(resultText.replace(/```json|```/g, '').trim()); +} + diff --git a/plants/api.php b/plants/api.php new file mode 100644 index 0000000..0b8bf8e --- /dev/null +++ b/plants/api.php @@ -0,0 +1,358 @@ +load(); + +if (!isset($_SESSION['user_id'])) { + http_response_code(401); + echo json_encode(['error' => 'Benutzer nicht authentifiziert.']); + exit; +} +$currentUserId = $_SESSION['user_id']; + +// === HELPER FUNCTIONS === +function callGeminiAPI(array $payload, string $apiKey): array { + $googleApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'; + $fullApiUrl = $googleApiUrl . '?key=' . $apiKey; + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $fullApiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_TIMEOUT => 90, + ]); + $response_body = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (curl_errno($ch) || $http_code >= 400) { + $error_message = curl_errno($ch) ? curl_error($ch) : 'KI-API-Fehler mit Status ' . $http_code . ' - ' . $response_body; + curl_close($ch); + throw new Exception('Fehler bei der Kommunikation mit der KI: ' . $error_message); + } + curl_close($ch); + return json_decode($response_body, true); +} + +function processUploadedPhoto(array $fileInfo, int $targetWidth = 800): ?string { + if ($fileInfo['error'] !== UPLOAD_ERR_OK) return null; + $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; + if (!in_array(mime_content_type($fileInfo['tmp_name']), $allowedMimeTypes) || $fileInfo['size'] > (10 * 1024 * 1024)) return null; + $uploadDir = 'uploads/'; + if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + $fileName = uniqid() . '-' . pathinfo($fileInfo['name'], PATHINFO_FILENAME) . '.webp'; + $targetFilePath = $uploadDir . $fileName; + $sourceImage = null; + switch (exif_imagetype($fileInfo['tmp_name'])) { + case IMAGETYPE_JPEG: $sourceImage = imagecreatefromjpeg($fileInfo['tmp_name']); break; + case IMAGETYPE_PNG: $sourceImage = imagecreatefrompng($fileInfo['tmp_name']); break; + case IMAGETYPE_WEBP: $sourceImage = imagecreatefromwebp($fileInfo['tmp_name']); break; + } + if (!$sourceImage) return null; + $resizedImage = (imagesx($sourceImage) > $targetWidth) ? imagescale($sourceImage, $targetWidth) : $sourceImage; + imagewebp($resizedImage, $targetFilePath, 85); + imagedestroy($sourceImage); + if ($resizedImage !== $sourceImage) imagedestroy($resizedImage); + return $targetFilePath; +} + +function getPlantWithDueDates($pdo, $plantId, $userId) { + $stmt = $pdo->prepare("SELECT p.id, p.name, p.latin_name, p.photo, p.last_watered, p.last_fertilized, p.watering_interval_days, p.fertilizing_interval_days, p.care_tips FROM plants p WHERE p.id = ? AND p.user_id = ?"); + $stmt->execute([$plantId, $userId]); + $plant = $stmt->fetch(PDO::FETCH_ASSOC); + if ($plant) { + $plant['watering_interval'] = $plant['watering_interval_days']; + $plant['fertilizing_interval'] = $plant['fertilizing_interval_days']; + $isValidDate = fn($dateStr) => !empty($dateStr) && $dateStr !== '0000-00-00'; + if ((int)$plant['watering_interval'] > 0 && $isValidDate($plant['last_watered'])) { + $plant['next_watering_due'] = (new DateTime($plant['last_watered']))->add(new DateInterval('P' . $plant['watering_interval'] . 'D'))->format('Y-m-d'); + } else { $plant['next_watering_due'] = null; } + if ((int)$plant['fertilizing_interval'] > 0 && $isValidDate($plant['last_fertilized'])) { + $plant['next_fertilizing_due'] = (new DateTime($plant['last_fertilized']))->add(new DateInterval('P' . $plant['fertilizing_interval'] . 'D'))->format('Y-m-d'); + } else { $plant['next_fertilizing_due'] = null; } + } + return $plant; +} + +// === CREDIT MANAGEMENT === +try { + $stmt = $pdo->prepare("SELECT ai_credits_last_reset FROM users WHERE id = ?"); + $stmt->execute([$currentUserId]); + $lastReset = $stmt->fetchColumn(); + $now = new DateTime(); + if ($lastReset === null || (new DateTime($lastReset))->format('Y-m') < $now->format('Y-m')) { + $stmt = $pdo->prepare("UPDATE users SET ai_credits = 100, ai_credits_last_reset = NOW() WHERE id = ?"); + $stmt->execute([$currentUserId]); + } +} catch (Exception $e) { /* non-blocking */ } + +// === API ROUTING === +$method = $_SERVER['REQUEST_METHOD']; +$json_data = json_decode(file_get_contents('php://input'), true); +header('Content-Type: application/json'); + +try { + // --- Handle JSON-based POST requests FIRST --- + if ($method === 'POST' && !empty($json_data)) { + $request_type = $json_data['request_type'] ?? ''; + switch ($request_type) { + case 'ai_identification': + case 'ai_enrichment': + $pdo->beginTransaction(); + $stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1"); + $stmt->execute([$currentUserId]); + if ($stmt->rowCount() === 0) throw new Exception('Nicht genügend KI-Credits (1 benötigt).', 402); + $response_data = callGeminiAPI($json_data['payload'], $_ENV['GEMINI_API_KEY']); + $pdo->commit(); + echo json_encode($response_data); + break; + case 'update_user_settings': + $settings = $json_data['settings'] ?? []; + $allowedKeys = ['theme', 'allow_photo_sharing', 'auto_fill_watering', 'auto_fill_fertilizing', 'auto_fill_care_tips']; + $sqlParts = []; $params = []; + foreach ($settings as $key => $value) { + if (in_array($key, $allowedKeys)) { + $sqlParts[] = "$key = ?"; + $params[] = is_bool($value) ? (int)$value : $value; + } + } + if (empty($sqlParts)) throw new Exception('Keine gültigen Einstellungsfelder.', 400); + $params[] = $currentUserId; + $stmt = $pdo->prepare("UPDATE users SET " . implode(', ', $sqlParts) . " WHERE id = ?"); + $stmt->execute($params); + echo json_encode(['success' => true]); + break; + case 'care_action': + $plantId = $json_data['id']; + $column = ($json_data['action'] === 'water') ? 'last_watered' : 'last_fertilized'; + $entryType = ($json_data['action'] === 'water') ? 'watered' : 'fertilized'; + $pdo->beginTransaction(); + $stmt = $pdo->prepare("UPDATE plants SET $column = CURDATE() WHERE id = ? AND user_id = ?"); + $stmt->execute([$plantId, $currentUserId]); + $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type) VALUES (?, ?, ?)"); + $stmt->execute([$plantId, $currentUserId, $entryType]); + $pdo->commit(); + echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]); + break; + case 'set_profile_picture': + $stmt = $pdo->prepare("UPDATE users SET profile_image = ? WHERE id = ?"); + $stmt->execute([$json_data['photo_path'], $currentUserId]); + echo json_encode(['success' => true, 'new_image' => $json_data['photo_path']]); + break; + default: + throw new Exception('Unbekannter JSON-Request-Typ.', 400); + } + exit; + } + + // --- Handle GET, DELETE, and multipart/form-data POST requests --- + switch ($method) { + case 'GET': + $action = $_GET['action'] ?? 'get_plants'; + switch ($action) { + case 'get_plants': + $stmt = $pdo->prepare("SELECT id FROM plants WHERE user_id = ? ORDER BY id DESC"); + $stmt->execute([$currentUserId]); + $plant_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); + echo json_encode(array_map(fn($id) => getPlantWithDueDates($pdo, $id, $currentUserId), $plant_ids)); + break; + case 'get_diary': + $stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE plant_id = ? AND user_id = ? ORDER BY entry_date DESC"); + $stmt->execute([$_GET['plant_id'], $currentUserId]); + echo json_encode($stmt->fetchAll()); + break; + case 'get_user_settings': + $stmt = $pdo->prepare("SELECT username, email, profile_image, theme, allow_photo_sharing, ai_credits, auto_fill_watering, auto_fill_fertilizing, auto_fill_care_tips FROM users WHERE id = ?"); + $stmt->execute([$currentUserId]); + $userSettings = $stmt->fetch(); + $stmt_count = $pdo->prepare("SELECT COUNT(id) as plant_count FROM plants WHERE user_id = ?"); + $stmt_count->execute([$currentUserId]); + $userSettings['plant_count'] = $stmt_count->fetchColumn(); + echo json_encode($userSettings); + break; + case 'get_lexicon_entries': + $stmt = $pdo->prepare("SELECT pl.name, pl.latin_name, lp.photo_path as photo_url FROM plant_lexicon pl LEFT JOIN (SELECT plant_latin_name, photo_path, ROW_NUMBER() OVER(PARTITION BY plant_latin_name ORDER BY ai_photo_quality_score DESC, id DESC) as rn FROM lexicon_photos WHERE status = 'approved') lp ON pl.latin_name = lp.plant_latin_name AND lp.rn = 1 WHERE pl.entry_count > 0 ORDER BY pl.name ASC"); + $stmt->execute(); + echo json_encode($stmt->fetchAll()); + break; + case 'get_lexicon_entry_details': + $latinName = $_GET['latin_name'] ?? ''; + if (empty($latinName)) throw new Exception('Kein botanischer Name angegeben.', 400); + $stmt = $pdo->prepare("SELECT * FROM plant_lexicon WHERE latin_name = ?"); + $stmt->execute([$latinName]); + $plantDetails = $stmt->fetch(); + if (!$plantDetails) throw new Exception('Pflanze nicht im Lexikon gefunden.', 404); + $stmtPhotos = $pdo->prepare("SELECT photo_path FROM lexicon_photos WHERE plant_latin_name = ? AND status = 'approved' ORDER BY ai_photo_quality_score DESC, id DESC LIMIT 10"); + $stmtPhotos->execute([$latinName]); + $plantDetails['photo_gallery'] = $stmtPhotos->fetchAll(PDO::FETCH_COLUMN); + echo json_encode($plantDetails); + break; + default: + throw new Exception('Ungültige GET-Aktion.', 400); + } + break; + + case 'POST': + $action = $_POST['action'] ?? 'save_plant'; + switch ($action) { + case 'add_diary_entry': + $photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo'], 600) : null; + $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, notes, photo, entry_type) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$_POST['plant_id'], $currentUserId, $_POST['notes'] ?? null, $photoPath, isset($_POST['is_milestone']) ? 'milestone' : 'manual']); + $newEntryId = $pdo->lastInsertId(); + $stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE id = ?"); + $stmt->execute([$newEntryId]); + echo json_encode(['success' => true, 'entry' => $stmt->fetch()]); + break; + case 'save_plant': + $name = trim($_POST['name'] ?? ''); + if (empty($name)) throw new Exception('Der Name der Pflanze darf nicht leer sein.', 400); + + $plantId = $_POST['id'] ?? null; + $latinName = trim($_POST['latin_name'] ?? '') ?: null; + $photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo']) : null; + $finalPhotoPath = $photoPath ?? $_POST['current_photo'] ?? null; + + // ROBUSTE PRÜFUNG DER INTERVALLE + $wateringInterval = (isset($_POST['watering_interval']) && is_numeric($_POST['watering_interval'])) ? (int)$_POST['watering_interval'] : null; + $fertilizingInterval = (isset($_POST['fertilizing_interval']) && is_numeric($_POST['fertilizing_interval'])) ? (int)$_POST['fertilizing_interval'] : null; + + $careTips = trim($_POST['care_tips'] ?? '') ?: null; + + $pdo->beginTransaction(); + + $oldLatinName = null; + if ($plantId) { // Edit + $stmtOld = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?"); + $stmtOld->execute([$plantId, $currentUserId]); + $oldLatinName = $stmtOld->fetchColumn(); + + $stmt = $pdo->prepare("UPDATE plants SET name = ?, latin_name = ?, photo = ?, watering_interval_days = ?, fertilizing_interval_days = ?, care_tips = ? WHERE id = ? AND user_id = ?"); + $stmt->execute([$name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips, $plantId, $currentUserId]); + } else { // Add + $stmt = $pdo->prepare("INSERT INTO plants (user_id, name, latin_name, photo, last_watered, last_fertilized, watering_interval_days, fertilizing_interval_days, care_tips) VALUES (?, ?, ?, ?, CURDATE(), CURDATE(), ?, ?, ?)"); + $stmt->execute([$currentUserId, $name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips]); + $plantId = $pdo->lastInsertId(); + $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type, notes) VALUES (?, ?, 'milestone', 'Pflanze hinzugefügt')"); + $stmt->execute([$plantId, $currentUserId]); + } + + // Handle lexicon entry count changes + if ($oldLatinName && $oldLatinName !== $latinName) { + $stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0"); + $stmtDec->execute([$oldLatinName]); + } + if ($latinName && (!$oldLatinName || $oldLatinName !== $latinName)) { + $stmt = $pdo->prepare("INSERT INTO plant_lexicon (latin_name, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE entry_count = entry_count + 1, name = VALUES(name)"); + $stmt->execute([$latinName, $name]); + } + + // Process AI data for lexicon if available + $enrichmentData = json_decode($_POST['ai_enrichment_json'] ?? '{}', true); + + if(!empty($enrichmentData)) { + $stmt = $pdo->prepare("UPDATE plant_lexicon SET plant_family = :plant_family, plant_genus = :plant_genus, description_full = :description_full, care_tips = :care_tips, watering_interval_days = :watering_interval_days, fertilizing_interval_days = :fertilizing_interval_days, light_requirement = :light_requirement, water_requirement = :water_requirement, humidity_requirement = :humidity_requirement, care_difficulty = :care_difficulty, growth_habit = :growth_habit, mature_size = :mature_size, toxicity = :toxicity, edibility = :edibility, temperature_preference = :temperature_preference, flowering_period = :flowering_period, substrate_recommendation = :substrate_recommendation, native_to = :native_to, tags = :tags, updated_at = NOW() WHERE latin_name = :latin_name AND description_full IS NULL"); + $stmt->execute([ + 'plant_family' => $enrichmentData['plant_family'] ?? null, + 'plant_genus' => $enrichmentData['plant_genus'] ?? null, + 'description_full' => $enrichmentData['description_full'] ?? null, + 'care_tips' => $enrichmentData['care_tips'] ?? null, + 'watering_interval_days' => $enrichmentData['watering_interval_days'] ?? null, + 'fertilizing_interval_days' => $enrichmentData['fertilizing_interval_days'] ?? null, + 'light_requirement' => $enrichmentData['light_requirement'] ?? null, + 'water_requirement' => $enrichmentData['water_requirement'] ?? null, + 'humidity_requirement' => $enrichmentData['humidity_requirement'] ?? null, + 'care_difficulty' => $enrichmentData['care_difficulty'] ?? null, + 'growth_habit' => $enrichmentData['growth_habit'] ?? null, + 'mature_size' => $enrichmentData['mature_size'] ?? null, + 'toxicity' => $enrichmentData['toxicity'] ?? null, + 'edibility' => $enrichmentData['edibility'] ?? null, + 'temperature_preference' => $enrichmentData['temperature_preference'] ?? null, + 'flowering_period' => $enrichmentData['flowering_period'] ?? null, + 'substrate_recommendation' => $enrichmentData['substrate_recommendation'] ?? null, + 'native_to' => $enrichmentData['native_to'] ?? null, + 'tags' => $enrichmentData['tags'] ?? null, + 'latin_name' => $latinName + ]); + } + + // Process photo for lexicon if allowed and available + $fullAiResponse = json_decode($_POST['ai_full_response_json'] ?? '{}', true); + $stmtAllow = $pdo->prepare("SELECT allow_photo_sharing FROM users WHERE id = ?"); + $stmtAllow->execute([$currentUserId]); + if ($stmtAllow->fetchColumn() && $finalPhotoPath && $photoPath && !empty($fullAiResponse['photo_analysis'])) { + $photoData = $fullAiResponse['photo_analysis']; + $isHighQuality = $photoData['is_high_quality'] ?? false; + + $stmt = $pdo->prepare("INSERT INTO lexicon_photos (user_id, plant_latin_name, photo_path, status, rejection_reason, ai_confidence, photo_type, focal_point_x, focal_point_y, dominant_color_hex, ai_photo_quality_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $currentUserId, + $latinName, + $finalPhotoPath, + ($isHighQuality ? 'approved' : 'rejected'), + $photoData['rejection_reason'] ?? null, + $fullAiResponse['identification']['confidence_score'] ?? null, + $photoData['photo_type'] ?? null, + $photoData['focal_point']['x'] ?? null, + $photoData['focal_point']['y'] ?? null, + $photoData['dominant_color_hex'] ?? null, + $photoData['photo_quality_score'] ?? null + ]); + } + + $pdo->commit(); + echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]); + break; + default: + throw new Exception('Ungültige Formular-Aktion.', 400); + } + break; + + case 'DELETE': + $plantId = $_GET['id'] ?? null; + if ($plantId) { + $pdo->beginTransaction(); + + $stmtLatin = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?"); + $stmtLatin->execute([$plantId, $currentUserId]); + $latinNameToDecrement = $stmtLatin->fetchColumn(); + + $stmt = $pdo->prepare("DELETE FROM plants WHERE id = ? AND user_id = ?"); + $stmt->execute([$plantId, $currentUserId]); + + if ($latinNameToDecrement) { + $stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0"); + $stmtDec->execute([$latinNameToDecrement]); + } + + $pdo->commit(); + echo json_encode(['success' => true]); + } else if (isset($_GET['diary_entry_id'])) { + $entryId = $_GET['diary_entry_id']; + $stmt = $pdo->prepare("DELETE FROM plant_diary WHERE id = ? AND user_id = ?"); + $stmt->execute([$entryId, $currentUserId]); + echo json_encode(['success' => $stmt->rowCount() > 0]); + } + else { + throw new Exception('Keine ID zum Löschen angegeben.', 400); + } + break; + + default: + throw new Exception('Methode nicht erlaubt.', 405); + } +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + $code = $e->getCode(); + if (!is_int($code) || $code < 400 || $code >= 600) { + $code = 500; + } + http_response_code($code); + echo json_encode(['error' => $e->getMessage()]); +} +?> + diff --git a/plants/api_proxy.php b/plants/api_proxy.php new file mode 100644 index 0000000..7995910 --- /dev/null +++ b/plants/api_proxy.php @@ -0,0 +1,60 @@ +load(); + +header('Content-Type: application/json'); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['error' => 'Nur POST-Anfragen sind erlaubt.']); + exit; +} + +// Dein geheimer API-Schlüssel wird sicher aus der .env-Datei geladen +$apiKey = $_ENV['GEMINI_API_KEY']; + +$json_input = file_get_contents('php://input'); +$request_data = json_decode($json_input, true); + +if (json_last_error() !== JSON_ERROR_NONE || !isset($request_data['payload'])) { + http_response_code(400); + echo json_encode(['error' => 'Ungültige Anfrage-Daten.']); + exit; +} + +$payload = $request_data['payload']; + +// Die Ziel-URL ist jetzt fest im Code hinterlegt und kann nicht von außen manipuliert werden. +$googleApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'; + +$fullApiUrl = $googleApiUrl . '?key=' . $apiKey; + +$ch = curl_init(); + +curl_setopt_array($ch, [ + CURLOPT_URL => $fullApiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], +]); + +$response_body = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + +if (curl_errno($ch)) { + http_response_code(500); + echo json_encode(['error' => 'Fehler bei der Weiterleitung: ' . curl_error($ch)]); + exit; +} + +curl_close($ch); + +http_response_code($http_code); +echo $response_body; +?> \ No newline at end of file diff --git a/plants/auth.js b/plants/auth.js new file mode 100644 index 0000000..0f97b68 --- /dev/null +++ b/plants/auth.js @@ -0,0 +1,117 @@ +// Version: 24.09.2025 16:29 + +// === DOM-Elemente === +const loginFormContainer = document.getElementById('login-form-container'); +const registerFormContainer = document.getElementById('register-form-container'); +const showRegisterLink = document.getElementById('showRegister'); +const showLoginLink = document.getElementById('showLogin'); +const registerForm = document.getElementById('registerForm'); +const loginForm = document.getElementById('loginForm'); + +const AUTH_API_URL = 'auth_api.php'; + +// === Toast-Funktion (isoliert für diese Seite) === +function showToast(message, isError = false) { + const existingToast = document.querySelector('.toast'); + if (existingToast) existingToast.remove(); + + const toast = document.createElement('div'); + toast.className = `toast ${isError ? 'error' : ''}`; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => toast.classList.add('show'), 10); + + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => document.body.removeChild(toast), 300); + }, 3000); +} + + +// === Event Handlers === + +function switchForms(e) { + e.preventDefault(); + if (loginFormContainer.style.display === 'none') { + registerFormContainer.style.display = 'none'; + loginFormContainer.style.display = 'block'; + } else { + loginFormContainer.style.display = 'none'; + registerFormContainer.style.display = 'block'; + } +} + +async function handleRegister(e) { + e.preventDefault(); + const submitBtn = registerForm.querySelector('button[type="submit"]'); + submitBtn.disabled = true; + submitBtn.textContent = 'Erstelle Konto...'; + + const formData = new FormData(registerForm); + const data = Object.fromEntries(formData.entries()); + data.action = 'register'; + + try { + const response = await fetch(AUTH_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.error); + + showToast(result.message || 'Registrierung erfolgreich!'); + registerForm.reset(); + switchForms(e); // Zum Login wechseln + } catch (error) { + showToast(error.message, true); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Konto erstellen'; + } +} + + +async function handleLogin(e) { + e.preventDefault(); + const submitBtn = loginForm.querySelector('button[type="submit"]'); + submitBtn.disabled = true; + submitBtn.textContent = 'Logge ein...'; + + const formData = new FormData(loginForm); + const data = Object.fromEntries(formData.entries()); + data.action = 'login'; + + try { + const response = await fetch(AUTH_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.error); + + // Login war erfolgreich, lade die Seite neu + // Der Server wird uns jetzt die Pflanzen-Ansicht schicken + location.reload(); + + } catch(error) { + showToast(error.message, true); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Einloggen'; + } +} + + +// === Initialisierung === +function init() { + showRegisterLink.addEventListener('click', switchForms); + showLoginLink.addEventListener('click', switchForms); + registerForm.addEventListener('submit', handleRegister); + loginForm.addEventListener('submit', handleLogin); +} + +document.addEventListener('DOMContentLoaded', init); + diff --git a/plants/auth_api.php b/plants/auth_api.php new file mode 100644 index 0000000..7f8291f --- /dev/null +++ b/plants/auth_api.php @@ -0,0 +1,86 @@ +prepare("SELECT id FROM users WHERE email = ?"); + $stmt->execute([$email]); + if ($stmt->fetch()) { + throw new Exception('Diese E-Mail-Adresse ist bereits registriert.'); + } + + $password_hash = password_hash($password, PASSWORD_ARGON2ID); + $stmt = $pdo->prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)"); + $stmt->execute([$username, $email, $password_hash]); + + echo json_encode(['success' => true, 'message' => 'Konto erfolgreich erstellt! Du kannst dich jetzt einloggen.']); + + } elseif ($action === 'login') { + // --- Login --- + $email = $data['email'] ?? ''; + $password = $data['password'] ?? ''; + + if (!filter_var($email, FILTER_VALIDATE_EMAIL) || empty($password)) { + throw new Exception('Bitte gib E-Mail und Passwort ein.'); + } + + $stmt = $pdo->prepare("SELECT id, username, password_hash FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + // Passwort ist korrekt, starte die Session + session_regenerate_id(true); // Wichtig für die Sicherheit + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + + echo json_encode(['success' => true, 'message' => 'Login erfolgreich!']); + } else { + throw new Exception('E-Mail oder Passwort ist falsch.'); + } + + } elseif ($action === 'logout') { + // --- Logout --- + session_destroy(); + echo json_encode(['success' => true, 'message' => 'Du wurdest ausgeloggt.']); + + } else { + throw new Exception('Ungültige Aktion.'); + } +} catch (Exception $e) { + http_response_code(400); // Bad Request + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} +?> + diff --git a/plants/components.css b/plants/components.css new file mode 100644 index 0000000..8e95740 --- /dev/null +++ b/plants/components.css @@ -0,0 +1,761 @@ +/* Version: 30.09.2025 08:00 */ + +/* =============================================== */ +/* === PLANT CARDS (GRID VIEW) === */ +/* =============================================== */ +.plant-card { + background-color: transparent; + width: 100%; + aspect-ratio: 1 / 1.35; + perspective: 1000px; + opacity: 0; + transform: translateY(20px); + animation: fadeIn 0.5s ease-out forwards; +} +.list-view .plant-card { + display: none; +} +.plant-card.menu-open { + position: relative; + z-index: 10; +} +.plant-card-inner { + position: relative; + width: 100%; + height: 100%; + transition: transform 0.6s, box-shadow 0.3s ease; + transform-style: preserve-3d; + border-radius: var(--border-radius-large); + border: 1px solid var(--border-color); + box-shadow: 0 4px 10px var(--shadow-color); +} +.plant-card:hover .plant-card-inner { + box-shadow: 0 8px 20px var(--shadow-color-hover); +} +.plant-card-inner.is-flipped { + transform: rotateY(180deg); +} +.plant-card-front, .plant-card-back { + position: absolute; + width: 100%; + height: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + background-color: var(--surface-color); + border-radius: var(--border-radius-large); + overflow: hidden; + display: flex; + flex-direction: column; +} +.plant-card-back { + transform: rotateY(180deg); + padding: 25px 20px 20px; + justify-content: flex-start; + cursor: pointer; +} +.plant-img-container { + width: 100%; + aspect-ratio: 1 / 1; + overflow: hidden; + position: relative; +} +.plant-img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} +.plant-name-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.6), transparent); + padding: 20px 15px 15px; +} +.plant-name-overlay h3 { + margin: 0; + color: white; + font-size: 1.5rem; + text-shadow: 1px 1px 3px rgba(0,0,0,0.5); +} +.plant-info { + padding: 15px; + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; +} +.card-actions { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; +} +.action-btn { + width: 100%; + height: auto; + background-color: transparent; + border: 1px solid var(--border-color); + border-radius: 10px; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + padding: 10px 12px; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; +} +.action-btn:hover { + background-color: #fafafa; + border-color: #e0e0e0; +} +.action-btn:active { + background-color: #f5f5f5; + transform: none !important; +} +.action-btn-progress { + position: absolute; + bottom: 0; + left: 0; + height: 4px; + border-radius: 0; + transition: width 0.4s ease, background-color 0.4s ease; +} +.action-btn-content { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + font-weight: 600; + pointer-events: none; + color: var(--text-color); + padding: 0; +} +.action-btn.simple { + background-color: transparent; + border-color: var(--border-color); +} +.action-btn.simple:hover { + background-color: #f5f5f5; +} +.action-btn.simple .action-btn-content { + justify-content: flex-start; + color: var(--text-color); +} +.action-btn.simple .care-status, +.action-btn.simple .action-btn-progress { + display: none; +} +.action-btn-content .care-status { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-light); + background-color: transparent; + padding: 0; +} +.water-btn .action-btn-progress[data-status="green"], +.water-btn .action-btn-progress[data-status="yellow"] { + background-color: var(--status-blue); +} +.fertilize-btn .action-btn-progress[data-status="green"] { + background-color: var(--secondary-color); +} +.action-btn-progress[data-status="yellow"] { + background-color: var(--status-yellow); +} +.action-btn-progress[data-status="red"] { + background-color: var(--status-red); +} +.action-btn-progress[data-status="disabled"] { + background-color: #e0e0e0; +} +.action-btn:disabled { + cursor: wait; +} +.action-btn.is-loading .action-btn-content > span:first-child { + display: none; +} +.action-btn.is-loading .care-status { + animation: pulse 1.5s infinite ease-in-out; +} +.plant-card-back h3 { + margin-top: 0; + margin-bottom: 15px; + color: var(--primary-color); +} +.care-tips-content { + flex-grow: 1; + overflow-y: auto; + font-size: 0.85rem; + line-height: 1.5; + padding-right: 5px; +} +.care-tips-content p { + margin: 0 0 1em 0; +} +.care-tips-content p:last-child { + margin-bottom: 0; +} +.care-tips-content strong { + color: var(--primary-color); + font-weight: 700; +} +.care-tips-latin-name { + background-color: #f8f8f8; + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 15px; + font-size: 0.9em; + border: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: flex-start; /* HIER IST DIE ÄNDERUNG */ + cursor: pointer; + transition: background-color 0.2s, box-shadow 0.2s; +} +.care-tips-latin-name:hover { + background-color: #fefefe; + box-shadow: 0 2px 8px var(--shadow-color); +} +.care-tips-latin-name strong { + color: var(--primary-color); + display: block; + font-weight: 600; + font-size: 0.9em; + margin-bottom: 2px; +} +.care-tips-latin-name em { + font-style: italic; + color: var(--text-color); +} +.latin-name-hint { + font-size: 0.8em; + font-weight: 600; + color: var(--text-light); + transition: color 0.2s; + flex-shrink: 0; + padding-left: 10px; +} +.care-tips-latin-name:hover .latin-name-hint { + color: var(--text-color); +} +.back-controls { + margin-top: auto; + padding-top: 15px; + text-align: center; + color: var(--text-light); + font-size: 0.85rem; + flex-shrink: 0; +} +.back-controls svg { + vertical-align: middle; + margin-right: 5px; +} +.card-overlay { + position: absolute; + top: 10px; + right: 10px; + display: flex; + gap: 8px; + z-index: 5; +} +.card-btn { + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + background-color: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(5px); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-color); + transition: background-color 0.2s, transform 0.2s; +} +.card-btn:hover { + background-color: white; + transform: scale(1.1); +} +.card-btn:active { + transform: scale(1); +} +.menu-container { + position: relative; +} +.menu-dropdown { + display: none; + position: absolute; + top: 40px; + right: 0; + background: white; + border-radius: var(--border-radius); + box-shadow: 0 4px 15px var(--shadow-color); + overflow: hidden; + z-index: 10; + width: 150px; +} +.menu-dropdown.show { + display: block; +} +.menu-dropdown button { + display: block; + width: 100%; + padding: 12px 15px; + background: none; + border: none; + text-align: left; + cursor: pointer; + font-size: 0.95rem; + color: var(--text-color); +} +.menu-dropdown button:hover { + background-color: #f5f5f5; +} +.menu-dropdown .delete-btn:hover { + background-color: var(--status-red); + color: white; +} + +/* =============================================== */ +/* === LIST VIEW === */ +/* =============================================== */ +.plant-list-item { + display: none; +} +.list-view .plant-list-item { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 15px; + background-color: var(--surface-color); + padding: 12px; + border-radius: var(--border-radius-large); + border: 1px solid var(--border-color); + opacity: 0; + transform: translateY(20px); + animation: fadeIn 0.5s ease-out forwards; + width: 100%; + transition: background-color 0.3s ease; +} +.plant-list-item.menu-open { + position: relative; + z-index: 10; +} +.plant-list-item.tips-visible { + background-color: #f9f9f9; +} +.list-item-img-container { + width: 70px; + height: 70px; + flex-shrink: 0; + border-radius: var(--border-radius); + overflow: hidden; +} +.list-item-img { + width: 100%; + height: 100%; + object-fit: cover; +} +.list-item-info { + flex-grow: 1; +} +.list-item-info h3 { + margin: 0 0 4px 0; + font-size: 1.2rem; +} +.list-item-actions { + display: flex; + gap: 10px; + align-items: center; +} +.list-item-actions .action-btn { + min-width: 165px; +} +.list-item-menu { + display: flex; + gap: 8px; +} +.list-item-care-tips { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s ease-out; + width: 100%; + font-size: 0.85rem; + box-sizing: border-box; + color: var(--text-color); + grid-column: 1 / -1; +} +.list-item-care-tips-content { + padding: 0; + border-top: 1px solid transparent; + transition: padding 0.4s ease-out, border-color 0.4s ease-out; +} +.list-item-care-tips-content p { + margin: 0 0 1em 0; +} +.list-item-care-tips-content p:last-child { + margin-bottom: 0; +} +.list-item-care-tips-content strong { + color: var(--primary-color); + font-weight: 700; +} +.plant-list-item.tips-visible .list-item-care-tips { + max-height: 500px; +} +.plant-list-item.tips-visible .list-item-care-tips-content { + padding: 15px 0 5px; + margin-top: 10px; + border-top: 1px solid var(--border-color); +} + + +/* =============================================== */ +/* === MOBILE & RESPONSIVE STYLES === */ +/* =============================================== */ +.plant-compact-card { + display: none; +} +.plant-detail-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(30, 32, 30, 0.6); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} +.plant-detail-overlay.show { + opacity: 1; + visibility: visible; +} +.plant-detail-content { + position: relative; + width: 100%; + max-width: 380px; + transform: scale(0.95); + transition: transform 0.3s ease; +} +.plant-detail-content .plant-card { + display: block; + opacity: 1; + transform: none; +} +.plant-detail-overlay.show .plant-detail-content { + transform: scale(1); +} +.plant-detail-close-btn { + position: absolute; + top: 15px; + right: 15px; + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background-color: rgba(255, 255, 255, 0.9); + color: var(--text-color); + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + z-index: 1101; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; +} + +@media (max-width: 768px) { + .add-btn span { + display: none; + } + .user-menu .username { + display: none; + } + .list-view .plant-list-item { + display: grid; + } + .list-view .plant-card { + display: none; + } + .list-view .plant-list-item { + grid-template-columns: 60px 1fr; + grid-template-rows: auto auto; + gap: 2px 12px; + align-items: center; + position: relative; + padding: 12px; + } + .list-view .list-item-img-container { + grid-row: 1 / 3; + width: 60px; + height: 60px; + } + .list-view .list-item-info { + grid-column: 2; + grid-row: 1; + align-self: end; + } + .list-view .list-item-info h3 { + font-size: 1.1rem; + margin-bottom: 2px; + } + .list-view .list-item-actions { + grid-column: 2; + grid-row: 2; + align-self: start; + flex-direction: row; + gap: 8px; + width: 100%; + } + .list-view .list-item-actions .action-btn { + flex: 1; + min-width: 0; + padding: 6px 8px; + height: auto; + border-radius: 8px; + } + .list-view .list-item-actions .action-btn-progress { + top: 0; + bottom: 0; + height: 100%; + opacity: 0.15; + } + .list-view .list-item-actions .action-btn-content span:first-child { + font-size: 0.8rem; + } + .list-view .list-item-actions .action-btn-content .care-status { + font-size: 0.75rem; + } + .list-view .list-item-menu { + position: absolute; + top: 8px; + right: 8px; + } + .compact-view-enabled.plant-grid:not(.list-view) { + grid-template-columns: repeat(3, 1fr); + gap: 15px; + align-items: start; + } + .compact-view-enabled:not(.list-view) .plant-list-item, + .compact-view-enabled:not(.list-view) .plant-card { + display: none; + } + .compact-view-enabled:not(.list-view) .plant-compact-card { + display: flex; + flex-direction: column; + width: 100%; + background-color: var(--surface-color); + border-radius: var(--border-radius); + overflow: hidden; + position: relative; + box-shadow: 0 2px 6px var(--shadow-color); + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + -webkit-tap-highlight-color: transparent; + opacity: 0; + transform: translateY(20px); + animation: fadeIn 0.5s ease-out forwards; + } + .plant-compact-card:active { + transform: scale(0.96); + box-shadow: 0 1px 3px var(--shadow-color); + } + .compact-img-container { + width: 100%; + aspect-ratio: 1 / 1; + position: relative; + flex-shrink: 0; + overflow: hidden; + } + .compact-img { + width: 100%; + height: 100%; + object-fit: cover; + } + .compact-name-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); + padding: 15px 8px 8px; + color: white; + text-align: center; + font-size: 0.8rem; + font-weight: 600; + pointer-events: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .compact-card-actions { + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + border-top: 1px solid var(--border-color); + flex-shrink: 0; + container-type: inline-size; + container-name: compact-actions; + } + .compact-action-pill { + width: 100%; + background-color: transparent; + border: 1px solid var(--border-color); + border-radius: 8px; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + padding: 5px 8px; + font-size: 0.8rem; + } + .compact-action-progress { + position: absolute; + top: 0; + bottom: 0; + left: 0; + opacity: 0.2; + transition: width 0.4s ease, background-color 0.4s ease; + } + .compact-action-content { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + font-weight: 600; + pointer-events: none; + color: var(--text-color); + } + .action-label { + display: flex; + align-items: center; + gap: 4px; + } + .action-emoji { + display: none; + font-size: 0.9rem; + } + .compact-action-content .care-status { + font-size: 0.75rem; + color: var(--text-light); + } + .compact-action-progress[data-status="green"] { + background-color: var(--status-blue); + } + .compact-action-pill[data-action="fertilize"] .compact-action-progress[data-status="green"] { + background-color: var(--secondary-color); + } + .compact-action-progress[data-status="yellow"] { + background-color: var(--status-yellow); + } + .compact-action-progress[data-status="red"] { + background-color: var(--status-red); + } + .compact-action-progress[data-status="disabled"] { + background-color: #e0e0e0; + } +} + +@container compact-actions (max-width: 110px) { + .compact-action-pill .action-text { + display: none; + } + .compact-action-pill .action-emoji { + display: inline; + } + .compact-action-content { + justify-content: space-between; + } +} + +/* =============================================== */ +/* === DARK MODE OVERRIDES === */ +/* =============================================== */ + +/* --- Manually selected Dark Mode --- */ +body[data-theme='dark'] .action-btn:hover { + background-color: #2a2a2a; + border-color: #444; +} +body[data-theme='dark'] .card-btn { + background-color: rgba(0, 0, 0, 0.3); + color: var(--text-color); +} +body[data-theme='dark'] .card-btn:hover { + background-color: rgba(0, 0, 0, 0.5); +} +body[data-theme='dark'] .menu-dropdown { + background: #2c2c2c; + box-shadow: 0 4px 15px var(--shadow-color); +} +body[data-theme='dark'] .menu-dropdown button:hover { + background-color: #3a3a3a; +} +body[data-theme='dark'] .menu-dropdown .delete-btn:hover { + background-color: var(--status-red); + color: white; +} +body[data-theme='dark'] .care-tips-latin-name { + background-color: #2a2a2a; +} +body[data-theme='dark'] .plant-list-item.tips-visible { + background-color: #252525; +} +body[data-theme='dark'] .plant-detail-close-btn { + background-color: rgba(40, 40, 40, 0.8); + color: var(--text-color); +} + +/* --- Auto Dark Mode (from System) --- */ +@media (prefers-color-scheme: dark) { + body[data-theme='auto'] .action-btn:hover { + background-color: #2a2a2a; + border-color: #444; + } + body[data-theme='auto'] .card-btn { + background-color: rgba(0, 0, 0, 0.3); + color: var(--text-color); + } + body[data-theme='auto'] .card-btn:hover { + background-color: rgba(0, 0, 0, 0.5); + } + body[data-theme='auto'] .menu-dropdown { + background: #2c2c2c; + box-shadow: 0 4px 15px var(--shadow-color); + } + body[data-theme='auto'] .menu-dropdown button:hover { + background-color: #3a3a3a; + } + body[data-theme='auto'] .menu-dropdown .delete-btn:hover { + background-color: var(--status-red); + color: white; + } + body[data-theme='auto'] .care-tips-latin-name { + background-color: #2a2a2a; + } + body[data-theme='auto'] .plant-list-item.tips-visible { + background-color: #252525; + } + body[data-theme='auto'] .plant-detail-close-btn { + background-color: rgba(40, 40, 40, 0.8); + color: var(--text-color); + } +} + diff --git a/plants/composer.json b/plants/composer.json new file mode 100644 index 0000000..d7c6bb7 --- /dev/null +++ b/plants/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "vlucas/phpdotenv": "^5.6" + } +} diff --git a/plants/composer.lock b/plants/composer.lock new file mode 100644 index 0000000..82fd965 --- /dev/null +++ b/plants/composer.lock @@ -0,0 +1,492 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "108be68e4e2b97fed51d36a10eed0849", + "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/plants/db.php b/plants/db.php new file mode 100644 index 0000000..59b224d --- /dev/null +++ b/plants/db.php @@ -0,0 +1,34 @@ +load(); + +// Datenbankverbindung herstellen +$host = 'localhost'; +$dbname = 'cmwrbwr55_philidb'; +$username = 'cmwrbwr55_philidb'; +// Das Passwort wird jetzt sicher aus der Umgebungsvariable geladen +$password = $_ENV['DB_PASSWORD']; +$charset = 'utf8mb4'; + +$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset"; +$options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; + +try { + $pdo = new PDO($dsn, $username, $password, $options); +} catch (\PDOException $e) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Datenbankverbindung fehlgeschlagen.']); + exit; +} +?> \ No newline at end of file diff --git a/plants/img/icon.png b/plants/img/icon.png new file mode 100644 index 0000000..0f6223e Binary files /dev/null and b/plants/img/icon.png differ diff --git a/plants/img/logo.webp b/plants/img/logo.webp new file mode 100644 index 0000000..74a728e Binary files /dev/null and b/plants/img/logo.webp differ diff --git a/plants/index.php b/plants/index.php new file mode 100644 index 0000000..27b0a6d --- /dev/null +++ b/plants/index.php @@ -0,0 +1,341 @@ +prepare("SELECT theme FROM users WHERE id = ?"); + $stmt->execute([$_SESSION['user_id']]); + $result = $stmt->fetch(); + if ($result && !empty($result['theme'])) { + $user_theme = $result['theme']; + } + } catch (PDOException $e) { + // Fehler ignorieren + } +} + +?> + + + + + + Plants + + + + + + + + + + +
+
+
+ +

Plants

+
+
+ + + +
+ + +
+
+
+
+ +
+
+
+
+ +
+
+ + +
+
+
+ +
+
+
+ + + + + + + + +
+
+ +
+ + + + + + + + +
+
+

Plants

+
+

Willkommen zurück!

Bitte logge dich ein, um deine Pflanzen zu sehen.

+
+
+
+ +
+

Noch kein Konto? Jetzt registrieren

+
+ +
+
+ + + + + + + diff --git a/plants/lexikon.js b/plants/lexikon.js new file mode 100644 index 0000000..252cdb5 --- /dev/null +++ b/plants/lexikon.js @@ -0,0 +1,200 @@ +// Version: 29.09.2025 16:05 +import * as api from './api.js'; + +// --- State-Variablen --- +let lexiconGrid; +let lexiconSearch; +let lexiconDetailModal; +let allPlants = []; + +// --- Hilfsfunktion, um das Modal-Element zu holen --- +function getModalElement() { + if (!lexiconDetailModal) { + lexiconDetailModal = document.getElementById('lexiconDetailModal'); + } + return lexiconDetailModal; +} + +// --- Hauptfunktionen für das Lexikon-Modal (exportiert) --- + +export async function openLexiconDetailModal(latinName) { + const modal = getModalElement(); + if (!modal) { + console.error("Lexikon-Detail-Modal nicht im DOM gefunden!"); + return; + } + + try { + const plant = await api.getLexiconEntryDetails(latinName); + + const mainImage = modal.querySelector('#lexiconModalImage'); + const placeholderImg = 'https://placehold.co/800x600/eeeeee/3D403D?text=' + encodeURIComponent(plant.name); + + modal.querySelector('#lexiconModalPlantName').textContent = plant.name; + modal.querySelector('#lexiconModalLatinName').textContent = plant.latin_name; + + const thumbnailContainer = modal.querySelector('.lexicon-modal-thumbnails'); + thumbnailContainer.innerHTML = ''; + + if (plant.photo_gallery && plant.photo_gallery.length > 0) { + mainImage.src = plant.photo_gallery[0]; + + if (plant.photo_gallery.length > 1) { + thumbnailContainer.style.display = 'flex'; + plant.photo_gallery.forEach((photoUrl, index) => { + const thumb = document.createElement('img'); + thumb.src = photoUrl; + thumb.className = 'thumbnail-img'; + if (index === 0) thumb.classList.add('active'); + thumb.addEventListener('click', (e) => { + mainImage.src = photoUrl; + thumbnailContainer.querySelectorAll('.thumbnail-img').forEach(t => t.classList.remove('active')); + e.target.classList.add('active'); + }); + thumbnailContainer.appendChild(thumb); + }); + } else { + thumbnailContainer.style.display = 'none'; + } + } else { + mainImage.src = placeholderImg; + thumbnailContainer.style.display = 'none'; + } + mainImage.onerror = function() { this.src = placeholderImg; }; + + const familySpan = modal.querySelector('#lexiconModalFamily'); + const genusSpan = modal.querySelector('#lexiconModalGenus'); + const metaContainer = modal.querySelector('.lexicon-modal-meta'); + const hasFamily = plant.plant_family && plant.plant_family.toLowerCase() !== 'unbekannt'; + const hasGenus = plant.plant_genus && plant.plant_genus.toLowerCase() !== 'unbekannt'; + familySpan.textContent = hasFamily ? plant.plant_family : ''; + familySpan.parentElement.style.display = hasFamily ? 'block' : 'none'; + genusSpan.textContent = hasGenus ? plant.plant_genus : ''; + genusSpan.parentElement.style.display = hasGenus ? 'block' : 'none'; + metaContainer.style.display = (hasFamily || hasGenus) ? 'flex' : 'none'; + + const descriptionContainer = modal.querySelector('.lexicon-modal-description'); + if (plant.description_full) { + const sections = plant.description_full.split(/\n\s*\n/); + let formattedHtml = ''; + sections.forEach(section => { + const parts = section.split(/:(.*)/s); + if (parts.length > 1) { + const title = parts[0].trim(); + const content = parts[1].trim(); + if (title && content) { + formattedHtml += `

${title}

${content.replace(/\n/g, '
')}

`; + } + } else if (section.trim()) { + formattedHtml += `

${section.trim().replace(/\n/g, '
')}

`; + } + }); + descriptionContainer.innerHTML = formattedHtml; + } else { + descriptionContainer.innerHTML = '

Keine Beschreibung verfügbar.

'; + } + + modal.classList.add('show'); + document.body.classList.add('modal-open'); + } catch (error) { + console.error('Fehler beim Laden der Pflanzendetails:', error); + // Hier könnte man dem User eine UI-Nachricht zeigen + } +} + +export function closeLexiconDetailModal() { + const modal = getModalElement(); + if (modal) { + modal.classList.remove('show'); + if (!document.querySelector('.modal.show')) { + document.body.classList.remove('modal-open'); + } + } +} + + +// --- Funktionen nur für die lexikon.php Seite --- + +async function loadLexiconOverview() { + try { + allPlants = await api.getLexiconEntries(); + renderLexiconOverview(allPlants); + } catch (error) { + console.error('Fehler beim Laden des Lexikons:', error); + if (lexiconGrid) { + lexiconGrid.innerHTML = '

Das Lexikon konnte nicht geladen werden.

'; + } + } +} + +function renderLexiconOverview(plants) { + if (!lexiconGrid) return; + lexiconGrid.innerHTML = ''; + if (plants.length === 0) { + lexiconGrid.innerHTML = '

Noch keine Einträge im Lexikon vorhanden.

'; + return; + } + plants.forEach(plant => { + const card = document.createElement('div'); + card.className = 'lexicon-card'; + card.dataset.latinName = plant.latin_name; + + const placeholderImg = 'https://placehold.co/600x600/eeeeee/3D403D?text=' + encodeURIComponent(plant.name); + + card.innerHTML = ` +
+ ${plant.name} +
+
+

${plant.name}

+

${plant.latin_name}

+
+ `; + lexiconGrid.appendChild(card); + }); +} + +function handleSearch() { + const searchTerm = lexiconSearch.value.toLowerCase(); + const filteredPlants = allPlants.filter(plant => + plant.name.toLowerCase().includes(searchTerm) || + plant.latin_name.toLowerCase().includes(searchTerm) + ); + renderLexiconOverview(filteredPlants); +} + +// --- Initialisierung --- +function init() { + // Diese DOM-Elemente gibt es NUR auf der lexikon.php + lexiconGrid = document.getElementById('lexicon-grid'); + lexiconSearch = document.getElementById('lexiconSearch'); + + // Wenn wir auf der Lexikon-Seite sind, initialisiere die Übersicht + if (lexiconGrid && lexiconSearch) { + loadLexiconOverview(); + lexiconSearch.addEventListener('input', handleSearch); + lexiconGrid.addEventListener('click', (e) => { + const card = e.target.closest('.lexicon-card'); + if (card) { + openLexiconDetailModal(card.dataset.latinName); + } + }); + } + + // Das Modal selbst kann auf beiden Seiten existieren + const modal = getModalElement(); + if (modal) { + const closeButton = modal.querySelector('.close-btn'); + if (closeButton) { + closeButton.addEventListener('click', closeLexiconDetailModal); + } + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeLexiconDetailModal(); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', init); + diff --git a/plants/lexikon.php b/plants/lexikon.php new file mode 100644 index 0000000..884600b --- /dev/null +++ b/plants/lexikon.php @@ -0,0 +1,115 @@ +prepare("SELECT theme FROM users WHERE id = ?"); + $stmt->execute([$_SESSION['user_id']]); + $result = $stmt->fetch(); + if ($result && !empty($result['theme'])) { + $user_theme = $result['theme']; + } + } catch (PDOException $e) { + // Fehler ignorieren + } +} else { + // Wenn nicht eingeloggt, zur Hauptseite weiterleiten + header('Location: index.php'); + exit; +} + +?> + + + + + + Pflanzen-Lexikon + + + + + + + + +
+ +
+ +
+
+

Pflanzen-Lexikon

+

Durchsuche die von der Community geteilten Pflanzen und lerne mehr über sie.

+
+ +
+ +
+ +
+ +
+
+ + + + + + + + diff --git a/plants/main.js b/plants/main.js new file mode 100644 index 0000000..dcbac50 --- /dev/null +++ b/plants/main.js @@ -0,0 +1,381 @@ +// Version: 01.10.2025 15:30 (Fix DB Autofill Logic) +import * as api from './api.js'; +import * as ui from './ui.js'; +import * as lexicon from './lexikon.js'; + +// === Globaler Zustand === +const AppState = { + plants: [], + settings: {}, + incorrectGuesses: [], + currentView: 'grid', + currentSort: 'date_desc', + fullAiAnalysisResult: null, + pendingEnrichmentPromise: null +}; + +// === Haupt-Initialisierung === +async function loadInitialData() { + try { + const [plants, settings] = await Promise.all([ + api.getPlants(), + api.getUserSettings() + ]); + AppState.plants = plants; + AppState.settings = settings; + renderDashboard(); + } catch (error) { + console.error('Fehler beim Laden der Anfangsdaten:', error); + ui.showToast('App-Daten konnten nicht geladen werden.', true); + } +} + +function renderDashboard() { + const sortedPlants = sortPlants(AppState.plants, AppState.currentSort); + const dashboard = document.getElementById('plant-dashboard'); + if (dashboard) { + const useCompactView = window.matchMedia("(max-width: 768px)").matches && AppState.plants.length >= 3; + dashboard.classList.toggle('compact-view-enabled', useCompactView); + ui.renderPlantDashboard(sortedPlants, dashboard); + } +} + +function sortPlants(plants, sortValue) { + const sorted = [...plants]; + switch (sortValue) { + case 'name_asc': sorted.sort((a, b) => a.name.localeCompare(b.name)); break; + case 'watering_due': + sorted.sort((a, b) => { + const dateA = a.next_watering_due ? new Date(a.next_watering_due) : new Date('9999-12-31'); + const dateB = b.next_watering_due ? new Date(b.next_watering_due) : new Date('9999-12-31'); + return dateA - dateB; + }); + break; + default: // 'date_desc' + sorted.sort((a, b) => { + if (String(a.id).startsWith('temp-')) return -1; + if (String(b.id).startsWith('temp-')) return 1; + return b.id - a.id; + }); + break; + } + return sorted; +} + +// === Event Handler === + +function fileToBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result.split(',')[1]); + reader.onerror = error => reject(error); + }); +} + +// --- Dashboard & Globale Aktionen --- +async function handleDashboardClick(e) { + const actionElement = e.target.closest('[data-action]'); + if (!actionElement) return; + + if (actionElement.dataset.action === 'show-lexicon') { + e.preventDefault(); + e.stopPropagation(); + const latinName = actionElement.dataset.latinName; + if (latinName) lexicon.openLexiconDetailModal(latinName); + return; + } + + const card = e.target.closest('[data-id]'); + if (!card) return; + + const plantId = parseInt(card.dataset.id, 10); + const plant = AppState.plants.find(p => p.id === plantId); + if (!plant) return; + + switch (actionElement.dataset.action) { + case 'show-detail': ui.openPlantDetailOverlay(plant); break; + case 'flip': case 'flip-back': + e.target.closest('.plant-card-inner')?.classList.toggle('is-flipped'); + break; + case 'show-care-tips': e.target.closest('.plant-list-item')?.classList.toggle('tips-visible'); break; + case 'toggle-menu': + e.stopPropagation(); + const menuContainer = actionElement.closest('.menu-container'); + menuContainer.parentElement.closest('[data-id]')?.classList.add('menu-open'); + menuContainer.querySelector('.menu-dropdown')?.classList.toggle('show'); + break; + case 'edit': + ui.closePlantDetailOverlay(); + setTimeout(() => ui.openPlantModal(plant, getModalHandlers()), 50); + break; + case 'delete': + if (confirm(`Möchtest du "${plant.name}" wirklich löschen?`)) { + try { + await api.deletePlant(plantId); + ui.closePlantDetailOverlay(); + ui.showToast(`"${plant.name}" wurde gelöscht.`); + await loadInitialData(); + } catch (error) { ui.showToast('Löschen fehlgeschlagen.', true); } + } + break; + case 'water': case 'fertilize': + e.stopPropagation(); + actionElement.disabled = true; + actionElement.classList.add('is-loading'); + try { + const { plant: updatedPlant } = await api.recordCareAction(plantId, actionElement.dataset.action); + const plantIndex = AppState.plants.findIndex(p => p.id === plantId); + if (plantIndex !== -1) AppState.plants[plantIndex] = updatedPlant; + renderDashboard(); + if (document.getElementById('plantDetailOverlay').classList.contains('show')) ui.openPlantDetailOverlay(updatedPlant); + ui.showToast(actionElement.dataset.action === 'water' ? 'Gegossen!' : 'Gedüngt!'); + } catch (error) { ui.showToast(`Aktion fehlgeschlagen.`, true); + } finally { + actionElement.disabled = false; + actionElement.classList.remove('is-loading'); + } + break; + case 'open-diary': + ui.closePlantDetailOverlay(); + setTimeout(() => ui.openDiaryModal(plant), e.target.closest('.plant-detail-overlay') ? 300 : 0); + break; + } +} + +async function handleSaveSettings(settingsToUpdate) { + try { + await api.updateUserSettings(settingsToUpdate); + AppState.settings = {...AppState.settings, ...settingsToUpdate}; + ui.showToast('Einstellungen gespeichert!'); + } catch (error) { + ui.showToast('Einstellungen konnten nicht gespeichert werden.', true); + } +} + +// --- Modal-spezifische Handler --- +function getModalHandlers() { + return { + onPhotoSelect: handlePhotoSelection, + onAnalyze: handleAiAnalysis, + onLatinNameBlur: handleLatinNameBlur, + onSubmit: handlePlantFormSubmit + }; +} + +async function handlePhotoSelection(e) { + const plantPhotoInput = e.target; + if (plantPhotoInput.files && plantPhotoInput.files[0]) { + const reader = new FileReader(); + reader.onload = function() { + ui.showImagePreview(reader.result); + ui.showAiSection(); + ui.clearAiResults(document.getElementById('plantForm')); + AppState.incorrectGuesses = []; + } + reader.readAsDataURL(plantPhotoInput.files[0]); + } +} + +async function handleAiAnalysis() { + const plantPhotoInput = document.getElementById('plantPhoto'); + const analyzeBtn = document.getElementById('analyzeBtn'); + const plantForm = document.getElementById('plantForm'); + const file = plantPhotoInput.files[0]; + if (!file) return; + + analyzeBtn.disabled = true; + AppState.pendingEnrichmentPromise = null; + let cleanupAnimation = () => {}; + + try { + cleanupAnimation = ui.runAiAnimation(document.getElementById('imagePreviewContainer')); + const base64Image = await fileToBase64(file); + const fullAnalysis = await api.performInitialAnalysis(base64Image, AppState.incorrectGuesses); + AppState.fullAiAnalysisResult = fullAnalysis; + cleanupAnimation(); + + const identification = fullAnalysis.identification; + if (!identification || identification.name.toLowerCase() === 'unbekannt' || identification.confidence_score < 75) { + ui.showToast(identification?.identification_notes || 'Pflanze nicht sicher erkannt.', true); + } else { + ui.populateFormWithIdentification(plantForm, identification); + plantForm.querySelector('#aiFullResponse').value = JSON.stringify(fullAnalysis); + AppState.incorrectGuesses.push(identification.latin_name.trim().toLowerCase()); + document.getElementById('retryAiBtn').style.display = 'block'; + + const settings = AppState.settings; + const needsEnrichmentForUI = settings.auto_fill_watering || settings.auto_fill_fertilizing || settings.auto_fill_care_tips; + + // Definiere den Promise, der die Daten holt + const enrichmentPromise = api.getLexiconEntryDetails(identification.latin_name) + .catch(() => api.aiEnrich(identification.latin_name)); + + // Weise den Promise dem globalen Zustand zu, damit die Speicherfunktion darauf warten kann + AppState.pendingEnrichmentPromise = enrichmentPromise; + + // Führe die Logik aus, sobald der Promise erfüllt ist + enrichmentPromise.then(enrichmentData => { + if (enrichmentData) { + // Befülle das versteckte Feld IMMER + plantForm.querySelector('#aiEnrichmentResponse').value = JSON.stringify(enrichmentData); + + // Befülle die sichtbaren Felder NUR, wenn der Nutzer es wünscht + if (settings.auto_fill_watering && plantForm.querySelector('#wateringInterval').value === '') { + plantForm.querySelector('#wateringInterval').value = enrichmentData.watering_interval_days || ''; + } + if (settings.auto_fill_fertilizing && plantForm.querySelector('#fertilizingInterval').value === '') { + plantForm.querySelector('#fertilizingInterval').value = enrichmentData.fertilizing_interval_days || ''; + } + if (settings.auto_fill_care_tips && plantForm.querySelector('#careTips').value === '') { + plantForm.querySelector('#careTips').value = enrichmentData.care_tips || ''; + } + } + }).catch(err => console.error("Fehler bei der Datenanreicherung:", err)); + + // Zeige den Lade-Spinner NUR, wenn der Nutzer die Daten auch sehen will + if (needsEnrichmentForUI) { + ui.showEnrichmentLoader(true); + enrichmentPromise.finally(() => ui.showEnrichmentLoader(false)); + } + } + } catch (error) { + ui.showToast(error.message, true); + cleanupAnimation(); + } finally { + analyzeBtn.disabled = false; + } +} + +async function handleLatinNameBlur(e) { + const latinName = e.target.value.trim(); + if (latinName) { + try { + const lexiconEntry = await api.getLexiconEntryDetails(latinName); + // Hier nutzen wir dieselbe Logik wie oben, um die Felder basierend auf den Settings zu befüllen + const form = document.getElementById('plantForm'); + const settings = AppState.settings; + if (settings.auto_fill_watering && form.querySelector('#wateringInterval').value === '') { + form.querySelector('#wateringInterval').value = lexiconEntry.watering_interval_days || ''; + } + if (settings.auto_fill_fertilizing && form.querySelector('#fertilizingInterval').value === '') { + form.querySelector('#fertilizingInterval').value = lexiconEntry.fertilizing_interval_days || ''; + } + if (settings.auto_fill_care_tips && form.querySelector('#careTips').value === '') { + form.querySelector('#careTips').value = lexiconEntry.care_tips || ''; + } + form.querySelector('#aiEnrichmentResponse').value = JSON.stringify(lexiconEntry); + ui.showToast('Daten aus Lexikon geladen!', false); + } catch (error) { /* Kein Eintrag gefunden, still bleiben */ } + } +} + +function handlePlantFormSubmit(e) { + e.preventDefault(); + const form = e.target; + const formData = new FormData(form); + + const tempId = `temp-${Date.now()}`; + const tempPlant = { + id: tempId, + name: formData.get('name'), + photo: document.getElementById('imagePreview').src, + latin_name: formData.get('latin_name'), + watering_interval: null, + fertilizing_interval: null, + next_watering_due: null, + next_fertilizing_due: null, + care_tips: 'Wird geladen...' + }; + + AppState.plants.unshift(tempPlant); + renderDashboard(); + + ui.closePlantModal(); + ui.showToast('Pflanze wird gespeichert...'); + + (async () => { + try { + if (AppState.pendingEnrichmentPromise) { + await AppState.pendingEnrichmentPromise; + } + + const { plant: finalPlant } = await api.savePlant(new FormData(form)); + + const plantIndex = AppState.plants.findIndex(p => p.id === tempId); + if (plantIndex !== -1) { + AppState.plants[plantIndex] = finalPlant; + } else { + AppState.plants.shift(); + AppState.plants.unshift(finalPlant); + } + + renderDashboard(); + ui.showToast('Pflanze erfolgreich gespeichert!'); + + } catch (error) { + ui.showToast(`Speichern fehlgeschlagen: ${error.message}`, true); + AppState.plants = AppState.plants.filter(p => p.id !== tempId); + renderDashboard(); + } finally { + AppState.pendingEnrichmentPromise = null; + } + })(); +} + + +// === Initialisierung der globalen Listener === +function init() { + document.getElementById('addPlantBtn')?.addEventListener('click', () => ui.openPlantModal(null, getModalHandlers())); + document.getElementById('settingsBtn')?.addEventListener('click', async () => { + try { + const settings = await api.getUserSettings(); + AppState.settings = settings; + ui.openSettingsModal(settings, handleSaveSettings); + } catch(e){ ui.showToast("Einstellungen konnten nicht geladen werden.", true); } + }); + + document.getElementById('plant-dashboard')?.addEventListener('click', handleDashboardClick); + document.getElementById('sortSelect')?.addEventListener('change', (e) => { + AppState.currentSort = e.target.value; + renderDashboard(); + }); + + document.getElementById('viewGridBtn')?.addEventListener('click', () => { + AppState.currentView = 'grid'; + document.getElementById('plant-dashboard')?.classList.remove('list-view'); + document.getElementById('viewGridBtn')?.classList.add('active'); + document.getElementById('viewListBtn')?.classList.remove('active'); + }); + + document.getElementById('viewListBtn')?.addEventListener('click', () => { + AppState.currentView = 'list'; + document.getElementById('plant-dashboard')?.classList.add('list-view'); + document.getElementById('viewListBtn')?.classList.add('active'); + document.getElementById('viewGridBtn')?.classList.remove('active'); + }); + + const plantDetailOverlay = document.getElementById('plantDetailOverlay'); + plantDetailOverlay?.addEventListener('click', (e) => { + if (e.target === plantDetailOverlay) { + ui.closePlantDetailOverlay(); + } else { + handleDashboardClick(e); + } + }); + document.getElementById('plantDetailCloseBtn')?.addEventListener('click', ui.closePlantDetailOverlay); + + window.addEventListener('click', (e) => { + if (!e.target.closest('.menu-container')) { + document.querySelectorAll('.menu-dropdown.show').forEach(menu => { + menu.classList.remove('show'); + menu.closest('[data-id]')?.classList.remove('menu-open'); + }); + } + }); + + loadInitialData(); +} + +document.addEventListener('DOMContentLoaded', init); + diff --git a/plants/style.css b/plants/style.css new file mode 100644 index 0000000..ff86a29 --- /dev/null +++ b/plants/style.css @@ -0,0 +1,1314 @@ +/* Version: 01.10.2025 08:00 */ + +/* =============================================== */ +/* === GLOBALS, VARIABLES & BASE STYLES === */ +/* =============================================== */ +*, *::before, *::after { + box-sizing: border-box; +} + +:root { + --primary-color: #5E8C61; + --secondary-color: #81A182; + --background-color: #FCFCFC; + --surface-color: #FFFFFF; + --text-color: #3D403D; + --text-light: #9E9E9E; + --border-color: #EEEEEE; + --shadow-color: rgba(61, 64, 61, 0.07); + --shadow-color-hover: rgba(61, 64, 61, 0.12); + --status-red: #E57373; + --status-yellow: #FFB74D; + --status-blue: #64B5F6; + --milestone-bg-color: #fffbeb; + --font-family: 'Alan Sans', sans-serif; + --font-mono: 'Roboto Mono', monospace; + --border-radius: 12px; + --border-radius-large: 16px; +} + +/* Manually toggled Dark Mode */ +body[data-theme='dark'] { + --primary-color: #6a9f6d; + --background-color: #121212; + --surface-color: #1E1E1E; + --text-color: #E0E0E0; + --text-light: #8A8A8A; + --border-color: #333333; + --shadow-color: rgba(0, 0, 0, 0.2); + --shadow-color-hover: rgba(0, 0, 0, 0.3); + --milestone-bg-color: #2c2a22; +} + +/* Auto Dark Mode via System Settings */ +@media (prefers-color-scheme: dark) { + body[data-theme='auto'] { + --primary-color: #6a9f6d; + --background-color: #121212; + --surface-color: #1E1E1E; + --text-color: #E0E0E0; + --text-light: #8A8A8A; + --border-color: #333333; + --shadow-color: rgba(0, 0, 0, 0.2); + --shadow-color-hover: rgba(0, 0, 0, 0.3); + --milestone-bg-color: #2c2a22; + } +} + + +body { + font-family: var(--font-family); + background-color: var(--background-color); + color: var(--text-color); + margin: 0; + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; +} + +body.modal-open { + overflow: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* --- Keyframe Animations --- */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes pulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + + +/* =============================================== */ +/* === AUTH PAGE (Login/Register) === */ +/* =============================================== */ +.auth-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; +} +.auth-box { + width: 100%; + max-width: 400px; + background: var(--surface-color); + padding: 40px; + border-radius: var(--border-radius-large); + box-shadow: 0 10px 30px var(--shadow-color-hover); +} +.auth-box .logo-title-container { + justify-content: center; + margin-bottom: 25px; +} +.auth-box h2 { + text-align: center; + margin-top: 0; + margin-bottom: 5px; +} +.auth-subtitle { + text-align: center; + color: var(--text-light); + margin-top: 0; + margin-bottom: 30px; +} +.auth-switch { + text-align: center; + margin-top: 20px; + font-size: 0.9rem; +} +.auth-switch a { + color: var(--primary-color); + font-weight: 600; + text-decoration: none; +} +.auth-switch a:hover { + text-decoration: underline; +} + + +/* =============================================== */ +/* === HEADER & NAVIGATION === */ +/* =============================================== */ +.main-header { + background: var(--surface-color); + border-bottom: 1px solid var(--border-color); + padding: 10px 0; + margin-bottom: 15px; +} + +.main-header .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo-title-container { + display: flex; + align-items: center; + gap: 12px; + color: var(--primary-color); +} + +.header-logo { + height: 36px; + width: auto; +} + +.main-header h1 { + margin: 0; + font-weight: 700; +} + +.header-controls { + display: flex; + align-items: center; + gap: 15px; +} + +.header-button-group { + display: flex; + align-items: center; +} +.header-button-group > .add-btn { + margin: 0; + border-radius: 0; + position: relative; +} +.header-button-group > .add-btn:not(:last-child)::after { + content: ''; + position: absolute; + right: 0; + top: 25%; + bottom: 25%; + width: 1px; + background-color: rgba(255, 255, 255, 0.2); +} +.header-button-group > .add-btn:first-child { + border-top-left-radius: var(--border-radius); + border-bottom-left-radius: var(--border-radius); +} +.header-button-group > .add-btn:last-child { + border-top-right-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); +} +.add-btn.icon-btn { + padding: 10px 12px; +} +.add-btn.icon-btn span { + display: none; +} +.add-btn span { + display: block; +} + + +/* =============================================== */ +/* === BUTTONS & TOASTS (Allgemein) === */ +/* =============================================== */ +.btn, .add-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 20px; + border: none; + border-radius: var(--border-radius); + background-color: var(--primary-color); + color: white; + font-family: var(--font-family); + font-size: 1rem; + line-height: 1.2; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} +.btn:hover, .add-btn:hover { + background-color: #537d56; + transform: translateY(-2px); + box-shadow: 0 4px 10px var(--shadow-color-hover); +} +.header-button-group > .add-btn:hover { + transform: translateY(0); +} +.btn:active, .add-btn:active { + transform: translateY(0) scale(0.97); + box-shadow: none; + transition-duration: 0.1s; +} +.btn.btn-secondary { + background-color: #e0e0e0; + color: #3D403D; +} +.btn.btn-secondary:hover { + background-color: #d0d0d0; +} +body[data-theme='dark'] .btn.btn-secondary { + background-color: #4a4a4a; + color: var(--text-color); +} +body[data-theme='dark'] .btn.btn-secondary:hover { + background-color: #5a5a5a; +} +.btn.btn-danger { + background-color: transparent; + color: var(--status-red); + border: 1px solid var(--status-red); +} +.btn.btn-danger:hover { + background-color: var(--status-red); + color: white; +} +.btn.btn-ai { + background-color: #8e44ad; + flex-grow: 1; + justify-content: center; + position: relative; + overflow: hidden; + min-height: 44.4px; +} +.btn.btn-ai:hover { + background-color: #7d3c98; +} +.btn:disabled, .btn:disabled:hover { + background-color: #cccccc; + color: #888; + cursor: not-allowed; + transform: none; + box-shadow: none; +} +.auth-btn { + width: 100%; + margin-top: 10px; +} +.btn-retry { + background: none; + border: none; + color: var(--primary-color); + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + padding: 0; + margin-top: 8px; + text-align: left; + display: block; + width: 100%; +} +.btn-retry:hover { + text-decoration: underline; +} + +.btn.is-loading { + color: transparent !important; + pointer-events: none; + position: relative; +} +.btn.is-loading::after { + content: ''; + display: block; + position: absolute; + left: 50%; + top: 50%; + width: 20px; + height: 20px; + margin-left: -10px; + margin-top: -10px; + border: 2px solid rgba(255, 255, 255, 0.5); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + + +/* --- Toast Notification --- */ +.toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%) translateY(100px); + background-color: var(--primary-color); + color: white; + padding: 12px 25px; + border-radius: var(--border-radius); + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + z-index: 2000; + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + font-weight: 600; +} +.toast.show { + transform: translateX(-50%) translateY(0); + opacity: 1; +} +.toast.error { + background-color: var(--status-red); +} + + +/* =============================================== */ +/* === DASHBOARD LAYOUT & CONTROLS === */ +/* =============================================== */ +.dashboard-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + gap: 15px; + flex-wrap: wrap; +} +.sort-container { + position: relative; +} +.sort-container select { + color:var(--text-light); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: 8px 30px 8px 12px; + border-radius: 8px; + border: 1px solid var(--border-color); + background-color: var(--surface-color); + font-family: var(--font-family); + font-size: 0.9rem; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%239E9E9E' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 16px; +} +.view-toggle { + display: flex; + background-color: #f3f4f6; + border-radius: 8px; + padding: 4px; +} +body[data-theme='dark'] .view-toggle { + background-color: #121212; +} +.view-btn { + padding: 6px 10px; + border: none; + background-color: transparent; + border-radius: 6px; + cursor: pointer; + color: var(--text-light); + transition: background-color 0.2s, color 0.2s; +} +.view-btn.active { + background-color: var(--surface-color); + color: var(--primary-color); + box-shadow: 0 1px 3px var(--shadow-color); +} +.plant-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 30px; + transition: grid-template-columns 0.3s ease; +} +.plant-grid.list-view { + display: block; + grid-template-columns: 1fr; + gap: 15px; +} +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 60px 20px; + background-color: var(--surface-color); + border-radius: var(--border-radius-large); + border: 2px dashed var(--border-color); +} +.empty-state h2 { + color: var(--primary-color); + margin-top: 0; +} +.empty-state p { + color: var(--text-light); + max-width: 400px; + margin: 10px auto 0; +} + +/* =============================================== */ +/* === LEXIKON PAGE === */ +/* =============================================== */ +.lexicon-intro { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background-color: var(--surface-color); + border-radius: var(--border-radius-large); +} +.lexicon-intro h2 { + margin-top: 0; +} +.search-bar-container { + margin-bottom: 30px; +} +#lexiconSearch { + width: 100%; + padding: 15px 20px; + font-size: 1.1rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + background-color: var(--surface-color); + color: var(--text-color); +} +#lexiconSearch:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(94, 140, 97, 0.2); +} +.lexicon-card { + background-color: var(--surface-color); + border-radius: var(--border-radius-large); + overflow: hidden; + border: 1px solid var(--border-color); + box-shadow: 0 4px 10px var(--shadow-color); + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + opacity: 0; + transform: translateY(20px); + animation: fadeIn 0.5s ease-out forwards; +} +.lexicon-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px var(--shadow-color-hover); +} +.lexicon-card-img-container { + width: 100%; + aspect-ratio: 1 / 1; + overflow: hidden; +} +.lexicon-card-img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} +.lexicon-card:hover .lexicon-card-img { + transform: scale(1.05); +} +.lexicon-card-info { + padding: 15px 20px; +} +.lexicon-card-info h3 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} +.lexicon-card-info p { + margin: 0; + font-style: italic; + color: var(--text-light); + font-size: 0.9rem; +} + + +/* =============================================== */ +/* === MODAL WINDOWS (General) === */ +/* =============================================== */ +.modal { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + z-index: 1200; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + transition: opacity 0.3s ease, visibility 0.3s ease; + opacity: 0; + visibility: hidden; +} +.modal.show { + opacity: 1; + visibility: visible; +} +.modal-content { + background-color: var(--surface-color); + margin: auto; + border: none; + width: 90%; + max-width: 500px; + border-radius: var(--border-radius-large); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + transition: transform 0.3s ease; + transform: scale(0.95); + display: flex; + flex-direction: column; + max-height: 90vh; +} +.modal.show .modal-content { + transform: scale(1); +} +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 25px; + border-bottom: 1px solid var(--border-color); + background-color: #fcfcfc; + flex-shrink: 0; +} +body[data-theme='dark'] .modal-header, +body[data-theme='dark'] .modal-footer { + background-color: #1a1a1a; +} +.modal-header h2 { + margin: 0; + font-size: 1.4rem; +} +.close-btn { + background: none; + border: none; + font-size: 2rem; + cursor: pointer; + color: var(--text-light); +} +.modal-body { + padding: 25px; + overflow-y: auto; +} +.modal-footer { + padding: 15px 25px; + border-top: 1px solid var(--border-color); + background-color: #fcfcfc; + flex-shrink: 0; +} + +/* --- Specific Modals --- */ +.settings-modal-content { + max-width: 420px; +} +.settings-modal-content .modal-body { + padding: 0; + background-color: var(--background-color); +} +.settings-profile-banner { + background: var(--surface-color); + padding: 25px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + border-bottom: 1px solid var(--border-color); +} +.settings-modal-content .modal-body > .settings-card { + margin: 15px; +} +.settings-card { + background: var(--surface-color); + border-radius: var(--border-radius-large); + padding: 20px; + border: 1px solid var(--border-color); +} +.settings-card:last-child { + margin-bottom: 0; +} +.settings-card h3 { + margin-top: 0; + margin-bottom: 18px; + font-size: 1.1rem; + color: var(--primary-color); +} +.profile-pic-container { + position: relative; + flex-shrink: 0; +} +.profile-pic { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 3px solid var(--surface-color); + box-shadow: 0 2px 6px rgba(0,0,0,0.1); +} +.btn-change-pic { + position: absolute; + bottom: 2px; + right: 2px; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--primary-color); + color: white; + border: 2px solid white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} +.profile-info { + display: flex; + align-items: center; + gap: 8px; +} +.profile-username { + font-size: 1.5rem; + font-weight: 600; +} +.btn-edit-username { + background: none; + border: none; + color: var(--text-light); + cursor: pointer; + padding: 5px; +} +.settings-row { + display: flex; + justify-content: space-between; + align-items: flex-start; /* Geändert von 'center' zu 'flex-start' für bessere Ausrichtung */ + margin-bottom: 15px; + gap: 15px; /* Fügt etwas Abstand hinzu */ +} + +.settings-row:last-child { + margin-bottom: 0; +} + +.settings-description { + font-size: 0.85rem; + color: var(--text-light); + margin: 4px 0 0 0; + line-height: 1.4; +} + +.settings-value { + font-weight: 600; + color: var(--text-color); +} +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; /* NEU: Verhindert, dass der Schalter gequetscht wird */ + +} +.toggle-switch input { display: none; } +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #767676; + transition: .4s; + border-radius: 24px; +} +.slider:before { + position: absolute; + content: ""; + height: 18px; width: 18px; + left: 3px; bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; +} +input:checked + .slider { background-color: var(--primary-color); } +input:checked + .slider:before { transform: translateX(20px); } +.theme-selector { + display: flex; + background-color: var(--background-color); + border-radius: 8px; + padding: 4px; + border: 1px solid var(--border-color); +} +.theme-btn { + padding: 4px 12px; + border: none; + background-color: transparent; + border-radius: 6px; + cursor: pointer; + color: var(--text-light); + transition: background-color 0.2s, color 0.2s; + font-family: var(--font-family); + font-size: 0.85rem; + font-weight: 600; +} +.theme-btn.active { + background-color: var(--surface-color); + color: var(--primary-color); + box-shadow: 0 1px 3px var(--shadow-color); +} +.full-width { + width: 100%; + margin-bottom: 10px; +} +.full-width:last-child { margin-bottom: 0; } +.danger-zone { + border-color: var(--status-red); +} +.danger-zone h3 { + color: var(--status-red); +} +.diary-modal-content { + max-width: 450px; + height: 85vh; + max-height: 750px; +} +.diary-modal-content .modal-body { + flex-grow: 1; + padding: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +#diaryContentWrapper { + overflow-y: auto; + padding: 0 25px 25px; +} +#diaryCameraView { + padding: 25px; + display: flex; + flex-direction: column; + justify-content: center; +} +.diary-timeline { + position: relative; + padding-left: 30px; + border-left: 2px solid var(--border-color); + margin-left: 15px; + padding-top: 10px; +} +.diary-date-header { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-light); + background-color: var(--background-color); + padding: 2px 8px; + border-radius: 6px; + display: inline-block; + position: relative; + left: -48px; + margin-top: 25px; + margin-bottom: 20px; +} +.diary-date-header:first-of-type { + margin-top: 0; +} +.diary-entry { + position: relative; + margin-bottom: 20px; +} +.diary-icon { + position: absolute; + left: -48px; + top: 0; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: var(--surface-color); + border: 2px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + transition: all 0.2s ease; +} +.diary-entry-content { + background-color: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 12px 15px; + transition: all 0.2s ease; + position: relative; +} +.diary-entry.is-milestone .diary-entry-content { + background-color: var(--milestone-bg-color); + border-color: var(--status-yellow); +} +.diary-entry.is-milestone .diary-icon { + background-color: var(--status-yellow); + border-color: var(--status-yellow); + color: white; +} +.diary-entry-title { + font-weight: 600; + margin: 0 0 5px 0; +} +.diary-entry-notes { + font-size: 0.9rem; + color: var(--text-light); + margin: 0; +} +.diary-entry-time { + display: block; + text-align: right; + font-size: 0.75rem; + color: #aaa; + margin-top: 8px; +} +.diary-photo-container { + position: relative; + margin-top: 10px; + border-radius: var(--border-radius); + overflow: hidden; + aspect-ratio: 1 / 1; +} +.diary-photo { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.delete-diary-entry-btn { + position: absolute; + top: 8px; + right: 8px; + width: 30px; + height: 30px; + opacity: 0; + transition: opacity 0.2s; + color: var(--status-red); +} +.diary-entry-content:hover .delete-diary-entry-btn { + opacity: 0.8; +} +.diary-entry-content:hover .delete-diary-entry-btn:hover { + opacity: 1; +} +.set-profile-btn { + position: absolute; + top: 8px; + right: 42px; + opacity: 0; + transition: opacity 0.2s; +} +.diary-entry-content:hover .set-profile-btn { + opacity: 0.8; +} +.diary-entry-content:hover .set-profile-btn:hover { + opacity: 1; +} +.empty-diary { + text-align: center; + padding: 40px 0; + color: var(--text-light); +} +.diary-form .form-group { + margin-bottom: 10px; +} +.diary-input { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 8px; + resize: vertical; + min-height: 40px; +} +.diary-form-options { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; + margin-bottom: 15px; +} +.diary-form-attachments { + display: flex; + gap: 10px; + align-items: center; +} +.diary-form .upload-area.small .btn { + padding: 8px 12px; + font-size: 0.9rem; +} +.diary-form-milestone { + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-weight: 600; + color: var(--text-color); +} +.diary-form-milestone input { + margin: 0; + cursor: pointer; + width: 16px; + height: 16px; +} +.diary-form button[type="submit"] { + width: 100%; +} + +/* =============================================== */ +/* === LEXIKON DETAIL MODAL === */ +/* =============================================== */ +.lexicon-detail-modal-content { + max-width: 700px; + width: 95%; +} +.lexicon-detail-modal-content .modal-body { + padding: 0; +} + +.lexicon-modal-layout { + display: flex; + flex-direction: column; +} +.lexicon-modal-image-gallery { + display: flex; + flex-direction: column; +} +.lexicon-modal-image-container { + width: 100%; + aspect-ratio: 4 / 3; + max-height: 40vh; + background-color: var(--background-color); + flex-shrink: 0; +} +.lexicon-modal-image-container img { + width: 100%; + height: 100%; + object-fit: cover; +} +.lexicon-modal-thumbnails { + display: flex; + gap: 10px; + padding: 10px; + overflow-x: auto; + background-color: var(--background-color); + border-top: 1px solid var(--border-color); +} +.thumbnail-img { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: var(--border-radius); + cursor: pointer; + border: 2px solid transparent; + transition: border-color 0.2s ease, opacity 0.2s ease; + opacity: 0.7; + flex-shrink: 0; +} +.thumbnail-img:hover { + opacity: 1; +} +.thumbnail-img.active { + border-color: var(--primary-color); + opacity: 1; +} +body[data-theme='dark'] .lexicon-modal-thumbnails { + background-color: #2a2a2a; +} +@media (prefers-color-scheme: dark) { + body[data-theme='auto'] .lexicon-modal-thumbnails { + background-color: #2a2a2a; + } +} +.lexicon-modal-info { + padding: 25px 30px 30px; +} +.lexicon-modal-latin { + display: block; + font-size: 1rem; + color: var(--text-light); + margin-top: -10px; + margin-bottom: 20px; +} +.lexicon-modal-info h2 { + margin-top: 0; + margin-bottom: 10px; + font-size: 1.8rem; +} +.lexicon-modal-meta { + display: flex; + gap: 25px; + background-color: var(--background-color); + padding: 15px 20px; + border-radius: var(--border-radius); + margin-bottom: 25px; + border: 1px solid var(--border-color); +} +.lexicon-modal-meta div { + font-size: 0.95rem; +} +.lexicon-modal-meta strong { + display: block; + color: var(--text-light); + font-weight: 600; + margin-bottom: 3px; + font-size: 0.8rem; + text-transform: uppercase; +} +body[data-theme='dark'] .lexicon-modal-meta { + background-color: #2a2a2a; +} +@media (prefers-color-scheme: dark) { + body[data-theme='auto'] .lexicon-modal-meta { + background-color: #2a2a2a; + } +} + + +.lexicon-modal-description p { + margin-top: 0; + margin-bottom: 1em; + font-size: 1rem; + line-height: 1.7; +} +.lexicon-modal-description h3 { + font-weight: 600; + font-size: 1.2rem; + display: block; + margin-top: 1.8em; + margin-bottom: 0.6em; + color: var(--primary-color); + padding-bottom: 5px; + border-bottom: 1px solid var(--border-color); +} +.lexicon-modal-description p:first-of-type + h3 { + margin-top: 0; +} + + +/* =============================================== */ +/* === FORMS (General) & CAMERA VIEW === */ +/* =============================================== */ +.form-group { + margin-bottom: 20px; +} +#ai-analysis-section { + display: flex; + gap: 10px; + align-items: center; +} +#ai-analysis-section .btn-ai { + width: 100%; +} +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 8px; +} +.form-group input, .form-group textarea { + width: 100%; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + font-family: var(--font-family); + box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: var(--surface-color); + color: var(--text-color); +} +.form-group input:focus, .form-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(94, 140, 97, 0.2); +} +.form-group-upload { + margin-bottom: 20px; +} +.upload-area { display: flex; align-items: center; justify-content: center; gap: 10px; } +.image-preview-wrapper { + position: relative; + border-radius: var(--border-radius); + overflow: hidden; + border: 1px solid var(--border-color); + width: 100%; + max-width: 350px; + margin: 0 auto; + aspect-ratio: 1 / 1; + background-color: var(--background-color); +} +.image-preview-wrapper.small { + width: 100px; + margin-right: auto; + margin-left: 0; +} +.image-preview-wrapper img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.preview-actions { + position: absolute; + bottom: 10px; + right: 10px; + display: flex; + gap: 8px; +} +.preview-actions label.card-btn { + padding: 0; +} +.remove-photo-btn { + position: absolute; + top: 5px; + right: 5px; + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background-color: rgba(0, 0, 0, 0.6); + color: white; + font-size: 1.2rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.video-container { + position: relative; + width: 100%; + border-radius: var(--border-radius); + overflow: hidden; + background: #000; + aspect-ratio: 1 / 1; +} +.video-container video { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.viewfinder-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} +.viewfinder-corner { + position: absolute; + width: 30px; + height: 30px; + border: 4px solid rgba(255, 255, 255, 0.8); +} +.viewfinder-corner.top-left { + top: 25px; left: 25px; + border-right: none; border-bottom: none; + border-top-left-radius: 8px; +} +.viewfinder-corner.top-right { + top: 25px; right: 25px; + border-left: none; border-bottom: none; + border-top-right-radius: 8px; +} +.viewfinder-corner.bottom-left { + bottom: 25px; left: 25px; + border-right: none; border-top: none; + border-bottom-left-radius: 8px; +} +.viewfinder-corner.bottom-right { + bottom: 25px; right: 25px; + border-left: none; border-top: none; + border-bottom-right-radius: 8px; +} +.viewfinder-focus-point { + position: absolute; + top: 50%; left: 50%; + width: 24px; height: 24px; + border: 4px solid rgba(255, 255, 255, 0.9); + border-radius: 50%; + transform: translate(-50%, -50%); +} +.camera-controls { + margin-top: 15px; + display: flex; + justify-content: center; +} + +.enrichment-loader { + width: 16px; + height: 16px; + border: 2px solid var(--primary-color); + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: spin 1s linear infinite; +} + +/* =============================================== */ +/* === AI SCANNING ANIMATION (v2) === */ +/* =============================================== */ +@keyframes pulse-color { + 0% { + filter: invert(0); + } + 50% { + filter: invert(1); + } + 100% { + filter: invert(0); + } +} + +@keyframes ai-loader-anim { + 0%,10% {border-radius:0} + 30%,40% {border-radius:50% 0} + 60%,70% {border-radius:50%} + 90%,100% {border-radius:0 50%} +} + +.ai-animation-overlay { + position: absolute; + inset: 0; + background-color: rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 2; +} +.image-preview-wrapper.scanning .ai-animation-overlay { + opacity: 1; +} +.image-preview-wrapper.scanning #imagePreview { + animation: pulse-color 6s infinite ease-in-out; +} + +.ai-loader { + width: 90px; + aspect-ratio: 1; + display: grid; +} +.ai-loader:before, +.ai-loader:after { + content: ""; + grid-area: 1/1; + width: 42px; + aspect-ratio: 1; + box-shadow: 0 0 0 3px #fff inset; + filter: drop-shadow(48px 48px 0 #fff); + animation: ai-loader-anim 2s infinite alternate; +} +.ai-loader:after { + margin: 0 0 0 auto; + filter: drop-shadow(-48px 48px 0 #fff); + animation-delay: -1s; +} + +.ai-analysis-text { + position: absolute; + top: 20px; + left: 10px; + right: 10px; + font-weight:300; + color: white; + text-shadow: 0 0 2px rgba(0,0,0,0.7); + text-align: center; +} + diff --git a/plants/test.html b/plants/test.html new file mode 100644 index 0000000..747e521 --- /dev/null +++ b/plants/test.html @@ -0,0 +1,82 @@ + + + + + + Benachrichtigungs-Test + + + +
+

Test für Browser-Benachrichtigungen

+

Klicke auf den Knopf, um eine Test-Benachrichtigung auszulösen.

+ +
+ + + + diff --git a/plants/test_prompt.php b/plants/test_prompt.php new file mode 100644 index 0000000..3d386a5 --- /dev/null +++ b/plants/test_prompt.php @@ -0,0 +1,228 @@ + + + + + + + Prompt-Vergleich + + + + +

Prompt-Vergleich

+

Lade ein Bild hoch und vergleiche die Ergebnisse mit dem alten und dem neuen Prompt.

+ +
+ + +
+ + + +
+
+

Alter Prompt

+
+
+
+

Finaler Profi-Prompt (V6)

+
+
+
+ + + + + diff --git a/plants/ui.js b/plants/ui.js new file mode 100644 index 0000000..087a963 --- /dev/null +++ b/plants/ui.js @@ -0,0 +1,553 @@ +// Version: 01.10.2025 11:30 (FINAL LEXICON FIX) +import * as api from './api.js'; + +// === Zustandsvariablen für Event Listener === +let activeModalListeners = new Map(); +let settingsDebounceTimer = null; +let pendingSettings = {}; + +// === Hilfsfunktionen für Event Listener === +function addManagedListener(element, event, handler) { + if (element) { + element.addEventListener(event, handler); + if (!activeModalListeners.has(element)) { + activeModalListeners.set(element, []); + } + activeModalListeners.get(element).push({ event, handler }); + } +} + +function removeAllManagedListeners() { + activeModalListeners.forEach((listeners, element) => { + listeners.forEach(({ event, handler }) => { + element.removeEventListener(event, handler); + }); + }); + activeModalListeners.clear(); +} + +// === Allgemeine UI-Funktionen === +export function showToast(message, isError = false) { + const toast = document.createElement('div'); + toast.className = `toast ${isError ? 'error' : ''}`; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(() => toast.classList.add('show'), 10); + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => document.body.removeChild(toast), 300); + }, 3000); +} + +// === Pflanzen-Dashboard & Karten === +export function renderPlantDashboard(plants, dashboardElement) { + dashboardElement.innerHTML = ''; + const dashboardControls = document.querySelector('.dashboard-controls'); + + if (plants.length === 0) { + dashboardElement.innerHTML = `

Noch keine Pflanzen hier!

Klicke oben auf "hinzufügen", um deine grüne Oase zu starten.

`; + if (dashboardControls) dashboardControls.style.display = 'none'; + return; + } + + if (dashboardControls) dashboardControls.style.display = 'flex'; + + plants.forEach(plant => { + dashboardElement.appendChild(createPlantCardElement(plant)); + dashboardElement.appendChild(createPlantListItemElement(plant)); + dashboardElement.appendChild(createPlantCompactCardElement(plant)); + }); +} + +function formatLastDate(dateString) { + if (!dateString || dateString === '0000-00-00') return ''; + const lastDate = new Date(dateString); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + lastDate.setHours(0, 0, 0, 0); + if (lastDate.getTime() === today.getTime()) return 'heute'; + if (lastDate.getTime() === yesterday.getTime()) return 'gestern'; + return `am ${lastDate.toLocaleString('de-DE', { day: '2-digit', month: '2-digit' })}`; +} + +function createPlantCardHTML(plant) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const msPerDay = 1000 * 60 * 60 * 24; + + const getProgress = (diff, interval) => { + if (diff <= 0) return { percent: 100, status: 'red' }; + if (diff >= interval) return { percent: 0, status: 'green' }; + return { percent: 100 - ((diff / interval) * 100), status: (diff / interval) < 0.25 ? 'yellow' : 'green' }; + }; + + const getStatusText = (days, compact = false) => { + if (days < 0) return `fällig!`; + if (days === 0) return `heute`; + if (days === 1) return `morgen`; + return `in ${days} ${compact ? 'T.' : 'Tagen'}`; + }; + + let waterButtonHTML, compactWaterPillHTML; + if (plant.watering_interval > 0 && plant.next_watering_due) { + const nextWatering = new Date(plant.next_watering_due + 'T00:00:00'); + const diff = Math.round((nextWatering - today) / msPerDay); + const progress = getProgress(diff, plant.watering_interval); + waterButtonHTML = ``; + compactWaterPillHTML = ``; + } else { + waterButtonHTML = ``; + compactWaterPillHTML = ``; + } + + let fertilizeButtonHTML, compactFertilizePillHTML; + if (plant.fertilizing_interval > 0 && plant.next_fertilizing_due) { + const nextFertilizing = new Date(plant.next_fertilizing_due + 'T00:00:00'); + const diff = Math.round((nextFertilizing - today) / msPerDay); + const progress = getProgress(diff, plant.fertilizing_interval); + fertilizeButtonHTML = ``; + compactFertilizePillHTML = ``; + } else { + fertilizeButtonHTML = ``; + compactFertilizePillHTML = ``; + } + + const placeholderImg = 'https://placehold.co/600x600/eeeeee/3D403D?text=' + encodeURIComponent(plant.name); + let tipsContentHTML = plant.latin_name ? `
Botanisch: ${plant.latin_name}
Mehr erfahren →
` : ''; + if (plant.care_tips) { + tipsContentHTML += plant.care_tips.split(/\n\s*\n/).map(tip => `

${tip.replace(/(\p{Emoji}[^:]+:)/gu, '$1')}

`).join(''); + } + if (!plant.care_tips && !plant.latin_name) { + tipsContentHTML = '

Keine Pflegetipps hinterlegt.

'; + } + + return { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML, compactWaterPillHTML, compactFertilizePillHTML }; +} + +function createPlantCardElement(plant) { + const cardWrapper = document.createElement('div'); + cardWrapper.className = 'plant-card'; + cardWrapper.dataset.id = plant.id; + const { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML } = createPlantCardHTML(plant); + cardWrapper.innerHTML = `
${plant.name}

${plant.name}

${waterButtonHTML}${fertilizeButtonHTML}
${(plant.care_tips || plant.latin_name) ? `` : ''}

${plant.name}

${tipsContentHTML}
Zum Umdrehen klicken
`; + return cardWrapper; +} + +function createPlantListItemElement(plant) { + const listItem = document.createElement('div'); + listItem.className = 'plant-list-item'; + listItem.dataset.id = plant.id; + const { waterButtonHTML, fertilizeButtonHTML, placeholderImg, tipsContentHTML } = createPlantCardHTML(plant); + listItem.innerHTML = `
${plant.name}

${plant.name}

${waterButtonHTML}${fertilizeButtonHTML}
${(plant.care_tips || plant.latin_name) ? `` : ''}
${tipsContentHTML}
`; + return listItem; +} + +function createPlantCompactCardElement(plant) { + const cardWrapper = document.createElement('div'); + cardWrapper.className = 'plant-compact-card'; + cardWrapper.dataset.id = plant.id; + const { placeholderImg, compactWaterPillHTML, compactFertilizePillHTML } = createPlantCardHTML(plant); + cardWrapper.innerHTML = `
${plant.name}
${plant.name}
${compactWaterPillHTML}${compactFertilizePillHTML}
`; + return cardWrapper; +} + +// === Pflanzen-Modal === +export function openPlantModal(plant = null, handlers = {}) { + const plantModal = document.getElementById('plantModal'); + const plantForm = document.getElementById('plantForm'); + if (!plantModal || !plantForm) return; + + plantForm.reset(); + clearAiResults(plantForm); + + if (plant) { + plantModal.querySelector('#modalTitle').textContent = 'Pflanze bearbeiten'; + document.getElementById('ai-analysis-section').style.display = 'none'; + plantForm.querySelector('#plantId').value = plant.id; + plantForm.querySelector('#plantName').value = plant.name; + plantForm.querySelector('#latinName').value = plant.latin_name || ''; + plantForm.querySelector('#wateringInterval').value = plant.watering_interval > 0 ? plant.watering_interval : ''; + plantForm.querySelector('#fertilizingInterval').value = plant.fertilizing_interval > 0 ? plant.fertilizing_interval : ''; + plantForm.querySelector('#careTips').value = plant.care_tips || ''; + plantForm.querySelector('#currentPhoto').value = plant.photo || ''; + if (plant.photo) showImagePreview(plant.photo); + else { + document.getElementById('initialUploadArea').style.display = 'flex'; + document.getElementById('imagePreviewContainer').style.display = 'none'; + } + } else { + plantModal.querySelector('#modalTitle').textContent = 'Neue Pflanze hinzufügen'; + document.getElementById('ai-analysis-section').style.display = 'none'; + document.getElementById('initialUploadArea').style.display = 'flex'; + document.getElementById('imagePreviewContainer').style.display = 'none'; + } + + addManagedListener(plantForm, 'submit', handlers.onSubmit); + addManagedListener(document.getElementById('plantPhoto'), 'change', handlers.onPhotoSelect); + addManagedListener(document.getElementById('analyzeBtn'), 'click', handlers.onAnalyze); + addManagedListener(document.getElementById('retryAiBtn'), 'click', handlers.onAnalyze); + addManagedListener(document.getElementById('latinName'), 'blur', handlers.onLatinNameBlur); + addManagedListener(document.getElementById('startCameraBtn'), 'click', () => startCamera('main')); + addManagedListener(document.getElementById('retakePhotoBtn'), 'click', () => startCamera('main')); + addManagedListener(document.getElementById('captureBtn'), 'click', () => capturePhoto('main')); + addManagedListener(plantModal.querySelector('.close-btn'), 'click', closePlantModal); + addManagedListener(plantModal, 'click', (e) => { if (e.target === plantModal) closePlantModal(); }); + + plantModal.classList.add('show'); + document.body.classList.add('modal-open'); +} + +export function closePlantModal() { + const plantModal = document.getElementById('plantModal'); + if (plantModal) { + stopCamera('main'); + removeAllManagedListeners(); + plantModal.classList.remove('show'); + if (!document.querySelector('.modal.show')) { + document.body.classList.remove('modal-open'); + } + } +} + +// === Detail Overlay (Mobile) === +export function openPlantDetailOverlay(plant) { + const plantDetailOverlay = document.getElementById('plantDetailOverlay'); + const plantDetailContent = document.getElementById('plantDetailContent'); + if (plantDetailOverlay && plantDetailContent) { + plantDetailContent.innerHTML = ''; + plantDetailContent.appendChild(createPlantCardElement(plant)); + plantDetailOverlay.classList.add('show'); + document.body.classList.add('modal-open'); + } +} + +export function closePlantDetailOverlay() { + const plantDetailOverlay = document.getElementById('plantDetailOverlay'); + if (plantDetailOverlay) { + plantDetailOverlay.classList.remove('show'); + if (!document.querySelector('.modal.show')) { + document.body.classList.remove('modal-open'); + } + } +} + +// === KI UI-Funktionen === +export function showAiSection() { + document.getElementById('ai-analysis-section').style.display = 'flex'; +} + +export function clearAiResults(form) { + if (!form) return; + form.querySelectorAll('input[name^="ai_"]').forEach(el => el.value = ''); + document.getElementById('retryAiBtn').style.display = 'none'; + showEnrichmentLoader(false); +} + +export function runAiAnimation(container) { + if(!container) return () => {}; + const analysisTextElement = container.querySelector('.ai-analysis-text'); + const analysisMessages = ["Initialisiere Scan", "Analysiere Blattform", "Extrahiere Merkmale", "Vergleiche Datenbank", "Bestimme Gattung", "Finalisiere..."]; + let messageIndex = 0; + + container.classList.add('scanning'); + if(analysisTextElement) analysisTextElement.textContent = analysisMessages[messageIndex]; + + const textInterval = setInterval(() => { + messageIndex = (messageIndex + 1) % analysisMessages.length; + if(analysisTextElement) analysisTextElement.textContent = analysisMessages[messageIndex]; + }, 1500); + + return function cleanup() { + clearInterval(textInterval); + container.classList.remove('scanning'); + }; +} + +function ensureEnrichmentLoaders() { + ['wateringInterval', 'fertilizingInterval', 'careTips'].forEach(fieldId => { + const label = document.querySelector(`label[for="${fieldId}"]`); + if (label && !label.querySelector('.enrichment-loader')) { + const loader = document.createElement('span'); + loader.className = 'enrichment-loader'; + loader.style.display = 'none'; + label.appendChild(loader); + } + }); +} + +export function showEnrichmentLoader(isLoading) { + ensureEnrichmentLoaders(); + document.querySelectorAll('.enrichment-loader').forEach(loader => { + loader.style.display = isLoading ? 'inline-block' : 'none'; + }); +} + +export function populateFormWithIdentification(form, idData) { + form.querySelector('#plantName').value = idData.name || ''; + form.querySelector('#latinName').value = idData.latin_name || ''; +} + +export function populateFormWithEnrichment(form, enrichmentData, settings) { + if (settings.auto_fill_watering && form.querySelector('#wateringInterval').value === '') { + form.querySelector('#wateringInterval').value = enrichmentData.watering_interval_days || ''; + } + if (settings.auto_fill_fertilizing && form.querySelector('#fertilizingInterval').value === '') { + form.querySelector('#fertilizingInterval').value = enrichmentData.fertilizing_interval_days || ''; + } + if (settings.auto_fill_care_tips && form.querySelector('#careTips').value === '') { + form.querySelector('#careTips').value = enrichmentData.care_tips || ''; + } + + const enrichmentField = form.querySelector('input[name="ai_enrichment_json"]'); + if (enrichmentField) { + enrichmentField.value = JSON.stringify(enrichmentData); + } +} + +// === Tagebuch-Modal === +export function openDiaryModal(plant) { + const diaryModal = document.getElementById('diaryModal'); + if(!diaryModal) return; + + diaryModal.querySelector('#diaryPlantName').textContent = plant.name; + diaryModal.querySelector('#diaryPlantId').value = plant.id; + document.getElementById('diaryForm')?.reset(); + removeDiaryPhoto(); + + const diaryContent = document.getElementById('diaryContent'); + if(diaryContent) diaryContent.innerHTML = '

Lade Einträge...

'; + + api.getDiaryEntries(plant.id) + .then(entries => renderDiaryEntries(entries, plant.id)) + .catch(() => { + if(diaryContent) diaryContent.innerHTML = '

Einträge konnten nicht geladen werden.

'; + showToast('Tagebuch konnte nicht geladen werden.', true); + }); + + addManagedListener(document.getElementById('diaryForm'), 'submit', handleDiaryFormSubmit); + addManagedListener(diaryModal.querySelector('.close-btn'), 'click', closeDiaryModal); + addManagedListener(diaryModal, 'click', (e) => { if (e.target === diaryModal) closeDiaryModal(); }); + + diaryModal.classList.add('show'); + document.body.classList.add('modal-open'); +} + +async function handleDiaryFormSubmit(e) { + e.preventDefault(); + const diaryForm = document.getElementById('diaryForm'); + const saveBtn = diaryForm.querySelector('button[type="submit"]'); + saveBtn.disabled = true; + try { + const result = await api.addDiaryEntry(new FormData(diaryForm)); + if(result.success) { + showToast('Eintrag gespeichert!'); + const plantId = result.entry.plant_id; + const entries = await api.getDiaryEntries(plantId); + renderDiaryEntries(entries, plantId); + diaryForm.reset(); + removeDiaryPhoto(); + } else { throw new Error(result.error); } + } catch (error) { showToast('Eintrag konnte nicht gespeichert werden.', true); + } finally { + saveBtn.disabled = false; + } +} + + +export function closeDiaryModal() { + const diaryModal = document.getElementById('diaryModal'); + if (diaryModal) { + stopCamera('diary'); + removeAllManagedListeners(); + diaryModal.classList.remove('show'); + if (!document.querySelector('.modal.show')) { + document.body.classList.remove('modal-open'); + } + } +} + +export function renderDiaryEntries(entries, plantId) { + const diaryContent = document.getElementById('diaryContent'); + if (!diaryContent) return; + diaryContent.innerHTML = ''; + if (entries.length === 0) { + diaryContent.innerHTML = '

Noch keine Einträge vorhanden.

'; + return; + } + const groupedEntries = entries.reduce((acc, entry) => { + const date = new Date(entry.entry_date).toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' }); + if (!acc[date]) acc[date] = []; + acc[date].push(entry); + return acc; + }, {}); + for (const date in groupedEntries) { + const dateHeader = document.createElement('h4'); + dateHeader.className = 'diary-date-header'; + dateHeader.textContent = date; + diaryContent.appendChild(dateHeader); + groupedEntries[date].forEach(entry => diaryContent.appendChild(createDiaryEntryElement(entry, plantId))); + } +} + +function createDiaryEntryElement(entry, plantId) { + const div = document.createElement('div'); + div.className = `diary-entry ${entry.entry_type === 'milestone' ? 'is-milestone' : ''}`; + div.dataset.plantId = plantId; + div.dataset.entryId = entry.id; + if(entry.photo) div.dataset.photo = entry.photo; + const icons = { manual: '📝', watered: '💧', fertilized: '🌱', milestone: '⭐', added: '🎉' }; + const titles = { manual: 'Notiz', watered: 'Gegossen', fertilized: 'Gedüngt', milestone: 'Meilenstein', added: 'Pflanze hinzugefügt' }; + let effectiveType = (entry.entry_type === 'milestone' && entry.notes === 'Pflanze hinzugefügt') ? 'added' : entry.entry_type; + let photoHTML = entry.photo ? `
` : ''; + div.innerHTML = `
${icons[effectiveType] || '📌'}

${titles[effectiveType] || 'Eintrag'}

${(entry.notes && effectiveType !== 'added') ? `

${entry.notes.replace(/\n/g, '
')}

` : ''}${photoHTML}${new Date(entry.entry_date).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
`; + return div; +} + +export function removeDiaryPhoto() { + const diaryPhotoInput = document.getElementById('diaryPhoto'); + if(diaryPhotoInput) diaryPhotoInput.value = ''; + document.getElementById('diaryImagePreviewContainer').style.display = 'none'; + document.getElementById('diaryInitialUploadArea').style.display = 'flex'; +} + +// === Settings Modal --- +function handleSettingsChange(e, onSave) { + const target = e.target; + let key, value; + if (target.matches('.toggle-switch input')) { + key = target.id.replace(/-/g, '_'); + value = target.checked; + } else if (target.matches('.theme-btn')) { + key = 'theme'; + value = target.dataset.theme; + document.body.dataset.theme = value; + document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.remove('active')); + target.classList.add('active'); + } else { + return; + } + pendingSettings[key] = value; + clearTimeout(settingsDebounceTimer); + settingsDebounceTimer = setTimeout(() => { + if (Object.keys(pendingSettings).length > 0) { + onSave(pendingSettings); + pendingSettings = {}; + } + }, 750); +} + +export function openSettingsModal(data, onSave) { + const modal = document.getElementById('settingsModal'); + if (modal) { + populateSettingsModal(data); + addManagedListener(modal, 'change', (e) => handleSettingsChange(e, onSave)); + addManagedListener(modal, 'click', (e) => handleSettingsChange(e, onSave)); + addManagedListener(modal.querySelector('.close-btn'), 'click', closeSettingsModal); + addManagedListener(modal, 'click', (e) => { if(e.target === modal) closeSettingsModal(); }); + modal.classList.add('show'); + document.body.classList.add('modal-open'); + } +} + +export function closeSettingsModal() { + const modal = document.getElementById('settingsModal'); + if (modal) { + removeAllManagedListeners(); + modal.classList.remove('show'); + if (!document.querySelector('.modal.show')) { + document.body.classList.remove('modal-open'); + } + } +} + +function populateSettingsModal(data) { + if (!data) return; + const safeSet = (id, property, value) => { + const el = document.getElementById(id); + if (el) el[property] = value; + }; + safeSet('settings-profile-pic', 'src', data.profile_image || `https://placehold.co/100x100/eeeeee/3D403D?text=${data.username.charAt(0)}`); + safeSet('settings-username', 'textContent', data.username); + safeSet('stats-plant-count', 'textContent', data.plant_count); + safeSet('stats-ai-credits', 'textContent', data.ai_credits); + safeSet('allow-photo-sharing', 'checked', data.allow_photo_sharing == 1); + safeSet('auto-fill-watering', 'checked', data.auto_fill_watering == 1); + safeSet('auto-fill-fertilizing', 'checked', data.auto_fill_fertilizing == 1); + safeSet('auto-fill-care-tips', 'checked', data.auto_fill_care_tips == 1); + document.querySelectorAll('.theme-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.theme === data.theme); + }); +} + +// === Kamera UI-Funktionen === +let cameraStream = null; + +export function showImagePreview(src) { + const imagePreview = document.getElementById('imagePreview'); + const imagePreviewContainer = document.getElementById('imagePreviewContainer'); + const initialUploadArea = document.getElementById('initialUploadArea'); + if (imagePreview) imagePreview.src = src; + if (imagePreviewContainer) imagePreviewContainer.style.display = 'block'; + if (initialUploadArea) initialUploadArea.style.display = 'none'; +} + +export async function startCamera(context = 'main') { + if (cameraStream) stopCamera(context); + const elements = context === 'main' + ? { view: document.getElementById('cameraView'), form: document.getElementById('formContent'), video: document.getElementById('videoElement') } + : { view: document.getElementById('diaryCameraView'), form: document.getElementById('diaryContentWrapper'), video: document.getElementById('diaryVideoElement'), footer: document.querySelector('#diaryModal .modal-footer') }; + if (!elements.view || !elements.form || !elements.video) return; + try { + cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1080, height: 1080 } }); + elements.video.srcObject = cameraStream; + elements.form.style.display = 'none'; + if(elements.footer) elements.footer.style.display = 'none'; + elements.view.style.display = 'block'; + } catch (err) { + showToast("Kamerazugriff verweigert.", true); + } +} + +export function stopCamera(context = 'main') { + if (cameraStream) { + cameraStream.getTracks().forEach(track => track.stop()); + cameraStream = null; + } + const elements = context === 'main' + ? { view: document.getElementById('cameraView'), form: document.getElementById('formContent') } + : { view: document.getElementById('diaryCameraView'), form: document.getElementById('diaryContentWrapper'), footer: document.querySelector('#diaryModal .modal-footer') }; + if (elements.view) elements.view.style.display = 'none'; + if (elements.form) elements.form.style.display = 'block'; + if (elements.footer) elements.footer.style.display = 'block'; +} + +export function capturePhoto(context = 'main') { + const elements = context === 'main' + ? { canvas: document.getElementById('photoCanvas'), video: document.getElementById('videoElement'), input: document.getElementById('plantPhoto'), showPreview: showImagePreview } + : { canvas: document.getElementById('diaryPhotoCanvas'), video: document.getElementById('diaryVideoElement'), input: document.getElementById('diaryPhoto'), showPreview: (files) => document.getElementById('plantPhoto').dispatchEvent(new CustomEvent('change', { detail: files })) }; + if (!elements.canvas || !elements.video || !elements.input) return; + const context2d = elements.canvas.getContext('2d'); + const size = Math.min(elements.video.videoWidth, elements.video.videoHeight); + elements.canvas.width = size; + elements.canvas.height = size; + context2d.drawImage(elements.video, (elements.video.videoWidth - size) / 2, (elements.video.videoHeight - size) / 2, size, size, 0, 0, size, size); + const dataUrl = elements.canvas.toDataURL('image/webp'); + if (context === 'main') { + elements.showPreview(dataUrl); + showAiSection(); + clearAiResults(document.getElementById('plantForm')); + } + elements.canvas.toBlob(blob => { + const imageFile = new File([blob], `aufnahme.webp`, { type: 'image/webp' }); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(imageFile); + elements.input.files = dataTransfer.files; + // Manually trigger change event + const changeEvent = new Event('change', { bubbles: true }); + elements.input.dispatchEvent(changeEvent); + stopCamera(context); + }, 'image/webp'); +} + diff --git a/plants/vendor/autoload.php b/plants/vendor/autoload.php new file mode 100644 index 0000000..20646e4 --- /dev/null +++ b/plants/vendor/autoload.php @@ -0,0 +1,22 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/plants/vendor/composer/InstalledVersions.php b/plants/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..2052022 --- /dev/null +++ b/plants/vendor/composer/InstalledVersions.php @@ -0,0 +1,396 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/plants/vendor/composer/LICENSE b/plants/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/plants/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plants/vendor/composer/autoload_classmap.php b/plants/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..5490b88 --- /dev/null +++ b/plants/vendor/composer/autoload_classmap.php @@ -0,0 +1,15 @@ + $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', +); diff --git a/plants/vendor/composer/autoload_files.php b/plants/vendor/composer/autoload_files.php new file mode 100644 index 0000000..7ff36b4 --- /dev/null +++ b/plants/vendor/composer/autoload_files.php @@ -0,0 +1,12 @@ + $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', +); diff --git a/plants/vendor/composer/autoload_namespaces.php b/plants/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/plants/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/symfony/polyfill-php80'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), + 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), + 'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'), + 'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'), +); diff --git a/plants/vendor/composer/autoload_real.php b/plants/vendor/composer/autoload_real.php new file mode 100644 index 0000000..124d9ee --- /dev/null +++ b/plants/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/plants/vendor/composer/autoload_static.php b/plants/vendor/composer/autoload_static.php new file mode 100644 index 0000000..6668bae --- /dev/null +++ b/plants/vendor/composer/autoload_static.php @@ -0,0 +1,81 @@ + __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Symfony\\Polyfill\\Php80\\' => 23, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Polyfill\\Ctype\\' => 23, + ), + 'P' => + array ( + 'PhpOption\\' => 10, + ), + 'G' => + array ( + 'GrahamCampbell\\ResultType\\' => 26, + ), + 'D' => + array ( + 'Dotenv\\' => 7, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Polyfill\\Ctype\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', + ), + 'PhpOption\\' => + array ( + 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', + ), + 'GrahamCampbell\\ResultType\\' => + array ( + 0 => __DIR__ . '/..' . '/graham-campbell/result-type/src', + ), + 'Dotenv\\' => + array ( + 0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src', + ), + ); + + public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plants/vendor/composer/installed.json b/plants/vendor/composer/installed.json new file mode 100644 index 0000000..ac873d6 --- /dev/null +++ b/plants/vendor/composer/installed.json @@ -0,0 +1,497 @@ +{ + "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "version_normalized": "1.1.3.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "time": "2024-07-20T21:45:45+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "install-path": "../graham-campbell/result-type" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "version_normalized": "1.9.4.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "time": "2025-08-21T11:53:16+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "install-path": "../phpoption/phpoption" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "version_normalized": "1.33.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-ctype" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "version_normalized": "1.33.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2024-12-23T08:48:59+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-mbstring" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "version_normalized": "1.33.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2025-01-02T08:10:11+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "version_normalized": "5.6.2.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "time": "2025-04-30T23:37:27+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "install-path": "../vlucas/phpdotenv" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/plants/vendor/composer/installed.php b/plants/vendor/composer/installed.php new file mode 100644 index 0000000..4cb3e4f --- /dev/null +++ b/plants/vendor/composer/installed.php @@ -0,0 +1,77 @@ + array( + 'name' => '__root__', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'graham-campbell/result-type' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'reference' => '3ba905c11371512af9d9bdd27d99b782216b6945', + 'type' => 'library', + 'install_path' => __DIR__ . '/../graham-campbell/result-type', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phpoption/phpoption' => array( + 'pretty_version' => '1.9.4', + 'version' => '1.9.4.0', + 'reference' => '638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phpoption/phpoption', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-ctype' => array( + 'pretty_version' => 'v1.33.0', + 'version' => '1.33.0.0', + 'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-ctype', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-mbstring' => array( + 'pretty_version' => 'v1.33.0', + 'version' => '1.33.0.0', + 'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.33.0', + 'version' => '1.33.0.0', + 'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'vlucas/phpdotenv' => array( + 'pretty_version' => 'v5.6.2', + 'version' => '5.6.2.0', + 'reference' => '24ac4c74f91ee2c193fa1aaa5c249cb0822809af', + 'type' => 'library', + 'install_path' => __DIR__ . '/../vlucas/phpdotenv', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/plants/vendor/composer/platform_check.php b/plants/vendor/composer/platform_check.php new file mode 100644 index 0000000..2a9ec55 --- /dev/null +++ b/plants/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 70205)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +} diff --git a/plants/vendor/graham-campbell/result-type/LICENSE b/plants/vendor/graham-campbell/result-type/LICENSE new file mode 100644 index 0000000..8e7c898 --- /dev/null +++ b/plants/vendor/graham-campbell/result-type/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020-2024 Graham Campbell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plants/vendor/graham-campbell/result-type/composer.json b/plants/vendor/graham-campbell/result-type/composer.json new file mode 100644 index 0000000..32bfc81 --- /dev/null +++ b/plants/vendor/graham-campbell/result-type/composer.json @@ -0,0 +1,33 @@ +{ + "name": "graham-campbell/result-type", + "description": "An Implementation Of The Result Type", + "keywords": ["result", "result-type", "Result", "Result Type", "Result-Type", "Graham Campbell", "GrahamCampbell"], + "license": "MIT", + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "GrahamCampbell\\Tests\\ResultType\\": "tests/" + } + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/plants/vendor/graham-campbell/result-type/src/Error.php b/plants/vendor/graham-campbell/result-type/src/Error.php new file mode 100644 index 0000000..2c37c3e --- /dev/null +++ b/plants/vendor/graham-campbell/result-type/src/Error.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +use PhpOption\None; +use PhpOption\Some; + +/** + * @template T + * @template E + * + * @extends \GrahamCampbell\ResultType\Result + */ +final class Error extends Result +{ + /** + * @var E + */ + private $value; + + /** + * Internal constructor for an error value. + * + * @param E $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template F + * + * @param F $value + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return None::create(); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function map(callable $f) + { + return self::create($this->value); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function flatMap(callable $f) + { + /** @var \GrahamCampbell\ResultType\Result */ + return self::create($this->value); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return Some::create($this->value); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function mapError(callable $f) + { + return self::create($f($this->value)); + } +} diff --git a/plants/vendor/graham-campbell/result-type/src/Result.php b/plants/vendor/graham-campbell/result-type/src/Result.php new file mode 100644 index 0000000..8c67bcd --- /dev/null +++ b/plants/vendor/graham-campbell/result-type/src/Result.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +/** + * @template T + * @template E + */ +abstract class Result +{ + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + abstract public function success(); + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function map(callable $f); + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function flatMap(callable $f); + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + abstract public function error(); + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function mapError(callable $f); +} diff --git a/plants/vendor/graham-campbell/result-type/src/Success.php b/plants/vendor/graham-campbell/result-type/src/Success.php new file mode 100644 index 0000000..27cd85e --- /dev/null +++ b/plants/vendor/graham-campbell/result-type/src/Success.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +use PhpOption\None; +use PhpOption\Some; + +/** + * @template T + * @template E + * + * @extends \GrahamCampbell\ResultType\Result + */ +final class Success extends Result +{ + /** + * @var T + */ + private $value; + + /** + * Internal constructor for a success value. + * + * @param T $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template S + * + * @param S $value + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return Some::create($this->value); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function map(callable $f) + { + return self::create($f($this->value)); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function flatMap(callable $f) + { + return $f($this->value); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return None::create(); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function mapError(callable $f) + { + return self::create($this->value); + } +} diff --git a/plants/vendor/phpoption/phpoption/LICENSE b/plants/vendor/phpoption/phpoption/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/plants/vendor/phpoption/phpoption/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plants/vendor/phpoption/phpoption/composer.json b/plants/vendor/phpoption/phpoption/composer.json new file mode 100644 index 0000000..c077040 --- /dev/null +++ b/plants/vendor/phpoption/phpoption/composer.json @@ -0,0 +1,50 @@ +{ + "name": "phpoption/phpoption", + "description": "Option Type for PHP", + "keywords": ["php", "option", "language", "type"], + "license": "Apache-2.0", + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "autoload-dev": { + "psr-4": { + "PhpOption\\Tests\\": "tests/PhpOption/Tests/" + } + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + }, + "preferred-install": "dist" + }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + } +} diff --git a/plants/vendor/phpoption/phpoption/src/PhpOption/LazyOption.php b/plants/vendor/phpoption/phpoption/src/PhpOption/LazyOption.php new file mode 100644 index 0000000..9cb77c8 --- /dev/null +++ b/plants/vendor/phpoption/phpoption/src/PhpOption/LazyOption.php @@ -0,0 +1,175 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use Traversable; + +/** + * @template T + * + * @extends Option + */ +final class LazyOption extends Option +{ + /** @var callable(mixed...):(Option) */ + private $callback; + + /** @var array */ + private $arguments; + + /** @var Option|null */ + private $option; + + /** + * @template S + * @param callable(mixed...):(Option) $callback + * @param array $arguments + * + * @return LazyOption + */ + public static function create($callback, array $arguments = []): self + { + return new self($callback, $arguments); + } + + /** + * @param callable(mixed...):(Option) $callback + * @param array $arguments + */ + public function __construct($callback, array $arguments = []) + { + if (!is_callable($callback)) { + throw new \InvalidArgumentException('Invalid callback given'); + } + + $this->callback = $callback; + $this->arguments = $arguments; + } + + public function isDefined(): bool + { + return $this->option()->isDefined(); + } + + public function isEmpty(): bool + { + return $this->option()->isEmpty(); + } + + public function get() + { + return $this->option()->get(); + } + + public function getOrElse($default) + { + return $this->option()->getOrElse($default); + } + + public function getOrCall($callable) + { + return $this->option()->getOrCall($callable); + } + + public function getOrThrow(\Exception $ex) + { + return $this->option()->getOrThrow($ex); + } + + public function orElse(Option $else) + { + return $this->option()->orElse($else); + } + + public function ifDefined($callable) + { + $this->option()->forAll($callable); + } + + public function forAll($callable) + { + return $this->option()->forAll($callable); + } + + public function map($callable) + { + return $this->option()->map($callable); + } + + public function flatMap($callable) + { + return $this->option()->flatMap($callable); + } + + public function filter($callable) + { + return $this->option()->filter($callable); + } + + public function filterNot($callable) + { + return $this->option()->filterNot($callable); + } + + public function select($value) + { + return $this->option()->select($value); + } + + public function reject($value) + { + return $this->option()->reject($value); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return $this->option()->getIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $this->option()->foldLeft($initialValue, $callable); + } + + public function foldRight($initialValue, $callable) + { + return $this->option()->foldRight($initialValue, $callable); + } + + /** + * @return Option + */ + private function option(): Option + { + if (null === $this->option) { + /** @var mixed */ + $option = call_user_func_array($this->callback, $this->arguments); + if ($option instanceof Option) { + $this->option = $option; + } else { + throw new \RuntimeException(sprintf('Expected instance of %s', Option::class)); + } + } + + return $this->option; + } +} diff --git a/plants/vendor/phpoption/phpoption/src/PhpOption/None.php b/plants/vendor/phpoption/phpoption/src/PhpOption/None.php new file mode 100644 index 0000000..4b85d22 --- /dev/null +++ b/plants/vendor/phpoption/phpoption/src/PhpOption/None.php @@ -0,0 +1,136 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use EmptyIterator; + +/** + * @extends Option + */ +final class None extends Option +{ + /** @var None|null */ + private static $instance; + + /** + * @return None + */ + public static function create(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function get() + { + throw new \RuntimeException('None has no value.'); + } + + public function getOrCall($callable) + { + return $callable(); + } + + public function getOrElse($default) + { + return $default; + } + + public function getOrThrow(\Exception $ex) + { + throw $ex; + } + + public function isEmpty(): bool + { + return true; + } + + public function isDefined(): bool + { + return false; + } + + public function orElse(Option $else) + { + return $else; + } + + public function ifDefined($callable) + { + // Just do nothing in that case. + } + + public function forAll($callable) + { + return $this; + } + + public function map($callable) + { + return $this; + } + + public function flatMap($callable) + { + return $this; + } + + public function filter($callable) + { + return $this; + } + + public function filterNot($callable) + { + return $this; + } + + public function select($value) + { + return $this; + } + + public function reject($value) + { + return $this; + } + + public function getIterator(): EmptyIterator + { + return new EmptyIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $initialValue; + } + + public function foldRight($initialValue, $callable) + { + return $initialValue; + } + + private function __construct() + { + } +} diff --git a/plants/vendor/phpoption/phpoption/src/PhpOption/Option.php b/plants/vendor/phpoption/phpoption/src/PhpOption/Option.php new file mode 100644 index 0000000..91fab9c --- /dev/null +++ b/plants/vendor/phpoption/phpoption/src/PhpOption/Option.php @@ -0,0 +1,434 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use ArrayAccess; +use IteratorAggregate; + +/** + * @template T + * + * @implements IteratorAggregate + */ +abstract class Option implements IteratorAggregate +{ + /** + * Creates an option given a return value. + * + * This is intended for consuming existing APIs and allows you to easily + * convert them to an option. By default, we treat ``null`` as the None + * case, and everything else as Some. + * + * @template S + * + * @param S $value The actual return value. + * @param S $noneValue The value which should be considered "None"; null by + * default. + * + * @return Option + */ + public static function fromValue($value, $noneValue = null) + { + if ($value === $noneValue) { + return None::create(); + } + + return new Some($value); + } + + /** + * Creates an option from an array's value. + * + * If the key does not exist in the array, the array is not actually an + * array, or the array's value at the given key is null, None is returned. + * Otherwise, Some is returned wrapping the value at the given key. + * + * @template S + * + * @param array|ArrayAccess|null $array A potential array or \ArrayAccess value. + * @param string|int|null $key The key to check. + * + * @return Option + */ + public static function fromArraysValue($array, $key) + { + if ($key === null || !(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) { + return None::create(); + } + + return new Some($array[$key]); + } + + /** + * Creates a lazy-option with the given callback. + * + * This is also a helper constructor for lazy-consuming existing APIs where + * the return value is not yet an option. By default, we treat ``null`` as + * None case, and everything else as Some. + * + * @template S + * + * @param callable $callback The callback to evaluate. + * @param array $arguments The arguments for the callback. + * @param S $noneValue The value which should be considered "None"; + * null by default. + * + * @return LazyOption + */ + public static function fromReturn($callback, array $arguments = [], $noneValue = null) + { + return new LazyOption(static function () use ($callback, $arguments, $noneValue) { + /** @var mixed */ + $return = call_user_func_array($callback, $arguments); + + if ($return === $noneValue) { + return None::create(); + } + + return new Some($return); + }); + } + + /** + * Option factory, which creates new option based on passed value. + * + * If value is already an option, it simply returns. If value is callable, + * LazyOption with passed callback created and returned. If Option + * returned from callback, it returns directly. On other case value passed + * to Option::fromValue() method. + * + * @template S + * + * @param Option|callable|S $value + * @param S $noneValue Used when $value is mixed or + * callable, for None-check. + * + * @return Option|LazyOption + */ + public static function ensure($value, $noneValue = null) + { + if ($value instanceof self) { + return $value; + } elseif (is_callable($value)) { + return new LazyOption(static function () use ($value, $noneValue) { + /** @var mixed */ + $return = $value(); + + if ($return instanceof self) { + return $return; + } else { + return self::fromValue($return, $noneValue); + } + }); + } else { + return self::fromValue($value, $noneValue); + } + } + + /** + * Lift a function so that it accepts Option as parameters. + * + * We return a new closure that wraps the original callback. If any of the + * parameters passed to the lifted function is empty, the function will + * return a value of None. Otherwise, we will pass all parameters to the + * original callback and return the value inside a new Option, unless an + * Option is returned from the function, in which case, we use that. + * + * @template S + * + * @param callable $callback + * @param mixed $noneValue + * + * @return callable + */ + public static function lift($callback, $noneValue = null) + { + return static function () use ($callback, $noneValue) { + /** @var array */ + $args = func_get_args(); + + $reduced_args = array_reduce( + $args, + /** @param bool $status */ + static function ($status, self $o) { + return $o->isEmpty() ? true : $status; + }, + false + ); + // if at least one parameter is empty, return None + if ($reduced_args) { + return None::create(); + } + + $args = array_map( + /** @return T */ + static function (self $o) { + // it is safe to do so because the fold above checked + // that all arguments are of type Some + /** @var T */ + return $o->get(); + }, + $args + ); + + return self::ensure(call_user_func_array($callback, $args), $noneValue); + }; + } + + /** + * Returns the value if available, or throws an exception otherwise. + * + * @throws \RuntimeException If value is not available. + * + * @return T + */ + abstract public function get(); + + /** + * Returns the value if available, or the default value if not. + * + * @template S + * + * @param S $default + * + * @return T|S + */ + abstract public function getOrElse($default); + + /** + * Returns the value if available, or the results of the callable. + * + * This is preferable over ``getOrElse`` if the computation of the default + * value is expensive. + * + * @template S + * + * @param callable():S $callable + * + * @return T|S + */ + abstract public function getOrCall($callable); + + /** + * Returns the value if available, or throws the passed exception. + * + * @param \Exception $ex + * + * @return T + */ + abstract public function getOrThrow(\Exception $ex); + + /** + * Returns true if no value is available, false otherwise. + * + * @return bool + */ + abstract public function isEmpty(); + + /** + * Returns true if a value is available, false otherwise. + * + * @return bool + */ + abstract public function isDefined(); + + /** + * Returns this option if non-empty, or the passed option otherwise. + * + * This can be used to try multiple alternatives, and is especially useful + * with lazy evaluating options: + * + * ```php + * $repo->findSomething() + * ->orElse(new LazyOption(array($repo, 'findSomethingElse'))) + * ->orElse(new LazyOption(array($repo, 'createSomething'))); + * ``` + * + * @param Option $else + * + * @return Option + */ + abstract public function orElse(self $else); + + /** + * This is similar to map() below except that the return value has no meaning; + * the passed callable is simply executed if the option is non-empty, and + * ignored if the option is empty. + * + * In all cases, the return value of the callable is discarded. + * + * ```php + * $comment->getMaybeFile()->ifDefined(function($file) { + * // Do something with $file here. + * }); + * ``` + * + * If you're looking for something like ``ifEmpty``, you can use ``getOrCall`` + * and ``getOrElse`` in these cases. + * + * @deprecated Use forAll() instead. + * + * @param callable(T):mixed $callable + * + * @return void + */ + abstract public function ifDefined($callable); + + /** + * This is similar to map() except that the return value of the callable has no meaning. + * + * The passed callable is simply executed if the option is non-empty, and ignored if the + * option is empty. This method is preferred for callables with side-effects, while map() + * is intended for callables without side-effects. + * + * @param callable(T):mixed $callable + * + * @return Option + */ + abstract public function forAll($callable); + + /** + * Applies the callable to the value of the option if it is non-empty, + * and returns the return value of the callable wrapped in Some(). + * + * If the option is empty, then the callable is not applied. + * + * ```php + * (new Some("foo"))->map('strtoupper')->get(); // "FOO" + * ``` + * + * @template S + * + * @param callable(T):S $callable + * + * @return Option + */ + abstract public function map($callable); + + /** + * Applies the callable to the value of the option if it is non-empty, and + * returns the return value of the callable directly. + * + * In contrast to ``map``, the return value of the callable is expected to + * be an Option itself; it is not automatically wrapped in Some(). + * + * @template S + * + * @param callable(T):Option $callable must return an Option + * + * @return Option + */ + abstract public function flatMap($callable); + + /** + * If the option is empty, it is returned immediately without applying the callable. + * + * If the option is non-empty, the callable is applied, and if it returns true, + * the option itself is returned; otherwise, None is returned. + * + * @param callable(T):bool $callable + * + * @return Option + */ + abstract public function filter($callable); + + /** + * If the option is empty, it is returned immediately without applying the callable. + * + * If the option is non-empty, the callable is applied, and if it returns false, + * the option itself is returned; otherwise, None is returned. + * + * @param callable(T):bool $callable + * + * @return Option + */ + abstract public function filterNot($callable); + + /** + * If the option is empty, it is returned immediately. + * + * If the option is non-empty, and its value does not equal the passed value + * (via a shallow comparison ===), then None is returned. Otherwise, the + * Option is returned. + * + * In other words, this will filter all but the passed value. + * + * @param T $value + * + * @return Option + */ + abstract public function select($value); + + /** + * If the option is empty, it is returned immediately. + * + * If the option is non-empty, and its value does equal the passed value (via + * a shallow comparison ===), then None is returned; otherwise, the Option is + * returned. + * + * In other words, this will let all values through except the passed value. + * + * @param T $value + * + * @return Option + */ + abstract public function reject($value); + + /** + * Binary operator for the initial value and the option's value. + * + * If empty, the initial value is returned. If non-empty, the callable + * receives the initial value and the option's value as arguments. + * + * ```php + * + * $some = new Some(5); + * $none = None::create(); + * $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6) + * $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1) + * + * // This can be used instead of something like the following: + * $option = Option::fromValue($integerOrNull); + * $result = 1; + * if ( ! $option->isEmpty()) { + * $result += $option->get(); + * } + * ``` + * + * @template S + * + * @param S $initialValue + * @param callable(S, T):S $callable + * + * @return S + */ + abstract public function foldLeft($initialValue, $callable); + + /** + * foldLeft() but with reversed arguments for the callable. + * + * @template S + * + * @param S $initialValue + * @param callable(T, S):S $callable + * + * @return S + */ + abstract public function foldRight($initialValue, $callable); +} diff --git a/plants/vendor/phpoption/phpoption/src/PhpOption/Some.php b/plants/vendor/phpoption/phpoption/src/PhpOption/Some.php new file mode 100644 index 0000000..032632e --- /dev/null +++ b/plants/vendor/phpoption/phpoption/src/PhpOption/Some.php @@ -0,0 +1,169 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use ArrayIterator; + +/** + * @template T + * + * @extends Option + */ +final class Some extends Option +{ + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @template U + * + * @param U $value + * + * @return Some + */ + public static function create($value): self + { + return new self($value); + } + + public function isDefined(): bool + { + return true; + } + + public function isEmpty(): bool + { + return false; + } + + public function get() + { + return $this->value; + } + + public function getOrElse($default) + { + return $this->value; + } + + public function getOrCall($callable) + { + return $this->value; + } + + public function getOrThrow(\Exception $ex) + { + return $this->value; + } + + public function orElse(Option $else) + { + return $this; + } + + public function ifDefined($callable) + { + $this->forAll($callable); + } + + public function forAll($callable) + { + $callable($this->value); + + return $this; + } + + public function map($callable) + { + return new self($callable($this->value)); + } + + public function flatMap($callable) + { + /** @var mixed */ + $rs = $callable($this->value); + if (!$rs instanceof Option) { + throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); + } + + return $rs; + } + + public function filter($callable) + { + if (true === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function filterNot($callable) + { + if (false === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function select($value) + { + if ($this->value === $value) { + return $this; + } + + return None::create(); + } + + public function reject($value) + { + if ($this->value === $value) { + return None::create(); + } + + return $this; + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator([$this->value]); + } + + public function foldLeft($initialValue, $callable) + { + return $callable($initialValue, $this->value); + } + + public function foldRight($initialValue, $callable) + { + return $callable($this->value, $initialValue); + } +} diff --git a/plants/vendor/symfony/polyfill-ctype/Ctype.php b/plants/vendor/symfony/polyfill-ctype/Ctype.php new file mode 100644 index 0000000..ba75a2c --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/Ctype.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Ctype; + +/** + * Ctype implementation through regex. + * + * @internal + * + * @author Gert de Pagter + */ +final class Ctype +{ + /** + * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise. + * + * @see https://php.net/ctype-alnum + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_alnum($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text); + } + + /** + * Returns TRUE if every character in text is a letter, FALSE otherwise. + * + * @see https://php.net/ctype-alpha + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_alpha($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text); + } + + /** + * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise. + * + * @see https://php.net/ctype-cntrl + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_cntrl($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text); + } + + /** + * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise. + * + * @see https://php.net/ctype-digit + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_digit($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text); + } + + /** + * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise. + * + * @see https://php.net/ctype-graph + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_graph($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text); + } + + /** + * Returns TRUE if every character in text is a lowercase letter. + * + * @see https://php.net/ctype-lower + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_lower($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text); + } + + /** + * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all. + * + * @see https://php.net/ctype-print + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_print($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text); + } + + /** + * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise. + * + * @see https://php.net/ctype-punct + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_punct($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text); + } + + /** + * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters. + * + * @see https://php.net/ctype-space + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_space($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text); + } + + /** + * Returns TRUE if every character in text is an uppercase letter. + * + * @see https://php.net/ctype-upper + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_upper($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text); + } + + /** + * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise. + * + * @see https://php.net/ctype-xdigit + * + * @param mixed $text + * + * @return bool + */ + public static function ctype_xdigit($text) + { + $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); + + return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text); + } + + /** + * Converts integers to their char versions according to normal ctype behaviour, if needed. + * + * If an integer between -128 and 255 inclusive is provided, + * it is interpreted as the ASCII value of a single character + * (negative values have 256 added in order to allow characters in the Extended ASCII range). + * Any other integer is interpreted as a string containing the decimal digits of the integer. + * + * @param mixed $int + * @param string $function + * + * @return mixed + */ + private static function convert_int_to_char_for_ctype($int, $function) + { + if (!\is_int($int)) { + return $int; + } + + if ($int < -128 || $int > 255) { + return (string) $int; + } + + if (\PHP_VERSION_ID >= 80100) { + @trigger_error($function.'(): Argument of type int will be interpreted as string in the future', \E_USER_DEPRECATED); + } + + if ($int < 0) { + $int += 256; + } + + return \chr($int); + } +} diff --git a/plants/vendor/symfony/polyfill-ctype/LICENSE b/plants/vendor/symfony/polyfill-ctype/LICENSE new file mode 100644 index 0000000..7536cae --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plants/vendor/symfony/polyfill-ctype/README.md b/plants/vendor/symfony/polyfill-ctype/README.md new file mode 100644 index 0000000..b144d03 --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/README.md @@ -0,0 +1,12 @@ +Symfony Polyfill / Ctype +======================== + +This component provides `ctype_*` functions to users who run php versions without the ctype extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plants/vendor/symfony/polyfill-ctype/bootstrap.php b/plants/vendor/symfony/polyfill-ctype/bootstrap.php new file mode 100644 index 0000000..d54524b --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/bootstrap.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Ctype as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('ctype_alnum')) { + function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); } +} +if (!function_exists('ctype_alpha')) { + function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); } +} +if (!function_exists('ctype_cntrl')) { + function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); } +} +if (!function_exists('ctype_digit')) { + function ctype_digit($text) { return p\Ctype::ctype_digit($text); } +} +if (!function_exists('ctype_graph')) { + function ctype_graph($text) { return p\Ctype::ctype_graph($text); } +} +if (!function_exists('ctype_lower')) { + function ctype_lower($text) { return p\Ctype::ctype_lower($text); } +} +if (!function_exists('ctype_print')) { + function ctype_print($text) { return p\Ctype::ctype_print($text); } +} +if (!function_exists('ctype_punct')) { + function ctype_punct($text) { return p\Ctype::ctype_punct($text); } +} +if (!function_exists('ctype_space')) { + function ctype_space($text) { return p\Ctype::ctype_space($text); } +} +if (!function_exists('ctype_upper')) { + function ctype_upper($text) { return p\Ctype::ctype_upper($text); } +} +if (!function_exists('ctype_xdigit')) { + function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); } +} diff --git a/plants/vendor/symfony/polyfill-ctype/bootstrap80.php b/plants/vendor/symfony/polyfill-ctype/bootstrap80.php new file mode 100644 index 0000000..ab2f861 --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/bootstrap80.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Ctype as p; + +if (!function_exists('ctype_alnum')) { + function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); } +} +if (!function_exists('ctype_alpha')) { + function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); } +} +if (!function_exists('ctype_cntrl')) { + function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); } +} +if (!function_exists('ctype_digit')) { + function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); } +} +if (!function_exists('ctype_graph')) { + function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); } +} +if (!function_exists('ctype_lower')) { + function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); } +} +if (!function_exists('ctype_print')) { + function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); } +} +if (!function_exists('ctype_punct')) { + function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); } +} +if (!function_exists('ctype_space')) { + function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); } +} +if (!function_exists('ctype_upper')) { + function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); } +} +if (!function_exists('ctype_xdigit')) { + function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); } +} diff --git a/plants/vendor/symfony/polyfill-ctype/composer.json b/plants/vendor/symfony/polyfill-ctype/composer.json new file mode 100644 index 0000000..131ca7a --- /dev/null +++ b/plants/vendor/symfony/polyfill-ctype/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/polyfill-ctype", + "type": "library", + "description": "Symfony polyfill for ctype functions", + "keywords": ["polyfill", "compatibility", "portable", "ctype"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plants/vendor/symfony/polyfill-mbstring/LICENSE b/plants/vendor/symfony/polyfill-mbstring/LICENSE new file mode 100644 index 0000000..6e3afce --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plants/vendor/symfony/polyfill-mbstring/Mbstring.php b/plants/vendor/symfony/polyfill-mbstring/Mbstring.php new file mode 100644 index 0000000..31e36a3 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -0,0 +1,1045 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Mbstring; + +/** + * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. + * + * Implemented: + * - mb_chr - Returns a specific character from its Unicode code point + * - mb_convert_encoding - Convert character encoding + * - mb_convert_variables - Convert character code in variable(s) + * - mb_decode_mimeheader - Decode string in MIME header field + * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED + * - mb_decode_numericentity - Decode HTML numeric string reference to character + * - mb_encode_numericentity - Encode character to HTML numeric string reference + * - mb_convert_case - Perform case folding on a string + * - mb_detect_encoding - Detect character encoding + * - mb_get_info - Get internal settings of mbstring + * - mb_http_input - Detect HTTP input character encoding + * - mb_http_output - Set/Get HTTP output character encoding + * - mb_internal_encoding - Set/Get internal character encoding + * - mb_list_encodings - Returns an array of all supported encodings + * - mb_ord - Returns the Unicode code point of a character + * - mb_output_handler - Callback function converts character encoding in output buffer + * - mb_scrub - Replaces ill-formed byte sequences with substitute characters + * - mb_strlen - Get string length + * - mb_strpos - Find position of first occurrence of string in a string + * - mb_strrpos - Find position of last occurrence of a string in a string + * - mb_str_split - Convert a string to an array + * - mb_strtolower - Make a string lowercase + * - mb_strtoupper - Make a string uppercase + * - mb_substitute_character - Set/Get substitution character + * - mb_substr - Get part of string + * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive + * - mb_stristr - Finds first occurrence of a string within another, case insensitive + * - mb_strrchr - Finds the last occurrence of a character in a string within another + * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive + * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive + * - mb_strstr - Finds first occurrence of a string within another + * - mb_strwidth - Return width of string + * - mb_substr_count - Count the number of substring occurrences + * - mb_ucfirst - Make a string's first character uppercase + * - mb_lcfirst - Make a string's first character lowercase + * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string + * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string + * - mb_rtrim - Strip whitespace (or other characters) from the end of a string + * + * Not implemented: + * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) + * - mb_ereg_* - Regular expression with multibyte support + * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable + * - mb_preferred_mime_name - Get MIME charset string + * - mb_regex_encoding - Returns current encoding for multibyte regex as string + * - mb_regex_set_options - Set/Get the default options for mbregex functions + * - mb_send_mail - Send encoded mail + * - mb_split - Split multibyte string using regular expression + * - mb_strcut - Get part of string + * - mb_strimwidth - Get truncated string with specified width + * + * @author Nicolas Grekas + * + * @internal + */ +final class Mbstring +{ + public const MB_CASE_FOLD = \PHP_INT_MAX; + + private const SIMPLE_CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + private static $encodingList = ['ASCII', 'UTF-8']; + private static $language = 'neutral'; + private static $internalEncoding = 'UTF-8'; + + public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) + { + if (\is_array($s)) { + $r = []; + foreach ($s as $str) { + $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding); + } + + return $r; + } + + if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { + $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); + } else { + $fromEncoding = self::getEncoding($fromEncoding); + } + + $toEncoding = self::getEncoding($toEncoding); + + if ('BASE64' === $fromEncoding) { + $s = base64_decode($s); + $fromEncoding = $toEncoding; + } + + if ('BASE64' === $toEncoding) { + return base64_encode($s); + } + + if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { + if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { + $fromEncoding = 'Windows-1252'; + } + if ('UTF-8' !== $fromEncoding) { + $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); + } + + return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); + } + + if ('HTML-ENTITIES' === $fromEncoding) { + $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); + $fromEncoding = 'UTF-8'; + } + + return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); + } + + public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) + { + $ok = true; + array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { + if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { + $ok = false; + } + }); + + return $ok ? $fromEncoding : false; + } + + public static function mb_decode_mimeheader($s) + { + return iconv_mime_decode($s, 2, self::$internalEncoding); + } + + public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) + { + trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); + } + + public static function mb_decode_numericentity($s, $convmap, $encoding = null) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return ''; // Instead of null (cf. mb_encode_numericentity). + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $cnt = floor(\count($convmap) / 4) * 4; + + for ($i = 0; $i < $cnt; $i += 4) { + // collector_decode_htmlnumericentity ignores $convmap[$i + 3] + $convmap[$i] += $convmap[$i + 2]; + $convmap[$i + 1] += $convmap[$i + 2]; + } + + $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { + $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; + for ($i = 0; $i < $cnt; $i += 4) { + if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { + return self::mb_chr($c - $convmap[$i + 2]); + } + } + + return $m[0]; + }, $s); + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { + trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); + + return null; // Instead of '' (cf. mb_decode_numericentity). + } + + if (null !== $is_hex && !\is_scalar($is_hex)) { + trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); + + return null; + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $cnt = floor(\count($convmap) / 4) * 4; + $i = 0; + $len = \strlen($s); + $result = ''; + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + $c = self::mb_ord($uchr); + + for ($j = 0; $j < $cnt; $j += 4) { + if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { + $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; + $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; + continue 2; + } + } + $result .= $uchr; + } + + if (null === $encoding) { + return $result; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $result); + } + + public static function mb_convert_case($s, $mode, $encoding = null) + { + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + if (\MB_CASE_TITLE == $mode) { + static $titleRegexp = null; + if (null === $titleRegexp) { + $titleRegexp = self::getData('titleCaseRegexp'); + } + $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); + } else { + if (\MB_CASE_UPPER == $mode) { + static $upper = null; + if (null === $upper) { + $upper = self::getData('upperCase'); + } + $map = $upper; + } else { + if (self::MB_CASE_FOLD === $mode) { + static $caseFolding = null; + if (null === $caseFolding) { + $caseFolding = self::getData('caseFolding'); + } + $s = strtr($s, $caseFolding); + } + + static $lower = null; + if (null === $lower) { + $lower = self::getData('lowerCase'); + } + $map = $lower; + } + + static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if (isset($map[$uchr])) { + $uchr = $map[$uchr]; + $nlen = \strlen($uchr); + + if ($nlen == $ulen) { + $nlen = $i; + do { + $s[--$nlen] = $uchr[--$ulen]; + } while ($ulen); + } else { + $s = substr_replace($s, $uchr, $i - $ulen, $ulen); + $len += $nlen - $ulen; + $i += $nlen - $ulen; + } + } + } + } + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_internal_encoding($encoding = null) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + $normalizedEncoding = self::getEncoding($encoding); + + if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { + self::$internalEncoding = $normalizedEncoding; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + public static function mb_language($lang = null) + { + if (null === $lang) { + return self::$language; + } + + switch ($normalizedLang = strtolower($lang)) { + case 'uni': + case 'neutral': + self::$language = $normalizedLang; + + return true; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); + } + + public static function mb_list_encodings() + { + return ['UTF-8']; + } + + public static function mb_encoding_aliases($encoding) + { + switch (strtoupper($encoding)) { + case 'UTF8': + case 'UTF-8': + return ['utf8']; + } + + return false; + } + + public static function mb_check_encoding($var = null, $encoding = null) + { + if (null === $encoding) { + if (null === $var) { + return false; + } + $encoding = self::$internalEncoding; + } + + if (!\is_array($var)) { + return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); + } + + foreach ($var as $key => $value) { + if (!self::mb_check_encoding($key, $encoding)) { + return false; + } + if (!self::mb_check_encoding($value, $encoding)) { + return false; + } + } + + return true; + } + + public static function mb_detect_encoding($str, $encodingList = null, $strict = false) + { + if (null === $encodingList) { + $encodingList = self::$encodingList; + } else { + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + } + + foreach ($encodingList as $enc) { + switch ($enc) { + case 'ASCII': + if (!preg_match('/[\x80-\xFF]/', $str)) { + return $enc; + } + break; + + case 'UTF8': + case 'UTF-8': + if (preg_match('//u', $str)) { + return 'UTF-8'; + } + break; + + default: + if (0 === strncmp($enc, 'ISO-8859-', 9)) { + return $enc; + } + } + } + + return false; + } + + public static function mb_detect_order($encodingList = null) + { + if (null === $encodingList) { + return self::$encodingList; + } + + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + + foreach ($encodingList as $enc) { + switch ($enc) { + default: + if (strncmp($enc, 'ISO-8859-', 9)) { + return false; + } + // no break + case 'ASCII': + case 'UTF8': + case 'UTF-8': + } + } + + self::$encodingList = $encodingList; + + return true; + } + + public static function mb_strlen($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return \strlen($s); + } + + return @iconv_strlen($s, $encoding); + } + + public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strpos($haystack, $needle, $offset); + } + + $needle = (string) $needle; + if ('' === $needle) { + if (80000 > \PHP_VERSION_ID) { + trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); + + return false; + } + + return 0; + } + + return iconv_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrpos($haystack, $needle, $offset); + } + + if ($offset != (int) $offset) { + $offset = 0; + } elseif ($offset = (int) $offset) { + if ($offset < 0) { + if (0 > $offset += self::mb_strlen($needle)) { + $haystack = self::mb_substr($haystack, 0, $offset, $encoding); + } + $offset = 0; + } else { + $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); + } + } + + $pos = '' !== $needle || 80000 > \PHP_VERSION_ID + ? iconv_strrpos($haystack, $needle, $encoding) + : self::mb_strlen($haystack, $encoding); + + return false !== $pos ? $offset + $pos : false; + } + + public static function mb_str_split($string, $split_length = 1, $encoding = null) + { + if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { + trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); + + return null; + } + + if (1 > $split_length = (int) $split_length) { + if (80000 > \PHP_VERSION_ID) { + trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); + + return false; + } + + throw new \ValueError('Argument #2 ($length) must be greater than 0'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + if ('UTF-8' === $encoding = self::getEncoding($encoding)) { + $rx = '/('; + while (65535 < $split_length) { + $rx .= '.{65535}'; + $split_length -= 65535; + } + $rx .= '.{'.$split_length.'})/us'; + + return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); + } + + $result = []; + $length = mb_strlen($string, $encoding); + + for ($i = 0; $i < $length; $i += $split_length) { + $result[] = mb_substr($string, $i, $split_length, $encoding); + } + + return $result; + } + + public static function mb_strtolower($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); + } + + public static function mb_strtoupper($s, $encoding = null) + { + return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); + } + + public static function mb_substitute_character($c = null) + { + if (null === $c) { + return 'none'; + } + if (0 === strcasecmp($c, 'none')) { + return true; + } + if (80000 > \PHP_VERSION_ID) { + return false; + } + if (\is_int($c) || 'long' === $c || 'entity' === $c) { + return false; + } + + throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); + } + + public static function mb_substr($s, $start, $length = null, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return (string) substr($s, $start, null === $length ? 2147483647 : $length); + } + + if ($start < 0) { + $start = iconv_strlen($s, $encoding) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = iconv_strlen($s, $encoding) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string) iconv_substr($s, $start, $length, $encoding); + } + + public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) + { + [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ + self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), + self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), + ]); + + return self::mb_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) + { + $pos = self::mb_stripos($haystack, $needle, 0, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + $pos = strrpos($haystack, $needle); + } else { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = iconv_strrpos($haystack, $needle, $encoding); + } + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) + { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = self::mb_strripos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); + $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); + + $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); + $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); + + return self::mb_strrpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) + { + $pos = strpos($haystack, $needle); + if (false === $pos) { + return false; + } + if ($part) { + return substr($haystack, 0, $pos); + } + + return substr($haystack, $pos); + } + + public static function mb_get_info($type = 'all') + { + $info = [ + 'internal_encoding' => self::$internalEncoding, + 'http_output' => 'pass', + 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', + 'func_overload' => 0, + 'func_overload_list' => 'no overload', + 'mail_charset' => 'UTF-8', + 'mail_header_encoding' => 'BASE64', + 'mail_body_encoding' => 'BASE64', + 'illegal_chars' => 0, + 'encoding_translation' => 'Off', + 'language' => self::$language, + 'detect_order' => self::$encodingList, + 'substitute_character' => 'none', + 'strict_detection' => 'Off', + ]; + + if ('all' === $type) { + return $info; + } + if (isset($info[$type])) { + return $info[$type]; + } + + return false; + } + + public static function mb_http_input($type = '') + { + return false; + } + + public static function mb_http_output($encoding = null) + { + return null !== $encoding ? 'pass' === $encoding : 'pass'; + } + + public static function mb_strwidth($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + + if ('UTF-8' !== $encoding) { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); + + return ($wide << 1) + iconv_strlen($s, 'UTF-8'); + } + + public static function mb_substr_count($haystack, $needle, $encoding = null) + { + return substr_count($haystack, $needle); + } + + public static function mb_output_handler($contents, $status) + { + return $contents; + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } + + public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string + { + if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { + throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); + } + + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given'); + } + + if (self::mb_strlen($pad_string, $encoding) <= 0) { + throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); + } + + $paddingRequired = $length - self::mb_strlen($string, $encoding); + + if ($paddingRequired < 1) { + return $string; + } + + switch ($pad_type) { + case \STR_PAD_LEFT: + return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; + case \STR_PAD_RIGHT: + return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); + default: + $leftPaddingLength = floor($paddingRequired / 2); + $rightPaddingLength = $paddingRequired - $leftPaddingLength; + + return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); + } + } + + public static function mb_ucfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + public static function mb_lcfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + private static function getSubpart($pos, $part, $haystack, $encoding) + { + if (false === $pos) { + return false; + } + if ($part) { + return self::mb_substr($haystack, 0, $pos, $encoding); + } + + return self::mb_substr($haystack, $pos, null, $encoding); + } + + private static function html_encoding_callback(array $m) + { + $i = 1; + $entities = ''; + $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); + + while (isset($m[$i])) { + if (0x80 > $m[$i]) { + $entities .= \chr($m[$i++]); + continue; + } + if (0xF0 <= $m[$i]) { + $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } elseif (0xE0 <= $m[$i]) { + $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } else { + $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; + } + + $entities .= '&#'.$c.';'; + } + + return $entities; + } + + private static function title_case(array $s) + { + return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } + + private static function getEncoding($encoding) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + if ('UTF-8' === $encoding) { + return 'UTF-8'; + } + + $encoding = strtoupper($encoding); + + if ('8BIT' === $encoding || 'BINARY' === $encoding) { + return 'CP850'; + } + + if ('UTF8' === $encoding) { + return 'UTF-8'; + } + + return $encoding; + } + + public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } else { + self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given'); + } + + if ('' === $characters) { + return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding); + } + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $string)) { + $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string); + } + if (null !== $characters && !preg_match('//u', $characters)) { + $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters); + } + } else { + $string = iconv($encoding, 'UTF-8//IGNORE', $string); + + if (null !== $characters) { + $characters = iconv($encoding, 'UTF-8//IGNORE', $characters); + } + } + + if (null === $characters) { + $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; + } else { + $characters = preg_quote($characters); + } + + $string = preg_replace(sprintf($regex, $characters), '', $string); + + if (null === $encoding) { + return $string; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $string); + } + + private static function assertEncoding(string $encoding, string $errorFormat): void + { + try { + $validEncoding = @self::mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf($errorFormat, $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf($errorFormat, $encoding)); + } + } +} diff --git a/plants/vendor/symfony/polyfill-mbstring/README.md b/plants/vendor/symfony/polyfill-mbstring/README.md new file mode 100644 index 0000000..478b40d --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/README.md @@ -0,0 +1,13 @@ +Symfony Polyfill / Mbstring +=========================== + +This component provides a partial, native PHP implementation for the +[Mbstring](https://php.net/mbstring) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php new file mode 100644 index 0000000..512bba0 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php @@ -0,0 +1,119 @@ + 'i̇', + 'µ' => 'μ', + 'ſ' => 's', + 'ͅ' => 'ι', + 'ς' => 'σ', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϵ' => 'ε', + 'ẛ' => 'ṡ', + 'ι' => 'ι', + 'ß' => 'ss', + 'ʼn' => 'ʼn', + 'ǰ' => 'ǰ', + 'ΐ' => 'ΐ', + 'ΰ' => 'ΰ', + 'և' => 'եւ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẚ' => 'aʾ', + 'ẞ' => 'ss', + 'ὐ' => 'ὐ', + 'ὒ' => 'ὒ', + 'ὔ' => 'ὔ', + 'ὖ' => 'ὖ', + 'ᾀ' => 'ἀι', + 'ᾁ' => 'ἁι', + 'ᾂ' => 'ἂι', + 'ᾃ' => 'ἃι', + 'ᾄ' => 'ἄι', + 'ᾅ' => 'ἅι', + 'ᾆ' => 'ἆι', + 'ᾇ' => 'ἇι', + 'ᾈ' => 'ἀι', + 'ᾉ' => 'ἁι', + 'ᾊ' => 'ἂι', + 'ᾋ' => 'ἃι', + 'ᾌ' => 'ἄι', + 'ᾍ' => 'ἅι', + 'ᾎ' => 'ἆι', + 'ᾏ' => 'ἇι', + 'ᾐ' => 'ἠι', + 'ᾑ' => 'ἡι', + 'ᾒ' => 'ἢι', + 'ᾓ' => 'ἣι', + 'ᾔ' => 'ἤι', + 'ᾕ' => 'ἥι', + 'ᾖ' => 'ἦι', + 'ᾗ' => 'ἧι', + 'ᾘ' => 'ἠι', + 'ᾙ' => 'ἡι', + 'ᾚ' => 'ἢι', + 'ᾛ' => 'ἣι', + 'ᾜ' => 'ἤι', + 'ᾝ' => 'ἥι', + 'ᾞ' => 'ἦι', + 'ᾟ' => 'ἧι', + 'ᾠ' => 'ὠι', + 'ᾡ' => 'ὡι', + 'ᾢ' => 'ὢι', + 'ᾣ' => 'ὣι', + 'ᾤ' => 'ὤι', + 'ᾥ' => 'ὥι', + 'ᾦ' => 'ὦι', + 'ᾧ' => 'ὧι', + 'ᾨ' => 'ὠι', + 'ᾩ' => 'ὡι', + 'ᾪ' => 'ὢι', + 'ᾫ' => 'ὣι', + 'ᾬ' => 'ὤι', + 'ᾭ' => 'ὥι', + 'ᾮ' => 'ὦι', + 'ᾯ' => 'ὧι', + 'ᾲ' => 'ὰι', + 'ᾳ' => 'αι', + 'ᾴ' => 'άι', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾶι', + 'ᾼ' => 'αι', + 'ῂ' => 'ὴι', + 'ῃ' => 'ηι', + 'ῄ' => 'ήι', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῆι', + 'ῌ' => 'ηι', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'ῲ' => 'ὼι', + 'ῳ' => 'ωι', + 'ῴ' => 'ώι', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῶι', + 'ῼ' => 'ωι', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', +]; diff --git a/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php new file mode 100644 index 0000000..fac60b0 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -0,0 +1,1397 @@ + 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + 'À' => 'à', + 'Á' => 'á', + 'Â' => 'â', + 'Ã' => 'ã', + 'Ä' => 'ä', + 'Å' => 'å', + 'Æ' => 'æ', + 'Ç' => 'ç', + 'È' => 'è', + 'É' => 'é', + 'Ê' => 'ê', + 'Ë' => 'ë', + 'Ì' => 'ì', + 'Í' => 'í', + 'Î' => 'î', + 'Ï' => 'ï', + 'Ð' => 'ð', + 'Ñ' => 'ñ', + 'Ò' => 'ò', + 'Ó' => 'ó', + 'Ô' => 'ô', + 'Õ' => 'õ', + 'Ö' => 'ö', + 'Ø' => 'ø', + 'Ù' => 'ù', + 'Ú' => 'ú', + 'Û' => 'û', + 'Ü' => 'ü', + 'Ý' => 'ý', + 'Þ' => 'þ', + 'Ā' => 'ā', + 'Ă' => 'ă', + 'Ą' => 'ą', + 'Ć' => 'ć', + 'Ĉ' => 'ĉ', + 'Ċ' => 'ċ', + 'Č' => 'č', + 'Ď' => 'ď', + 'Đ' => 'đ', + 'Ē' => 'ē', + 'Ĕ' => 'ĕ', + 'Ė' => 'ė', + 'Ę' => 'ę', + 'Ě' => 'ě', + 'Ĝ' => 'ĝ', + 'Ğ' => 'ğ', + 'Ġ' => 'ġ', + 'Ģ' => 'ģ', + 'Ĥ' => 'ĥ', + 'Ħ' => 'ħ', + 'Ĩ' => 'ĩ', + 'Ī' => 'ī', + 'Ĭ' => 'ĭ', + 'Į' => 'į', + 'İ' => 'i̇', + 'IJ' => 'ij', + 'Ĵ' => 'ĵ', + 'Ķ' => 'ķ', + 'Ĺ' => 'ĺ', + 'Ļ' => 'ļ', + 'Ľ' => 'ľ', + 'Ŀ' => 'ŀ', + 'Ł' => 'ł', + 'Ń' => 'ń', + 'Ņ' => 'ņ', + 'Ň' => 'ň', + 'Ŋ' => 'ŋ', + 'Ō' => 'ō', + 'Ŏ' => 'ŏ', + 'Ő' => 'ő', + 'Œ' => 'œ', + 'Ŕ' => 'ŕ', + 'Ŗ' => 'ŗ', + 'Ř' => 'ř', + 'Ś' => 'ś', + 'Ŝ' => 'ŝ', + 'Ş' => 'ş', + 'Š' => 'š', + 'Ţ' => 'ţ', + 'Ť' => 'ť', + 'Ŧ' => 'ŧ', + 'Ũ' => 'ũ', + 'Ū' => 'ū', + 'Ŭ' => 'ŭ', + 'Ů' => 'ů', + 'Ű' => 'ű', + 'Ų' => 'ų', + 'Ŵ' => 'ŵ', + 'Ŷ' => 'ŷ', + 'Ÿ' => 'ÿ', + 'Ź' => 'ź', + 'Ż' => 'ż', + 'Ž' => 'ž', + 'Ɓ' => 'ɓ', + 'Ƃ' => 'ƃ', + 'Ƅ' => 'ƅ', + 'Ɔ' => 'ɔ', + 'Ƈ' => 'ƈ', + 'Ɖ' => 'ɖ', + 'Ɗ' => 'ɗ', + 'Ƌ' => 'ƌ', + 'Ǝ' => 'ǝ', + 'Ə' => 'ə', + 'Ɛ' => 'ɛ', + 'Ƒ' => 'ƒ', + 'Ɠ' => 'ɠ', + 'Ɣ' => 'ɣ', + 'Ɩ' => 'ɩ', + 'Ɨ' => 'ɨ', + 'Ƙ' => 'ƙ', + 'Ɯ' => 'ɯ', + 'Ɲ' => 'ɲ', + 'Ɵ' => 'ɵ', + 'Ơ' => 'ơ', + 'Ƣ' => 'ƣ', + 'Ƥ' => 'ƥ', + 'Ʀ' => 'ʀ', + 'Ƨ' => 'ƨ', + 'Ʃ' => 'ʃ', + 'Ƭ' => 'ƭ', + 'Ʈ' => 'ʈ', + 'Ư' => 'ư', + 'Ʊ' => 'ʊ', + 'Ʋ' => 'ʋ', + 'Ƴ' => 'ƴ', + 'Ƶ' => 'ƶ', + 'Ʒ' => 'ʒ', + 'Ƹ' => 'ƹ', + 'Ƽ' => 'ƽ', + 'DŽ' => 'dž', + 'Dž' => 'dž', + 'LJ' => 'lj', + 'Lj' => 'lj', + 'NJ' => 'nj', + 'Nj' => 'nj', + 'Ǎ' => 'ǎ', + 'Ǐ' => 'ǐ', + 'Ǒ' => 'ǒ', + 'Ǔ' => 'ǔ', + 'Ǖ' => 'ǖ', + 'Ǘ' => 'ǘ', + 'Ǚ' => 'ǚ', + 'Ǜ' => 'ǜ', + 'Ǟ' => 'ǟ', + 'Ǡ' => 'ǡ', + 'Ǣ' => 'ǣ', + 'Ǥ' => 'ǥ', + 'Ǧ' => 'ǧ', + 'Ǩ' => 'ǩ', + 'Ǫ' => 'ǫ', + 'Ǭ' => 'ǭ', + 'Ǯ' => 'ǯ', + 'DZ' => 'dz', + 'Dz' => 'dz', + 'Ǵ' => 'ǵ', + 'Ƕ' => 'ƕ', + 'Ƿ' => 'ƿ', + 'Ǹ' => 'ǹ', + 'Ǻ' => 'ǻ', + 'Ǽ' => 'ǽ', + 'Ǿ' => 'ǿ', + 'Ȁ' => 'ȁ', + 'Ȃ' => 'ȃ', + 'Ȅ' => 'ȅ', + 'Ȇ' => 'ȇ', + 'Ȉ' => 'ȉ', + 'Ȋ' => 'ȋ', + 'Ȍ' => 'ȍ', + 'Ȏ' => 'ȏ', + 'Ȑ' => 'ȑ', + 'Ȓ' => 'ȓ', + 'Ȕ' => 'ȕ', + 'Ȗ' => 'ȗ', + 'Ș' => 'ș', + 'Ț' => 'ț', + 'Ȝ' => 'ȝ', + 'Ȟ' => 'ȟ', + 'Ƞ' => 'ƞ', + 'Ȣ' => 'ȣ', + 'Ȥ' => 'ȥ', + 'Ȧ' => 'ȧ', + 'Ȩ' => 'ȩ', + 'Ȫ' => 'ȫ', + 'Ȭ' => 'ȭ', + 'Ȯ' => 'ȯ', + 'Ȱ' => 'ȱ', + 'Ȳ' => 'ȳ', + 'Ⱥ' => 'ⱥ', + 'Ȼ' => 'ȼ', + 'Ƚ' => 'ƚ', + 'Ⱦ' => 'ⱦ', + 'Ɂ' => 'ɂ', + 'Ƀ' => 'ƀ', + 'Ʉ' => 'ʉ', + 'Ʌ' => 'ʌ', + 'Ɇ' => 'ɇ', + 'Ɉ' => 'ɉ', + 'Ɋ' => 'ɋ', + 'Ɍ' => 'ɍ', + 'Ɏ' => 'ɏ', + 'Ͱ' => 'ͱ', + 'Ͳ' => 'ͳ', + 'Ͷ' => 'ͷ', + 'Ϳ' => 'ϳ', + 'Ά' => 'ά', + 'Έ' => 'έ', + 'Ή' => 'ή', + 'Ί' => 'ί', + 'Ό' => 'ό', + 'Ύ' => 'ύ', + 'Ώ' => 'ώ', + 'Α' => 'α', + 'Β' => 'β', + 'Γ' => 'γ', + 'Δ' => 'δ', + 'Ε' => 'ε', + 'Ζ' => 'ζ', + 'Η' => 'η', + 'Θ' => 'θ', + 'Ι' => 'ι', + 'Κ' => 'κ', + 'Λ' => 'λ', + 'Μ' => 'μ', + 'Ν' => 'ν', + 'Ξ' => 'ξ', + 'Ο' => 'ο', + 'Π' => 'π', + 'Ρ' => 'ρ', + 'Σ' => 'σ', + 'Τ' => 'τ', + 'Υ' => 'υ', + 'Φ' => 'φ', + 'Χ' => 'χ', + 'Ψ' => 'ψ', + 'Ω' => 'ω', + 'Ϊ' => 'ϊ', + 'Ϋ' => 'ϋ', + 'Ϗ' => 'ϗ', + 'Ϙ' => 'ϙ', + 'Ϛ' => 'ϛ', + 'Ϝ' => 'ϝ', + 'Ϟ' => 'ϟ', + 'Ϡ' => 'ϡ', + 'Ϣ' => 'ϣ', + 'Ϥ' => 'ϥ', + 'Ϧ' => 'ϧ', + 'Ϩ' => 'ϩ', + 'Ϫ' => 'ϫ', + 'Ϭ' => 'ϭ', + 'Ϯ' => 'ϯ', + 'ϴ' => 'θ', + 'Ϸ' => 'ϸ', + 'Ϲ' => 'ϲ', + 'Ϻ' => 'ϻ', + 'Ͻ' => 'ͻ', + 'Ͼ' => 'ͼ', + 'Ͽ' => 'ͽ', + 'Ѐ' => 'ѐ', + 'Ё' => 'ё', + 'Ђ' => 'ђ', + 'Ѓ' => 'ѓ', + 'Є' => 'є', + 'Ѕ' => 'ѕ', + 'І' => 'і', + 'Ї' => 'ї', + 'Ј' => 'ј', + 'Љ' => 'љ', + 'Њ' => 'њ', + 'Ћ' => 'ћ', + 'Ќ' => 'ќ', + 'Ѝ' => 'ѝ', + 'Ў' => 'ў', + 'Џ' => 'џ', + 'А' => 'а', + 'Б' => 'б', + 'В' => 'в', + 'Г' => 'г', + 'Д' => 'д', + 'Е' => 'е', + 'Ж' => 'ж', + 'З' => 'з', + 'И' => 'и', + 'Й' => 'й', + 'К' => 'к', + 'Л' => 'л', + 'М' => 'м', + 'Н' => 'н', + 'О' => 'о', + 'П' => 'п', + 'Р' => 'р', + 'С' => 'с', + 'Т' => 'т', + 'У' => 'у', + 'Ф' => 'ф', + 'Х' => 'х', + 'Ц' => 'ц', + 'Ч' => 'ч', + 'Ш' => 'ш', + 'Щ' => 'щ', + 'Ъ' => 'ъ', + 'Ы' => 'ы', + 'Ь' => 'ь', + 'Э' => 'э', + 'Ю' => 'ю', + 'Я' => 'я', + 'Ѡ' => 'ѡ', + 'Ѣ' => 'ѣ', + 'Ѥ' => 'ѥ', + 'Ѧ' => 'ѧ', + 'Ѩ' => 'ѩ', + 'Ѫ' => 'ѫ', + 'Ѭ' => 'ѭ', + 'Ѯ' => 'ѯ', + 'Ѱ' => 'ѱ', + 'Ѳ' => 'ѳ', + 'Ѵ' => 'ѵ', + 'Ѷ' => 'ѷ', + 'Ѹ' => 'ѹ', + 'Ѻ' => 'ѻ', + 'Ѽ' => 'ѽ', + 'Ѿ' => 'ѿ', + 'Ҁ' => 'ҁ', + 'Ҋ' => 'ҋ', + 'Ҍ' => 'ҍ', + 'Ҏ' => 'ҏ', + 'Ґ' => 'ґ', + 'Ғ' => 'ғ', + 'Ҕ' => 'ҕ', + 'Җ' => 'җ', + 'Ҙ' => 'ҙ', + 'Қ' => 'қ', + 'Ҝ' => 'ҝ', + 'Ҟ' => 'ҟ', + 'Ҡ' => 'ҡ', + 'Ң' => 'ң', + 'Ҥ' => 'ҥ', + 'Ҧ' => 'ҧ', + 'Ҩ' => 'ҩ', + 'Ҫ' => 'ҫ', + 'Ҭ' => 'ҭ', + 'Ү' => 'ү', + 'Ұ' => 'ұ', + 'Ҳ' => 'ҳ', + 'Ҵ' => 'ҵ', + 'Ҷ' => 'ҷ', + 'Ҹ' => 'ҹ', + 'Һ' => 'һ', + 'Ҽ' => 'ҽ', + 'Ҿ' => 'ҿ', + 'Ӏ' => 'ӏ', + 'Ӂ' => 'ӂ', + 'Ӄ' => 'ӄ', + 'Ӆ' => 'ӆ', + 'Ӈ' => 'ӈ', + 'Ӊ' => 'ӊ', + 'Ӌ' => 'ӌ', + 'Ӎ' => 'ӎ', + 'Ӑ' => 'ӑ', + 'Ӓ' => 'ӓ', + 'Ӕ' => 'ӕ', + 'Ӗ' => 'ӗ', + 'Ә' => 'ә', + 'Ӛ' => 'ӛ', + 'Ӝ' => 'ӝ', + 'Ӟ' => 'ӟ', + 'Ӡ' => 'ӡ', + 'Ӣ' => 'ӣ', + 'Ӥ' => 'ӥ', + 'Ӧ' => 'ӧ', + 'Ө' => 'ө', + 'Ӫ' => 'ӫ', + 'Ӭ' => 'ӭ', + 'Ӯ' => 'ӯ', + 'Ӱ' => 'ӱ', + 'Ӳ' => 'ӳ', + 'Ӵ' => 'ӵ', + 'Ӷ' => 'ӷ', + 'Ӹ' => 'ӹ', + 'Ӻ' => 'ӻ', + 'Ӽ' => 'ӽ', + 'Ӿ' => 'ӿ', + 'Ԁ' => 'ԁ', + 'Ԃ' => 'ԃ', + 'Ԅ' => 'ԅ', + 'Ԇ' => 'ԇ', + 'Ԉ' => 'ԉ', + 'Ԋ' => 'ԋ', + 'Ԍ' => 'ԍ', + 'Ԏ' => 'ԏ', + 'Ԑ' => 'ԑ', + 'Ԓ' => 'ԓ', + 'Ԕ' => 'ԕ', + 'Ԗ' => 'ԗ', + 'Ԙ' => 'ԙ', + 'Ԛ' => 'ԛ', + 'Ԝ' => 'ԝ', + 'Ԟ' => 'ԟ', + 'Ԡ' => 'ԡ', + 'Ԣ' => 'ԣ', + 'Ԥ' => 'ԥ', + 'Ԧ' => 'ԧ', + 'Ԩ' => 'ԩ', + 'Ԫ' => 'ԫ', + 'Ԭ' => 'ԭ', + 'Ԯ' => 'ԯ', + 'Ա' => 'ա', + 'Բ' => 'բ', + 'Գ' => 'գ', + 'Դ' => 'դ', + 'Ե' => 'ե', + 'Զ' => 'զ', + 'Է' => 'է', + 'Ը' => 'ը', + 'Թ' => 'թ', + 'Ժ' => 'ժ', + 'Ի' => 'ի', + 'Լ' => 'լ', + 'Խ' => 'խ', + 'Ծ' => 'ծ', + 'Կ' => 'կ', + 'Հ' => 'հ', + 'Ձ' => 'ձ', + 'Ղ' => 'ղ', + 'Ճ' => 'ճ', + 'Մ' => 'մ', + 'Յ' => 'յ', + 'Ն' => 'ն', + 'Շ' => 'շ', + 'Ո' => 'ո', + 'Չ' => 'չ', + 'Պ' => 'պ', + 'Ջ' => 'ջ', + 'Ռ' => 'ռ', + 'Ս' => 'ս', + 'Վ' => 'վ', + 'Տ' => 'տ', + 'Ր' => 'ր', + 'Ց' => 'ց', + 'Ւ' => 'ւ', + 'Փ' => 'փ', + 'Ք' => 'ք', + 'Օ' => 'օ', + 'Ֆ' => 'ֆ', + 'Ⴀ' => 'ⴀ', + 'Ⴁ' => 'ⴁ', + 'Ⴂ' => 'ⴂ', + 'Ⴃ' => 'ⴃ', + 'Ⴄ' => 'ⴄ', + 'Ⴅ' => 'ⴅ', + 'Ⴆ' => 'ⴆ', + 'Ⴇ' => 'ⴇ', + 'Ⴈ' => 'ⴈ', + 'Ⴉ' => 'ⴉ', + 'Ⴊ' => 'ⴊ', + 'Ⴋ' => 'ⴋ', + 'Ⴌ' => 'ⴌ', + 'Ⴍ' => 'ⴍ', + 'Ⴎ' => 'ⴎ', + 'Ⴏ' => 'ⴏ', + 'Ⴐ' => 'ⴐ', + 'Ⴑ' => 'ⴑ', + 'Ⴒ' => 'ⴒ', + 'Ⴓ' => 'ⴓ', + 'Ⴔ' => 'ⴔ', + 'Ⴕ' => 'ⴕ', + 'Ⴖ' => 'ⴖ', + 'Ⴗ' => 'ⴗ', + 'Ⴘ' => 'ⴘ', + 'Ⴙ' => 'ⴙ', + 'Ⴚ' => 'ⴚ', + 'Ⴛ' => 'ⴛ', + 'Ⴜ' => 'ⴜ', + 'Ⴝ' => 'ⴝ', + 'Ⴞ' => 'ⴞ', + 'Ⴟ' => 'ⴟ', + 'Ⴠ' => 'ⴠ', + 'Ⴡ' => 'ⴡ', + 'Ⴢ' => 'ⴢ', + 'Ⴣ' => 'ⴣ', + 'Ⴤ' => 'ⴤ', + 'Ⴥ' => 'ⴥ', + 'Ⴧ' => 'ⴧ', + 'Ⴭ' => 'ⴭ', + 'Ꭰ' => 'ꭰ', + 'Ꭱ' => 'ꭱ', + 'Ꭲ' => 'ꭲ', + 'Ꭳ' => 'ꭳ', + 'Ꭴ' => 'ꭴ', + 'Ꭵ' => 'ꭵ', + 'Ꭶ' => 'ꭶ', + 'Ꭷ' => 'ꭷ', + 'Ꭸ' => 'ꭸ', + 'Ꭹ' => 'ꭹ', + 'Ꭺ' => 'ꭺ', + 'Ꭻ' => 'ꭻ', + 'Ꭼ' => 'ꭼ', + 'Ꭽ' => 'ꭽ', + 'Ꭾ' => 'ꭾ', + 'Ꭿ' => 'ꭿ', + 'Ꮀ' => 'ꮀ', + 'Ꮁ' => 'ꮁ', + 'Ꮂ' => 'ꮂ', + 'Ꮃ' => 'ꮃ', + 'Ꮄ' => 'ꮄ', + 'Ꮅ' => 'ꮅ', + 'Ꮆ' => 'ꮆ', + 'Ꮇ' => 'ꮇ', + 'Ꮈ' => 'ꮈ', + 'Ꮉ' => 'ꮉ', + 'Ꮊ' => 'ꮊ', + 'Ꮋ' => 'ꮋ', + 'Ꮌ' => 'ꮌ', + 'Ꮍ' => 'ꮍ', + 'Ꮎ' => 'ꮎ', + 'Ꮏ' => 'ꮏ', + 'Ꮐ' => 'ꮐ', + 'Ꮑ' => 'ꮑ', + 'Ꮒ' => 'ꮒ', + 'Ꮓ' => 'ꮓ', + 'Ꮔ' => 'ꮔ', + 'Ꮕ' => 'ꮕ', + 'Ꮖ' => 'ꮖ', + 'Ꮗ' => 'ꮗ', + 'Ꮘ' => 'ꮘ', + 'Ꮙ' => 'ꮙ', + 'Ꮚ' => 'ꮚ', + 'Ꮛ' => 'ꮛ', + 'Ꮜ' => 'ꮜ', + 'Ꮝ' => 'ꮝ', + 'Ꮞ' => 'ꮞ', + 'Ꮟ' => 'ꮟ', + 'Ꮠ' => 'ꮠ', + 'Ꮡ' => 'ꮡ', + 'Ꮢ' => 'ꮢ', + 'Ꮣ' => 'ꮣ', + 'Ꮤ' => 'ꮤ', + 'Ꮥ' => 'ꮥ', + 'Ꮦ' => 'ꮦ', + 'Ꮧ' => 'ꮧ', + 'Ꮨ' => 'ꮨ', + 'Ꮩ' => 'ꮩ', + 'Ꮪ' => 'ꮪ', + 'Ꮫ' => 'ꮫ', + 'Ꮬ' => 'ꮬ', + 'Ꮭ' => 'ꮭ', + 'Ꮮ' => 'ꮮ', + 'Ꮯ' => 'ꮯ', + 'Ꮰ' => 'ꮰ', + 'Ꮱ' => 'ꮱ', + 'Ꮲ' => 'ꮲ', + 'Ꮳ' => 'ꮳ', + 'Ꮴ' => 'ꮴ', + 'Ꮵ' => 'ꮵ', + 'Ꮶ' => 'ꮶ', + 'Ꮷ' => 'ꮷ', + 'Ꮸ' => 'ꮸ', + 'Ꮹ' => 'ꮹ', + 'Ꮺ' => 'ꮺ', + 'Ꮻ' => 'ꮻ', + 'Ꮼ' => 'ꮼ', + 'Ꮽ' => 'ꮽ', + 'Ꮾ' => 'ꮾ', + 'Ꮿ' => 'ꮿ', + 'Ᏸ' => 'ᏸ', + 'Ᏹ' => 'ᏹ', + 'Ᏺ' => 'ᏺ', + 'Ᏻ' => 'ᏻ', + 'Ᏼ' => 'ᏼ', + 'Ᏽ' => 'ᏽ', + 'Ა' => 'ა', + 'Ბ' => 'ბ', + 'Გ' => 'გ', + 'Დ' => 'დ', + 'Ე' => 'ე', + 'Ვ' => 'ვ', + 'Ზ' => 'ზ', + 'Თ' => 'თ', + 'Ი' => 'ი', + 'Კ' => 'კ', + 'Ლ' => 'ლ', + 'Მ' => 'მ', + 'Ნ' => 'ნ', + 'Ო' => 'ო', + 'Პ' => 'პ', + 'Ჟ' => 'ჟ', + 'Რ' => 'რ', + 'Ს' => 'ს', + 'Ტ' => 'ტ', + 'Უ' => 'უ', + 'Ფ' => 'ფ', + 'Ქ' => 'ქ', + 'Ღ' => 'ღ', + 'Ყ' => 'ყ', + 'Შ' => 'შ', + 'Ჩ' => 'ჩ', + 'Ც' => 'ც', + 'Ძ' => 'ძ', + 'Წ' => 'წ', + 'Ჭ' => 'ჭ', + 'Ხ' => 'ხ', + 'Ჯ' => 'ჯ', + 'Ჰ' => 'ჰ', + 'Ჱ' => 'ჱ', + 'Ჲ' => 'ჲ', + 'Ჳ' => 'ჳ', + 'Ჴ' => 'ჴ', + 'Ჵ' => 'ჵ', + 'Ჶ' => 'ჶ', + 'Ჷ' => 'ჷ', + 'Ჸ' => 'ჸ', + 'Ჹ' => 'ჹ', + 'Ჺ' => 'ჺ', + 'Ჽ' => 'ჽ', + 'Ჾ' => 'ჾ', + 'Ჿ' => 'ჿ', + 'Ḁ' => 'ḁ', + 'Ḃ' => 'ḃ', + 'Ḅ' => 'ḅ', + 'Ḇ' => 'ḇ', + 'Ḉ' => 'ḉ', + 'Ḋ' => 'ḋ', + 'Ḍ' => 'ḍ', + 'Ḏ' => 'ḏ', + 'Ḑ' => 'ḑ', + 'Ḓ' => 'ḓ', + 'Ḕ' => 'ḕ', + 'Ḗ' => 'ḗ', + 'Ḙ' => 'ḙ', + 'Ḛ' => 'ḛ', + 'Ḝ' => 'ḝ', + 'Ḟ' => 'ḟ', + 'Ḡ' => 'ḡ', + 'Ḣ' => 'ḣ', + 'Ḥ' => 'ḥ', + 'Ḧ' => 'ḧ', + 'Ḩ' => 'ḩ', + 'Ḫ' => 'ḫ', + 'Ḭ' => 'ḭ', + 'Ḯ' => 'ḯ', + 'Ḱ' => 'ḱ', + 'Ḳ' => 'ḳ', + 'Ḵ' => 'ḵ', + 'Ḷ' => 'ḷ', + 'Ḹ' => 'ḹ', + 'Ḻ' => 'ḻ', + 'Ḽ' => 'ḽ', + 'Ḿ' => 'ḿ', + 'Ṁ' => 'ṁ', + 'Ṃ' => 'ṃ', + 'Ṅ' => 'ṅ', + 'Ṇ' => 'ṇ', + 'Ṉ' => 'ṉ', + 'Ṋ' => 'ṋ', + 'Ṍ' => 'ṍ', + 'Ṏ' => 'ṏ', + 'Ṑ' => 'ṑ', + 'Ṓ' => 'ṓ', + 'Ṕ' => 'ṕ', + 'Ṗ' => 'ṗ', + 'Ṙ' => 'ṙ', + 'Ṛ' => 'ṛ', + 'Ṝ' => 'ṝ', + 'Ṟ' => 'ṟ', + 'Ṡ' => 'ṡ', + 'Ṣ' => 'ṣ', + 'Ṥ' => 'ṥ', + 'Ṧ' => 'ṧ', + 'Ṩ' => 'ṩ', + 'Ṫ' => 'ṫ', + 'Ṭ' => 'ṭ', + 'Ṯ' => 'ṯ', + 'Ṱ' => 'ṱ', + 'Ṳ' => 'ṳ', + 'Ṵ' => 'ṵ', + 'Ṷ' => 'ṷ', + 'Ṹ' => 'ṹ', + 'Ṻ' => 'ṻ', + 'Ṽ' => 'ṽ', + 'Ṿ' => 'ṿ', + 'Ẁ' => 'ẁ', + 'Ẃ' => 'ẃ', + 'Ẅ' => 'ẅ', + 'Ẇ' => 'ẇ', + 'Ẉ' => 'ẉ', + 'Ẋ' => 'ẋ', + 'Ẍ' => 'ẍ', + 'Ẏ' => 'ẏ', + 'Ẑ' => 'ẑ', + 'Ẓ' => 'ẓ', + 'Ẕ' => 'ẕ', + 'ẞ' => 'ß', + 'Ạ' => 'ạ', + 'Ả' => 'ả', + 'Ấ' => 'ấ', + 'Ầ' => 'ầ', + 'Ẩ' => 'ẩ', + 'Ẫ' => 'ẫ', + 'Ậ' => 'ậ', + 'Ắ' => 'ắ', + 'Ằ' => 'ằ', + 'Ẳ' => 'ẳ', + 'Ẵ' => 'ẵ', + 'Ặ' => 'ặ', + 'Ẹ' => 'ẹ', + 'Ẻ' => 'ẻ', + 'Ẽ' => 'ẽ', + 'Ế' => 'ế', + 'Ề' => 'ề', + 'Ể' => 'ể', + 'Ễ' => 'ễ', + 'Ệ' => 'ệ', + 'Ỉ' => 'ỉ', + 'Ị' => 'ị', + 'Ọ' => 'ọ', + 'Ỏ' => 'ỏ', + 'Ố' => 'ố', + 'Ồ' => 'ồ', + 'Ổ' => 'ổ', + 'Ỗ' => 'ỗ', + 'Ộ' => 'ộ', + 'Ớ' => 'ớ', + 'Ờ' => 'ờ', + 'Ở' => 'ở', + 'Ỡ' => 'ỡ', + 'Ợ' => 'ợ', + 'Ụ' => 'ụ', + 'Ủ' => 'ủ', + 'Ứ' => 'ứ', + 'Ừ' => 'ừ', + 'Ử' => 'ử', + 'Ữ' => 'ữ', + 'Ự' => 'ự', + 'Ỳ' => 'ỳ', + 'Ỵ' => 'ỵ', + 'Ỷ' => 'ỷ', + 'Ỹ' => 'ỹ', + 'Ỻ' => 'ỻ', + 'Ỽ' => 'ỽ', + 'Ỿ' => 'ỿ', + 'Ἀ' => 'ἀ', + 'Ἁ' => 'ἁ', + 'Ἂ' => 'ἂ', + 'Ἃ' => 'ἃ', + 'Ἄ' => 'ἄ', + 'Ἅ' => 'ἅ', + 'Ἆ' => 'ἆ', + 'Ἇ' => 'ἇ', + 'Ἐ' => 'ἐ', + 'Ἑ' => 'ἑ', + 'Ἒ' => 'ἒ', + 'Ἓ' => 'ἓ', + 'Ἔ' => 'ἔ', + 'Ἕ' => 'ἕ', + 'Ἠ' => 'ἠ', + 'Ἡ' => 'ἡ', + 'Ἢ' => 'ἢ', + 'Ἣ' => 'ἣ', + 'Ἤ' => 'ἤ', + 'Ἥ' => 'ἥ', + 'Ἦ' => 'ἦ', + 'Ἧ' => 'ἧ', + 'Ἰ' => 'ἰ', + 'Ἱ' => 'ἱ', + 'Ἲ' => 'ἲ', + 'Ἳ' => 'ἳ', + 'Ἴ' => 'ἴ', + 'Ἵ' => 'ἵ', + 'Ἶ' => 'ἶ', + 'Ἷ' => 'ἷ', + 'Ὀ' => 'ὀ', + 'Ὁ' => 'ὁ', + 'Ὂ' => 'ὂ', + 'Ὃ' => 'ὃ', + 'Ὄ' => 'ὄ', + 'Ὅ' => 'ὅ', + 'Ὑ' => 'ὑ', + 'Ὓ' => 'ὓ', + 'Ὕ' => 'ὕ', + 'Ὗ' => 'ὗ', + 'Ὠ' => 'ὠ', + 'Ὡ' => 'ὡ', + 'Ὢ' => 'ὢ', + 'Ὣ' => 'ὣ', + 'Ὤ' => 'ὤ', + 'Ὥ' => 'ὥ', + 'Ὦ' => 'ὦ', + 'Ὧ' => 'ὧ', + 'ᾈ' => 'ᾀ', + 'ᾉ' => 'ᾁ', + 'ᾊ' => 'ᾂ', + 'ᾋ' => 'ᾃ', + 'ᾌ' => 'ᾄ', + 'ᾍ' => 'ᾅ', + 'ᾎ' => 'ᾆ', + 'ᾏ' => 'ᾇ', + 'ᾘ' => 'ᾐ', + 'ᾙ' => 'ᾑ', + 'ᾚ' => 'ᾒ', + 'ᾛ' => 'ᾓ', + 'ᾜ' => 'ᾔ', + 'ᾝ' => 'ᾕ', + 'ᾞ' => 'ᾖ', + 'ᾟ' => 'ᾗ', + 'ᾨ' => 'ᾠ', + 'ᾩ' => 'ᾡ', + 'ᾪ' => 'ᾢ', + 'ᾫ' => 'ᾣ', + 'ᾬ' => 'ᾤ', + 'ᾭ' => 'ᾥ', + 'ᾮ' => 'ᾦ', + 'ᾯ' => 'ᾧ', + 'Ᾰ' => 'ᾰ', + 'Ᾱ' => 'ᾱ', + 'Ὰ' => 'ὰ', + 'Ά' => 'ά', + 'ᾼ' => 'ᾳ', + 'Ὲ' => 'ὲ', + 'Έ' => 'έ', + 'Ὴ' => 'ὴ', + 'Ή' => 'ή', + 'ῌ' => 'ῃ', + 'Ῐ' => 'ῐ', + 'Ῑ' => 'ῑ', + 'Ὶ' => 'ὶ', + 'Ί' => 'ί', + 'Ῠ' => 'ῠ', + 'Ῡ' => 'ῡ', + 'Ὺ' => 'ὺ', + 'Ύ' => 'ύ', + 'Ῥ' => 'ῥ', + 'Ὸ' => 'ὸ', + 'Ό' => 'ό', + 'Ὼ' => 'ὼ', + 'Ώ' => 'ώ', + 'ῼ' => 'ῳ', + 'Ω' => 'ω', + 'K' => 'k', + 'Å' => 'å', + 'Ⅎ' => 'ⅎ', + 'Ⅰ' => 'ⅰ', + 'Ⅱ' => 'ⅱ', + 'Ⅲ' => 'ⅲ', + 'Ⅳ' => 'ⅳ', + 'Ⅴ' => 'ⅴ', + 'Ⅵ' => 'ⅵ', + 'Ⅶ' => 'ⅶ', + 'Ⅷ' => 'ⅷ', + 'Ⅸ' => 'ⅸ', + 'Ⅹ' => 'ⅹ', + 'Ⅺ' => 'ⅺ', + 'Ⅻ' => 'ⅻ', + 'Ⅼ' => 'ⅼ', + 'Ⅽ' => 'ⅽ', + 'Ⅾ' => 'ⅾ', + 'Ⅿ' => 'ⅿ', + 'Ↄ' => 'ↄ', + 'Ⓐ' => 'ⓐ', + 'Ⓑ' => 'ⓑ', + 'Ⓒ' => 'ⓒ', + 'Ⓓ' => 'ⓓ', + 'Ⓔ' => 'ⓔ', + 'Ⓕ' => 'ⓕ', + 'Ⓖ' => 'ⓖ', + 'Ⓗ' => 'ⓗ', + 'Ⓘ' => 'ⓘ', + 'Ⓙ' => 'ⓙ', + 'Ⓚ' => 'ⓚ', + 'Ⓛ' => 'ⓛ', + 'Ⓜ' => 'ⓜ', + 'Ⓝ' => 'ⓝ', + 'Ⓞ' => 'ⓞ', + 'Ⓟ' => 'ⓟ', + 'Ⓠ' => 'ⓠ', + 'Ⓡ' => 'ⓡ', + 'Ⓢ' => 'ⓢ', + 'Ⓣ' => 'ⓣ', + 'Ⓤ' => 'ⓤ', + 'Ⓥ' => 'ⓥ', + 'Ⓦ' => 'ⓦ', + 'Ⓧ' => 'ⓧ', + 'Ⓨ' => 'ⓨ', + 'Ⓩ' => 'ⓩ', + 'Ⰰ' => 'ⰰ', + 'Ⰱ' => 'ⰱ', + 'Ⰲ' => 'ⰲ', + 'Ⰳ' => 'ⰳ', + 'Ⰴ' => 'ⰴ', + 'Ⰵ' => 'ⰵ', + 'Ⰶ' => 'ⰶ', + 'Ⰷ' => 'ⰷ', + 'Ⰸ' => 'ⰸ', + 'Ⰹ' => 'ⰹ', + 'Ⰺ' => 'ⰺ', + 'Ⰻ' => 'ⰻ', + 'Ⰼ' => 'ⰼ', + 'Ⰽ' => 'ⰽ', + 'Ⰾ' => 'ⰾ', + 'Ⰿ' => 'ⰿ', + 'Ⱀ' => 'ⱀ', + 'Ⱁ' => 'ⱁ', + 'Ⱂ' => 'ⱂ', + 'Ⱃ' => 'ⱃ', + 'Ⱄ' => 'ⱄ', + 'Ⱅ' => 'ⱅ', + 'Ⱆ' => 'ⱆ', + 'Ⱇ' => 'ⱇ', + 'Ⱈ' => 'ⱈ', + 'Ⱉ' => 'ⱉ', + 'Ⱊ' => 'ⱊ', + 'Ⱋ' => 'ⱋ', + 'Ⱌ' => 'ⱌ', + 'Ⱍ' => 'ⱍ', + 'Ⱎ' => 'ⱎ', + 'Ⱏ' => 'ⱏ', + 'Ⱐ' => 'ⱐ', + 'Ⱑ' => 'ⱑ', + 'Ⱒ' => 'ⱒ', + 'Ⱓ' => 'ⱓ', + 'Ⱔ' => 'ⱔ', + 'Ⱕ' => 'ⱕ', + 'Ⱖ' => 'ⱖ', + 'Ⱗ' => 'ⱗ', + 'Ⱘ' => 'ⱘ', + 'Ⱙ' => 'ⱙ', + 'Ⱚ' => 'ⱚ', + 'Ⱛ' => 'ⱛ', + 'Ⱜ' => 'ⱜ', + 'Ⱝ' => 'ⱝ', + 'Ⱞ' => 'ⱞ', + 'Ⱡ' => 'ⱡ', + 'Ɫ' => 'ɫ', + 'Ᵽ' => 'ᵽ', + 'Ɽ' => 'ɽ', + 'Ⱨ' => 'ⱨ', + 'Ⱪ' => 'ⱪ', + 'Ⱬ' => 'ⱬ', + 'Ɑ' => 'ɑ', + 'Ɱ' => 'ɱ', + 'Ɐ' => 'ɐ', + 'Ɒ' => 'ɒ', + 'Ⱳ' => 'ⱳ', + 'Ⱶ' => 'ⱶ', + 'Ȿ' => 'ȿ', + 'Ɀ' => 'ɀ', + 'Ⲁ' => 'ⲁ', + 'Ⲃ' => 'ⲃ', + 'Ⲅ' => 'ⲅ', + 'Ⲇ' => 'ⲇ', + 'Ⲉ' => 'ⲉ', + 'Ⲋ' => 'ⲋ', + 'Ⲍ' => 'ⲍ', + 'Ⲏ' => 'ⲏ', + 'Ⲑ' => 'ⲑ', + 'Ⲓ' => 'ⲓ', + 'Ⲕ' => 'ⲕ', + 'Ⲗ' => 'ⲗ', + 'Ⲙ' => 'ⲙ', + 'Ⲛ' => 'ⲛ', + 'Ⲝ' => 'ⲝ', + 'Ⲟ' => 'ⲟ', + 'Ⲡ' => 'ⲡ', + 'Ⲣ' => 'ⲣ', + 'Ⲥ' => 'ⲥ', + 'Ⲧ' => 'ⲧ', + 'Ⲩ' => 'ⲩ', + 'Ⲫ' => 'ⲫ', + 'Ⲭ' => 'ⲭ', + 'Ⲯ' => 'ⲯ', + 'Ⲱ' => 'ⲱ', + 'Ⲳ' => 'ⲳ', + 'Ⲵ' => 'ⲵ', + 'Ⲷ' => 'ⲷ', + 'Ⲹ' => 'ⲹ', + 'Ⲻ' => 'ⲻ', + 'Ⲽ' => 'ⲽ', + 'Ⲿ' => 'ⲿ', + 'Ⳁ' => 'ⳁ', + 'Ⳃ' => 'ⳃ', + 'Ⳅ' => 'ⳅ', + 'Ⳇ' => 'ⳇ', + 'Ⳉ' => 'ⳉ', + 'Ⳋ' => 'ⳋ', + 'Ⳍ' => 'ⳍ', + 'Ⳏ' => 'ⳏ', + 'Ⳑ' => 'ⳑ', + 'Ⳓ' => 'ⳓ', + 'Ⳕ' => 'ⳕ', + 'Ⳗ' => 'ⳗ', + 'Ⳙ' => 'ⳙ', + 'Ⳛ' => 'ⳛ', + 'Ⳝ' => 'ⳝ', + 'Ⳟ' => 'ⳟ', + 'Ⳡ' => 'ⳡ', + 'Ⳣ' => 'ⳣ', + 'Ⳬ' => 'ⳬ', + 'Ⳮ' => 'ⳮ', + 'Ⳳ' => 'ⳳ', + 'Ꙁ' => 'ꙁ', + 'Ꙃ' => 'ꙃ', + 'Ꙅ' => 'ꙅ', + 'Ꙇ' => 'ꙇ', + 'Ꙉ' => 'ꙉ', + 'Ꙋ' => 'ꙋ', + 'Ꙍ' => 'ꙍ', + 'Ꙏ' => 'ꙏ', + 'Ꙑ' => 'ꙑ', + 'Ꙓ' => 'ꙓ', + 'Ꙕ' => 'ꙕ', + 'Ꙗ' => 'ꙗ', + 'Ꙙ' => 'ꙙ', + 'Ꙛ' => 'ꙛ', + 'Ꙝ' => 'ꙝ', + 'Ꙟ' => 'ꙟ', + 'Ꙡ' => 'ꙡ', + 'Ꙣ' => 'ꙣ', + 'Ꙥ' => 'ꙥ', + 'Ꙧ' => 'ꙧ', + 'Ꙩ' => 'ꙩ', + 'Ꙫ' => 'ꙫ', + 'Ꙭ' => 'ꙭ', + 'Ꚁ' => 'ꚁ', + 'Ꚃ' => 'ꚃ', + 'Ꚅ' => 'ꚅ', + 'Ꚇ' => 'ꚇ', + 'Ꚉ' => 'ꚉ', + 'Ꚋ' => 'ꚋ', + 'Ꚍ' => 'ꚍ', + 'Ꚏ' => 'ꚏ', + 'Ꚑ' => 'ꚑ', + 'Ꚓ' => 'ꚓ', + 'Ꚕ' => 'ꚕ', + 'Ꚗ' => 'ꚗ', + 'Ꚙ' => 'ꚙ', + 'Ꚛ' => 'ꚛ', + 'Ꜣ' => 'ꜣ', + 'Ꜥ' => 'ꜥ', + 'Ꜧ' => 'ꜧ', + 'Ꜩ' => 'ꜩ', + 'Ꜫ' => 'ꜫ', + 'Ꜭ' => 'ꜭ', + 'Ꜯ' => 'ꜯ', + 'Ꜳ' => 'ꜳ', + 'Ꜵ' => 'ꜵ', + 'Ꜷ' => 'ꜷ', + 'Ꜹ' => 'ꜹ', + 'Ꜻ' => 'ꜻ', + 'Ꜽ' => 'ꜽ', + 'Ꜿ' => 'ꜿ', + 'Ꝁ' => 'ꝁ', + 'Ꝃ' => 'ꝃ', + 'Ꝅ' => 'ꝅ', + 'Ꝇ' => 'ꝇ', + 'Ꝉ' => 'ꝉ', + 'Ꝋ' => 'ꝋ', + 'Ꝍ' => 'ꝍ', + 'Ꝏ' => 'ꝏ', + 'Ꝑ' => 'ꝑ', + 'Ꝓ' => 'ꝓ', + 'Ꝕ' => 'ꝕ', + 'Ꝗ' => 'ꝗ', + 'Ꝙ' => 'ꝙ', + 'Ꝛ' => 'ꝛ', + 'Ꝝ' => 'ꝝ', + 'Ꝟ' => 'ꝟ', + 'Ꝡ' => 'ꝡ', + 'Ꝣ' => 'ꝣ', + 'Ꝥ' => 'ꝥ', + 'Ꝧ' => 'ꝧ', + 'Ꝩ' => 'ꝩ', + 'Ꝫ' => 'ꝫ', + 'Ꝭ' => 'ꝭ', + 'Ꝯ' => 'ꝯ', + 'Ꝺ' => 'ꝺ', + 'Ꝼ' => 'ꝼ', + 'Ᵹ' => 'ᵹ', + 'Ꝿ' => 'ꝿ', + 'Ꞁ' => 'ꞁ', + 'Ꞃ' => 'ꞃ', + 'Ꞅ' => 'ꞅ', + 'Ꞇ' => 'ꞇ', + 'Ꞌ' => 'ꞌ', + 'Ɥ' => 'ɥ', + 'Ꞑ' => 'ꞑ', + 'Ꞓ' => 'ꞓ', + 'Ꞗ' => 'ꞗ', + 'Ꞙ' => 'ꞙ', + 'Ꞛ' => 'ꞛ', + 'Ꞝ' => 'ꞝ', + 'Ꞟ' => 'ꞟ', + 'Ꞡ' => 'ꞡ', + 'Ꞣ' => 'ꞣ', + 'Ꞥ' => 'ꞥ', + 'Ꞧ' => 'ꞧ', + 'Ꞩ' => 'ꞩ', + 'Ɦ' => 'ɦ', + 'Ɜ' => 'ɜ', + 'Ɡ' => 'ɡ', + 'Ɬ' => 'ɬ', + 'Ɪ' => 'ɪ', + 'Ʞ' => 'ʞ', + 'Ʇ' => 'ʇ', + 'Ʝ' => 'ʝ', + 'Ꭓ' => 'ꭓ', + 'Ꞵ' => 'ꞵ', + 'Ꞷ' => 'ꞷ', + 'Ꞹ' => 'ꞹ', + 'Ꞻ' => 'ꞻ', + 'Ꞽ' => 'ꞽ', + 'Ꞿ' => 'ꞿ', + 'Ꟃ' => 'ꟃ', + 'Ꞔ' => 'ꞔ', + 'Ʂ' => 'ʂ', + 'Ᶎ' => 'ᶎ', + 'Ꟈ' => 'ꟈ', + 'Ꟊ' => 'ꟊ', + 'Ꟶ' => 'ꟶ', + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + '𐐀' => '𐐨', + '𐐁' => '𐐩', + '𐐂' => '𐐪', + '𐐃' => '𐐫', + '𐐄' => '𐐬', + '𐐅' => '𐐭', + '𐐆' => '𐐮', + '𐐇' => '𐐯', + '𐐈' => '𐐰', + '𐐉' => '𐐱', + '𐐊' => '𐐲', + '𐐋' => '𐐳', + '𐐌' => '𐐴', + '𐐍' => '𐐵', + '𐐎' => '𐐶', + '𐐏' => '𐐷', + '𐐐' => '𐐸', + '𐐑' => '𐐹', + '𐐒' => '𐐺', + '𐐓' => '𐐻', + '𐐔' => '𐐼', + '𐐕' => '𐐽', + '𐐖' => '𐐾', + '𐐗' => '𐐿', + '𐐘' => '𐑀', + '𐐙' => '𐑁', + '𐐚' => '𐑂', + '𐐛' => '𐑃', + '𐐜' => '𐑄', + '𐐝' => '𐑅', + '𐐞' => '𐑆', + '𐐟' => '𐑇', + '𐐠' => '𐑈', + '𐐡' => '𐑉', + '𐐢' => '𐑊', + '𐐣' => '𐑋', + '𐐤' => '𐑌', + '𐐥' => '𐑍', + '𐐦' => '𐑎', + '𐐧' => '𐑏', + '𐒰' => '𐓘', + '𐒱' => '𐓙', + '𐒲' => '𐓚', + '𐒳' => '𐓛', + '𐒴' => '𐓜', + '𐒵' => '𐓝', + '𐒶' => '𐓞', + '𐒷' => '𐓟', + '𐒸' => '𐓠', + '𐒹' => '𐓡', + '𐒺' => '𐓢', + '𐒻' => '𐓣', + '𐒼' => '𐓤', + '𐒽' => '𐓥', + '𐒾' => '𐓦', + '𐒿' => '𐓧', + '𐓀' => '𐓨', + '𐓁' => '𐓩', + '𐓂' => '𐓪', + '𐓃' => '𐓫', + '𐓄' => '𐓬', + '𐓅' => '𐓭', + '𐓆' => '𐓮', + '𐓇' => '𐓯', + '𐓈' => '𐓰', + '𐓉' => '𐓱', + '𐓊' => '𐓲', + '𐓋' => '𐓳', + '𐓌' => '𐓴', + '𐓍' => '𐓵', + '𐓎' => '𐓶', + '𐓏' => '𐓷', + '𐓐' => '𐓸', + '𐓑' => '𐓹', + '𐓒' => '𐓺', + '𐓓' => '𐓻', + '𐲀' => '𐳀', + '𐲁' => '𐳁', + '𐲂' => '𐳂', + '𐲃' => '𐳃', + '𐲄' => '𐳄', + '𐲅' => '𐳅', + '𐲆' => '𐳆', + '𐲇' => '𐳇', + '𐲈' => '𐳈', + '𐲉' => '𐳉', + '𐲊' => '𐳊', + '𐲋' => '𐳋', + '𐲌' => '𐳌', + '𐲍' => '𐳍', + '𐲎' => '𐳎', + '𐲏' => '𐳏', + '𐲐' => '𐳐', + '𐲑' => '𐳑', + '𐲒' => '𐳒', + '𐲓' => '𐳓', + '𐲔' => '𐳔', + '𐲕' => '𐳕', + '𐲖' => '𐳖', + '𐲗' => '𐳗', + '𐲘' => '𐳘', + '𐲙' => '𐳙', + '𐲚' => '𐳚', + '𐲛' => '𐳛', + '𐲜' => '𐳜', + '𐲝' => '𐳝', + '𐲞' => '𐳞', + '𐲟' => '𐳟', + '𐲠' => '𐳠', + '𐲡' => '𐳡', + '𐲢' => '𐳢', + '𐲣' => '𐳣', + '𐲤' => '𐳤', + '𐲥' => '𐳥', + '𐲦' => '𐳦', + '𐲧' => '𐳧', + '𐲨' => '𐳨', + '𐲩' => '𐳩', + '𐲪' => '𐳪', + '𐲫' => '𐳫', + '𐲬' => '𐳬', + '𐲭' => '𐳭', + '𐲮' => '𐳮', + '𐲯' => '𐳯', + '𐲰' => '𐳰', + '𐲱' => '𐳱', + '𐲲' => '𐳲', + '𑢠' => '𑣀', + '𑢡' => '𑣁', + '𑢢' => '𑣂', + '𑢣' => '𑣃', + '𑢤' => '𑣄', + '𑢥' => '𑣅', + '𑢦' => '𑣆', + '𑢧' => '𑣇', + '𑢨' => '𑣈', + '𑢩' => '𑣉', + '𑢪' => '𑣊', + '𑢫' => '𑣋', + '𑢬' => '𑣌', + '𑢭' => '𑣍', + '𑢮' => '𑣎', + '𑢯' => '𑣏', + '𑢰' => '𑣐', + '𑢱' => '𑣑', + '𑢲' => '𑣒', + '𑢳' => '𑣓', + '𑢴' => '𑣔', + '𑢵' => '𑣕', + '𑢶' => '𑣖', + '𑢷' => '𑣗', + '𑢸' => '𑣘', + '𑢹' => '𑣙', + '𑢺' => '𑣚', + '𑢻' => '𑣛', + '𑢼' => '𑣜', + '𑢽' => '𑣝', + '𑢾' => '𑣞', + '𑢿' => '𑣟', + '𖹀' => '𖹠', + '𖹁' => '𖹡', + '𖹂' => '𖹢', + '𖹃' => '𖹣', + '𖹄' => '𖹤', + '𖹅' => '𖹥', + '𖹆' => '𖹦', + '𖹇' => '𖹧', + '𖹈' => '𖹨', + '𖹉' => '𖹩', + '𖹊' => '𖹪', + '𖹋' => '𖹫', + '𖹌' => '𖹬', + '𖹍' => '𖹭', + '𖹎' => '𖹮', + '𖹏' => '𖹯', + '𖹐' => '𖹰', + '𖹑' => '𖹱', + '𖹒' => '𖹲', + '𖹓' => '𖹳', + '𖹔' => '𖹴', + '𖹕' => '𖹵', + '𖹖' => '𖹶', + '𖹗' => '𖹷', + '𖹘' => '𖹸', + '𖹙' => '𖹹', + '𖹚' => '𖹺', + '𖹛' => '𖹻', + '𖹜' => '𖹼', + '𖹝' => '𖹽', + '𖹞' => '𖹾', + '𖹟' => '𖹿', + '𞤀' => '𞤢', + '𞤁' => '𞤣', + '𞤂' => '𞤤', + '𞤃' => '𞤥', + '𞤄' => '𞤦', + '𞤅' => '𞤧', + '𞤆' => '𞤨', + '𞤇' => '𞤩', + '𞤈' => '𞤪', + '𞤉' => '𞤫', + '𞤊' => '𞤬', + '𞤋' => '𞤭', + '𞤌' => '𞤮', + '𞤍' => '𞤯', + '𞤎' => '𞤰', + '𞤏' => '𞤱', + '𞤐' => '𞤲', + '𞤑' => '𞤳', + '𞤒' => '𞤴', + '𞤓' => '𞤵', + '𞤔' => '𞤶', + '𞤕' => '𞤷', + '𞤖' => '𞤸', + '𞤗' => '𞤹', + '𞤘' => '𞤺', + '𞤙' => '𞤻', + '𞤚' => '𞤼', + '𞤛' => '𞤽', + '𞤜' => '𞤾', + '𞤝' => '𞤿', + '𞤞' => '𞥀', + '𞤟' => '𞥁', + '𞤠' => '𞥂', + '𞤡' => '𞥃', +); diff --git a/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php new file mode 100644 index 0000000..2a8f6e7 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php @@ -0,0 +1,5 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + 'µ' => 'Μ', + 'à' => 'À', + 'á' => 'Á', + 'â' => 'Â', + 'ã' => 'Ã', + 'ä' => 'Ä', + 'å' => 'Å', + 'æ' => 'Æ', + 'ç' => 'Ç', + 'è' => 'È', + 'é' => 'É', + 'ê' => 'Ê', + 'ë' => 'Ë', + 'ì' => 'Ì', + 'í' => 'Í', + 'î' => 'Î', + 'ï' => 'Ï', + 'ð' => 'Ð', + 'ñ' => 'Ñ', + 'ò' => 'Ò', + 'ó' => 'Ó', + 'ô' => 'Ô', + 'õ' => 'Õ', + 'ö' => 'Ö', + 'ø' => 'Ø', + 'ù' => 'Ù', + 'ú' => 'Ú', + 'û' => 'Û', + 'ü' => 'Ü', + 'ý' => 'Ý', + 'þ' => 'Þ', + 'ÿ' => 'Ÿ', + 'ā' => 'Ā', + 'ă' => 'Ă', + 'ą' => 'Ą', + 'ć' => 'Ć', + 'ĉ' => 'Ĉ', + 'ċ' => 'Ċ', + 'č' => 'Č', + 'ď' => 'Ď', + 'đ' => 'Đ', + 'ē' => 'Ē', + 'ĕ' => 'Ĕ', + 'ė' => 'Ė', + 'ę' => 'Ę', + 'ě' => 'Ě', + 'ĝ' => 'Ĝ', + 'ğ' => 'Ğ', + 'ġ' => 'Ġ', + 'ģ' => 'Ģ', + 'ĥ' => 'Ĥ', + 'ħ' => 'Ħ', + 'ĩ' => 'Ĩ', + 'ī' => 'Ī', + 'ĭ' => 'Ĭ', + 'į' => 'Į', + 'ı' => 'I', + 'ij' => 'IJ', + 'ĵ' => 'Ĵ', + 'ķ' => 'Ķ', + 'ĺ' => 'Ĺ', + 'ļ' => 'Ļ', + 'ľ' => 'Ľ', + 'ŀ' => 'Ŀ', + 'ł' => 'Ł', + 'ń' => 'Ń', + 'ņ' => 'Ņ', + 'ň' => 'Ň', + 'ŋ' => 'Ŋ', + 'ō' => 'Ō', + 'ŏ' => 'Ŏ', + 'ő' => 'Ő', + 'œ' => 'Œ', + 'ŕ' => 'Ŕ', + 'ŗ' => 'Ŗ', + 'ř' => 'Ř', + 'ś' => 'Ś', + 'ŝ' => 'Ŝ', + 'ş' => 'Ş', + 'š' => 'Š', + 'ţ' => 'Ţ', + 'ť' => 'Ť', + 'ŧ' => 'Ŧ', + 'ũ' => 'Ũ', + 'ū' => 'Ū', + 'ŭ' => 'Ŭ', + 'ů' => 'Ů', + 'ű' => 'Ű', + 'ų' => 'Ų', + 'ŵ' => 'Ŵ', + 'ŷ' => 'Ŷ', + 'ź' => 'Ź', + 'ż' => 'Ż', + 'ž' => 'Ž', + 'ſ' => 'S', + 'ƀ' => 'Ƀ', + 'ƃ' => 'Ƃ', + 'ƅ' => 'Ƅ', + 'ƈ' => 'Ƈ', + 'ƌ' => 'Ƌ', + 'ƒ' => 'Ƒ', + 'ƕ' => 'Ƕ', + 'ƙ' => 'Ƙ', + 'ƚ' => 'Ƚ', + 'ƞ' => 'Ƞ', + 'ơ' => 'Ơ', + 'ƣ' => 'Ƣ', + 'ƥ' => 'Ƥ', + 'ƨ' => 'Ƨ', + 'ƭ' => 'Ƭ', + 'ư' => 'Ư', + 'ƴ' => 'Ƴ', + 'ƶ' => 'Ƶ', + 'ƹ' => 'Ƹ', + 'ƽ' => 'Ƽ', + 'ƿ' => 'Ƿ', + 'Dž' => 'DŽ', + 'dž' => 'DŽ', + 'Lj' => 'LJ', + 'lj' => 'LJ', + 'Nj' => 'NJ', + 'nj' => 'NJ', + 'ǎ' => 'Ǎ', + 'ǐ' => 'Ǐ', + 'ǒ' => 'Ǒ', + 'ǔ' => 'Ǔ', + 'ǖ' => 'Ǖ', + 'ǘ' => 'Ǘ', + 'ǚ' => 'Ǚ', + 'ǜ' => 'Ǜ', + 'ǝ' => 'Ǝ', + 'ǟ' => 'Ǟ', + 'ǡ' => 'Ǡ', + 'ǣ' => 'Ǣ', + 'ǥ' => 'Ǥ', + 'ǧ' => 'Ǧ', + 'ǩ' => 'Ǩ', + 'ǫ' => 'Ǫ', + 'ǭ' => 'Ǭ', + 'ǯ' => 'Ǯ', + 'Dz' => 'DZ', + 'dz' => 'DZ', + 'ǵ' => 'Ǵ', + 'ǹ' => 'Ǹ', + 'ǻ' => 'Ǻ', + 'ǽ' => 'Ǽ', + 'ǿ' => 'Ǿ', + 'ȁ' => 'Ȁ', + 'ȃ' => 'Ȃ', + 'ȅ' => 'Ȅ', + 'ȇ' => 'Ȇ', + 'ȉ' => 'Ȉ', + 'ȋ' => 'Ȋ', + 'ȍ' => 'Ȍ', + 'ȏ' => 'Ȏ', + 'ȑ' => 'Ȑ', + 'ȓ' => 'Ȓ', + 'ȕ' => 'Ȕ', + 'ȗ' => 'Ȗ', + 'ș' => 'Ș', + 'ț' => 'Ț', + 'ȝ' => 'Ȝ', + 'ȟ' => 'Ȟ', + 'ȣ' => 'Ȣ', + 'ȥ' => 'Ȥ', + 'ȧ' => 'Ȧ', + 'ȩ' => 'Ȩ', + 'ȫ' => 'Ȫ', + 'ȭ' => 'Ȭ', + 'ȯ' => 'Ȯ', + 'ȱ' => 'Ȱ', + 'ȳ' => 'Ȳ', + 'ȼ' => 'Ȼ', + 'ȿ' => 'Ȿ', + 'ɀ' => 'Ɀ', + 'ɂ' => 'Ɂ', + 'ɇ' => 'Ɇ', + 'ɉ' => 'Ɉ', + 'ɋ' => 'Ɋ', + 'ɍ' => 'Ɍ', + 'ɏ' => 'Ɏ', + 'ɐ' => 'Ɐ', + 'ɑ' => 'Ɑ', + 'ɒ' => 'Ɒ', + 'ɓ' => 'Ɓ', + 'ɔ' => 'Ɔ', + 'ɖ' => 'Ɖ', + 'ɗ' => 'Ɗ', + 'ə' => 'Ə', + 'ɛ' => 'Ɛ', + 'ɜ' => 'Ɜ', + 'ɠ' => 'Ɠ', + 'ɡ' => 'Ɡ', + 'ɣ' => 'Ɣ', + 'ɥ' => 'Ɥ', + 'ɦ' => 'Ɦ', + 'ɨ' => 'Ɨ', + 'ɩ' => 'Ɩ', + 'ɪ' => 'Ɪ', + 'ɫ' => 'Ɫ', + 'ɬ' => 'Ɬ', + 'ɯ' => 'Ɯ', + 'ɱ' => 'Ɱ', + 'ɲ' => 'Ɲ', + 'ɵ' => 'Ɵ', + 'ɽ' => 'Ɽ', + 'ʀ' => 'Ʀ', + 'ʂ' => 'Ʂ', + 'ʃ' => 'Ʃ', + 'ʇ' => 'Ʇ', + 'ʈ' => 'Ʈ', + 'ʉ' => 'Ʉ', + 'ʊ' => 'Ʊ', + 'ʋ' => 'Ʋ', + 'ʌ' => 'Ʌ', + 'ʒ' => 'Ʒ', + 'ʝ' => 'Ʝ', + 'ʞ' => 'Ʞ', + 'ͅ' => 'Ι', + 'ͱ' => 'Ͱ', + 'ͳ' => 'Ͳ', + 'ͷ' => 'Ͷ', + 'ͻ' => 'Ͻ', + 'ͼ' => 'Ͼ', + 'ͽ' => 'Ͽ', + 'ά' => 'Ά', + 'έ' => 'Έ', + 'ή' => 'Ή', + 'ί' => 'Ί', + 'α' => 'Α', + 'β' => 'Β', + 'γ' => 'Γ', + 'δ' => 'Δ', + 'ε' => 'Ε', + 'ζ' => 'Ζ', + 'η' => 'Η', + 'θ' => 'Θ', + 'ι' => 'Ι', + 'κ' => 'Κ', + 'λ' => 'Λ', + 'μ' => 'Μ', + 'ν' => 'Ν', + 'ξ' => 'Ξ', + 'ο' => 'Ο', + 'π' => 'Π', + 'ρ' => 'Ρ', + 'ς' => 'Σ', + 'σ' => 'Σ', + 'τ' => 'Τ', + 'υ' => 'Υ', + 'φ' => 'Φ', + 'χ' => 'Χ', + 'ψ' => 'Ψ', + 'ω' => 'Ω', + 'ϊ' => 'Ϊ', + 'ϋ' => 'Ϋ', + 'ό' => 'Ό', + 'ύ' => 'Ύ', + 'ώ' => 'Ώ', + 'ϐ' => 'Β', + 'ϑ' => 'Θ', + 'ϕ' => 'Φ', + 'ϖ' => 'Π', + 'ϗ' => 'Ϗ', + 'ϙ' => 'Ϙ', + 'ϛ' => 'Ϛ', + 'ϝ' => 'Ϝ', + 'ϟ' => 'Ϟ', + 'ϡ' => 'Ϡ', + 'ϣ' => 'Ϣ', + 'ϥ' => 'Ϥ', + 'ϧ' => 'Ϧ', + 'ϩ' => 'Ϩ', + 'ϫ' => 'Ϫ', + 'ϭ' => 'Ϭ', + 'ϯ' => 'Ϯ', + 'ϰ' => 'Κ', + 'ϱ' => 'Ρ', + 'ϲ' => 'Ϲ', + 'ϳ' => 'Ϳ', + 'ϵ' => 'Ε', + 'ϸ' => 'Ϸ', + 'ϻ' => 'Ϻ', + 'а' => 'А', + 'б' => 'Б', + 'в' => 'В', + 'г' => 'Г', + 'д' => 'Д', + 'е' => 'Е', + 'ж' => 'Ж', + 'з' => 'З', + 'и' => 'И', + 'й' => 'Й', + 'к' => 'К', + 'л' => 'Л', + 'м' => 'М', + 'н' => 'Н', + 'о' => 'О', + 'п' => 'П', + 'р' => 'Р', + 'с' => 'С', + 'т' => 'Т', + 'у' => 'У', + 'ф' => 'Ф', + 'х' => 'Х', + 'ц' => 'Ц', + 'ч' => 'Ч', + 'ш' => 'Ш', + 'щ' => 'Щ', + 'ъ' => 'Ъ', + 'ы' => 'Ы', + 'ь' => 'Ь', + 'э' => 'Э', + 'ю' => 'Ю', + 'я' => 'Я', + 'ѐ' => 'Ѐ', + 'ё' => 'Ё', + 'ђ' => 'Ђ', + 'ѓ' => 'Ѓ', + 'є' => 'Є', + 'ѕ' => 'Ѕ', + 'і' => 'І', + 'ї' => 'Ї', + 'ј' => 'Ј', + 'љ' => 'Љ', + 'њ' => 'Њ', + 'ћ' => 'Ћ', + 'ќ' => 'Ќ', + 'ѝ' => 'Ѝ', + 'ў' => 'Ў', + 'џ' => 'Џ', + 'ѡ' => 'Ѡ', + 'ѣ' => 'Ѣ', + 'ѥ' => 'Ѥ', + 'ѧ' => 'Ѧ', + 'ѩ' => 'Ѩ', + 'ѫ' => 'Ѫ', + 'ѭ' => 'Ѭ', + 'ѯ' => 'Ѯ', + 'ѱ' => 'Ѱ', + 'ѳ' => 'Ѳ', + 'ѵ' => 'Ѵ', + 'ѷ' => 'Ѷ', + 'ѹ' => 'Ѹ', + 'ѻ' => 'Ѻ', + 'ѽ' => 'Ѽ', + 'ѿ' => 'Ѿ', + 'ҁ' => 'Ҁ', + 'ҋ' => 'Ҋ', + 'ҍ' => 'Ҍ', + 'ҏ' => 'Ҏ', + 'ґ' => 'Ґ', + 'ғ' => 'Ғ', + 'ҕ' => 'Ҕ', + 'җ' => 'Җ', + 'ҙ' => 'Ҙ', + 'қ' => 'Қ', + 'ҝ' => 'Ҝ', + 'ҟ' => 'Ҟ', + 'ҡ' => 'Ҡ', + 'ң' => 'Ң', + 'ҥ' => 'Ҥ', + 'ҧ' => 'Ҧ', + 'ҩ' => 'Ҩ', + 'ҫ' => 'Ҫ', + 'ҭ' => 'Ҭ', + 'ү' => 'Ү', + 'ұ' => 'Ұ', + 'ҳ' => 'Ҳ', + 'ҵ' => 'Ҵ', + 'ҷ' => 'Ҷ', + 'ҹ' => 'Ҹ', + 'һ' => 'Һ', + 'ҽ' => 'Ҽ', + 'ҿ' => 'Ҿ', + 'ӂ' => 'Ӂ', + 'ӄ' => 'Ӄ', + 'ӆ' => 'Ӆ', + 'ӈ' => 'Ӈ', + 'ӊ' => 'Ӊ', + 'ӌ' => 'Ӌ', + 'ӎ' => 'Ӎ', + 'ӏ' => 'Ӏ', + 'ӑ' => 'Ӑ', + 'ӓ' => 'Ӓ', + 'ӕ' => 'Ӕ', + 'ӗ' => 'Ӗ', + 'ә' => 'Ә', + 'ӛ' => 'Ӛ', + 'ӝ' => 'Ӝ', + 'ӟ' => 'Ӟ', + 'ӡ' => 'Ӡ', + 'ӣ' => 'Ӣ', + 'ӥ' => 'Ӥ', + 'ӧ' => 'Ӧ', + 'ө' => 'Ө', + 'ӫ' => 'Ӫ', + 'ӭ' => 'Ӭ', + 'ӯ' => 'Ӯ', + 'ӱ' => 'Ӱ', + 'ӳ' => 'Ӳ', + 'ӵ' => 'Ӵ', + 'ӷ' => 'Ӷ', + 'ӹ' => 'Ӹ', + 'ӻ' => 'Ӻ', + 'ӽ' => 'Ӽ', + 'ӿ' => 'Ӿ', + 'ԁ' => 'Ԁ', + 'ԃ' => 'Ԃ', + 'ԅ' => 'Ԅ', + 'ԇ' => 'Ԇ', + 'ԉ' => 'Ԉ', + 'ԋ' => 'Ԋ', + 'ԍ' => 'Ԍ', + 'ԏ' => 'Ԏ', + 'ԑ' => 'Ԑ', + 'ԓ' => 'Ԓ', + 'ԕ' => 'Ԕ', + 'ԗ' => 'Ԗ', + 'ԙ' => 'Ԙ', + 'ԛ' => 'Ԛ', + 'ԝ' => 'Ԝ', + 'ԟ' => 'Ԟ', + 'ԡ' => 'Ԡ', + 'ԣ' => 'Ԣ', + 'ԥ' => 'Ԥ', + 'ԧ' => 'Ԧ', + 'ԩ' => 'Ԩ', + 'ԫ' => 'Ԫ', + 'ԭ' => 'Ԭ', + 'ԯ' => 'Ԯ', + 'ա' => 'Ա', + 'բ' => 'Բ', + 'գ' => 'Գ', + 'դ' => 'Դ', + 'ե' => 'Ե', + 'զ' => 'Զ', + 'է' => 'Է', + 'ը' => 'Ը', + 'թ' => 'Թ', + 'ժ' => 'Ժ', + 'ի' => 'Ի', + 'լ' => 'Լ', + 'խ' => 'Խ', + 'ծ' => 'Ծ', + 'կ' => 'Կ', + 'հ' => 'Հ', + 'ձ' => 'Ձ', + 'ղ' => 'Ղ', + 'ճ' => 'Ճ', + 'մ' => 'Մ', + 'յ' => 'Յ', + 'ն' => 'Ն', + 'շ' => 'Շ', + 'ո' => 'Ո', + 'չ' => 'Չ', + 'պ' => 'Պ', + 'ջ' => 'Ջ', + 'ռ' => 'Ռ', + 'ս' => 'Ս', + 'վ' => 'Վ', + 'տ' => 'Տ', + 'ր' => 'Ր', + 'ց' => 'Ց', + 'ւ' => 'Ւ', + 'փ' => 'Փ', + 'ք' => 'Ք', + 'օ' => 'Օ', + 'ֆ' => 'Ֆ', + 'ა' => 'Ა', + 'ბ' => 'Ბ', + 'გ' => 'Გ', + 'დ' => 'Დ', + 'ე' => 'Ე', + 'ვ' => 'Ვ', + 'ზ' => 'Ზ', + 'თ' => 'Თ', + 'ი' => 'Ი', + 'კ' => 'Კ', + 'ლ' => 'Ლ', + 'მ' => 'Მ', + 'ნ' => 'Ნ', + 'ო' => 'Ო', + 'პ' => 'Პ', + 'ჟ' => 'Ჟ', + 'რ' => 'Რ', + 'ს' => 'Ს', + 'ტ' => 'Ტ', + 'უ' => 'Უ', + 'ფ' => 'Ფ', + 'ქ' => 'Ქ', + 'ღ' => 'Ღ', + 'ყ' => 'Ყ', + 'შ' => 'Შ', + 'ჩ' => 'Ჩ', + 'ც' => 'Ც', + 'ძ' => 'Ძ', + 'წ' => 'Წ', + 'ჭ' => 'Ჭ', + 'ხ' => 'Ხ', + 'ჯ' => 'Ჯ', + 'ჰ' => 'Ჰ', + 'ჱ' => 'Ჱ', + 'ჲ' => 'Ჲ', + 'ჳ' => 'Ჳ', + 'ჴ' => 'Ჴ', + 'ჵ' => 'Ჵ', + 'ჶ' => 'Ჶ', + 'ჷ' => 'Ჷ', + 'ჸ' => 'Ჸ', + 'ჹ' => 'Ჹ', + 'ჺ' => 'Ჺ', + 'ჽ' => 'Ჽ', + 'ჾ' => 'Ჾ', + 'ჿ' => 'Ჿ', + 'ᏸ' => 'Ᏸ', + 'ᏹ' => 'Ᏹ', + 'ᏺ' => 'Ᏺ', + 'ᏻ' => 'Ᏻ', + 'ᏼ' => 'Ᏼ', + 'ᏽ' => 'Ᏽ', + 'ᲀ' => 'В', + 'ᲁ' => 'Д', + 'ᲂ' => 'О', + 'ᲃ' => 'С', + 'ᲄ' => 'Т', + 'ᲅ' => 'Т', + 'ᲆ' => 'Ъ', + 'ᲇ' => 'Ѣ', + 'ᲈ' => 'Ꙋ', + 'ᵹ' => 'Ᵹ', + 'ᵽ' => 'Ᵽ', + 'ᶎ' => 'Ᶎ', + 'ḁ' => 'Ḁ', + 'ḃ' => 'Ḃ', + 'ḅ' => 'Ḅ', + 'ḇ' => 'Ḇ', + 'ḉ' => 'Ḉ', + 'ḋ' => 'Ḋ', + 'ḍ' => 'Ḍ', + 'ḏ' => 'Ḏ', + 'ḑ' => 'Ḑ', + 'ḓ' => 'Ḓ', + 'ḕ' => 'Ḕ', + 'ḗ' => 'Ḗ', + 'ḙ' => 'Ḙ', + 'ḛ' => 'Ḛ', + 'ḝ' => 'Ḝ', + 'ḟ' => 'Ḟ', + 'ḡ' => 'Ḡ', + 'ḣ' => 'Ḣ', + 'ḥ' => 'Ḥ', + 'ḧ' => 'Ḧ', + 'ḩ' => 'Ḩ', + 'ḫ' => 'Ḫ', + 'ḭ' => 'Ḭ', + 'ḯ' => 'Ḯ', + 'ḱ' => 'Ḱ', + 'ḳ' => 'Ḳ', + 'ḵ' => 'Ḵ', + 'ḷ' => 'Ḷ', + 'ḹ' => 'Ḹ', + 'ḻ' => 'Ḻ', + 'ḽ' => 'Ḽ', + 'ḿ' => 'Ḿ', + 'ṁ' => 'Ṁ', + 'ṃ' => 'Ṃ', + 'ṅ' => 'Ṅ', + 'ṇ' => 'Ṇ', + 'ṉ' => 'Ṉ', + 'ṋ' => 'Ṋ', + 'ṍ' => 'Ṍ', + 'ṏ' => 'Ṏ', + 'ṑ' => 'Ṑ', + 'ṓ' => 'Ṓ', + 'ṕ' => 'Ṕ', + 'ṗ' => 'Ṗ', + 'ṙ' => 'Ṙ', + 'ṛ' => 'Ṛ', + 'ṝ' => 'Ṝ', + 'ṟ' => 'Ṟ', + 'ṡ' => 'Ṡ', + 'ṣ' => 'Ṣ', + 'ṥ' => 'Ṥ', + 'ṧ' => 'Ṧ', + 'ṩ' => 'Ṩ', + 'ṫ' => 'Ṫ', + 'ṭ' => 'Ṭ', + 'ṯ' => 'Ṯ', + 'ṱ' => 'Ṱ', + 'ṳ' => 'Ṳ', + 'ṵ' => 'Ṵ', + 'ṷ' => 'Ṷ', + 'ṹ' => 'Ṹ', + 'ṻ' => 'Ṻ', + 'ṽ' => 'Ṽ', + 'ṿ' => 'Ṿ', + 'ẁ' => 'Ẁ', + 'ẃ' => 'Ẃ', + 'ẅ' => 'Ẅ', + 'ẇ' => 'Ẇ', + 'ẉ' => 'Ẉ', + 'ẋ' => 'Ẋ', + 'ẍ' => 'Ẍ', + 'ẏ' => 'Ẏ', + 'ẑ' => 'Ẑ', + 'ẓ' => 'Ẓ', + 'ẕ' => 'Ẕ', + 'ẛ' => 'Ṡ', + 'ạ' => 'Ạ', + 'ả' => 'Ả', + 'ấ' => 'Ấ', + 'ầ' => 'Ầ', + 'ẩ' => 'Ẩ', + 'ẫ' => 'Ẫ', + 'ậ' => 'Ậ', + 'ắ' => 'Ắ', + 'ằ' => 'Ằ', + 'ẳ' => 'Ẳ', + 'ẵ' => 'Ẵ', + 'ặ' => 'Ặ', + 'ẹ' => 'Ẹ', + 'ẻ' => 'Ẻ', + 'ẽ' => 'Ẽ', + 'ế' => 'Ế', + 'ề' => 'Ề', + 'ể' => 'Ể', + 'ễ' => 'Ễ', + 'ệ' => 'Ệ', + 'ỉ' => 'Ỉ', + 'ị' => 'Ị', + 'ọ' => 'Ọ', + 'ỏ' => 'Ỏ', + 'ố' => 'Ố', + 'ồ' => 'Ồ', + 'ổ' => 'Ổ', + 'ỗ' => 'Ỗ', + 'ộ' => 'Ộ', + 'ớ' => 'Ớ', + 'ờ' => 'Ờ', + 'ở' => 'Ở', + 'ỡ' => 'Ỡ', + 'ợ' => 'Ợ', + 'ụ' => 'Ụ', + 'ủ' => 'Ủ', + 'ứ' => 'Ứ', + 'ừ' => 'Ừ', + 'ử' => 'Ử', + 'ữ' => 'Ữ', + 'ự' => 'Ự', + 'ỳ' => 'Ỳ', + 'ỵ' => 'Ỵ', + 'ỷ' => 'Ỷ', + 'ỹ' => 'Ỹ', + 'ỻ' => 'Ỻ', + 'ỽ' => 'Ỽ', + 'ỿ' => 'Ỿ', + 'ἀ' => 'Ἀ', + 'ἁ' => 'Ἁ', + 'ἂ' => 'Ἂ', + 'ἃ' => 'Ἃ', + 'ἄ' => 'Ἄ', + 'ἅ' => 'Ἅ', + 'ἆ' => 'Ἆ', + 'ἇ' => 'Ἇ', + 'ἐ' => 'Ἐ', + 'ἑ' => 'Ἑ', + 'ἒ' => 'Ἒ', + 'ἓ' => 'Ἓ', + 'ἔ' => 'Ἔ', + 'ἕ' => 'Ἕ', + 'ἠ' => 'Ἠ', + 'ἡ' => 'Ἡ', + 'ἢ' => 'Ἢ', + 'ἣ' => 'Ἣ', + 'ἤ' => 'Ἤ', + 'ἥ' => 'Ἥ', + 'ἦ' => 'Ἦ', + 'ἧ' => 'Ἧ', + 'ἰ' => 'Ἰ', + 'ἱ' => 'Ἱ', + 'ἲ' => 'Ἲ', + 'ἳ' => 'Ἳ', + 'ἴ' => 'Ἴ', + 'ἵ' => 'Ἵ', + 'ἶ' => 'Ἶ', + 'ἷ' => 'Ἷ', + 'ὀ' => 'Ὀ', + 'ὁ' => 'Ὁ', + 'ὂ' => 'Ὂ', + 'ὃ' => 'Ὃ', + 'ὄ' => 'Ὄ', + 'ὅ' => 'Ὅ', + 'ὑ' => 'Ὑ', + 'ὓ' => 'Ὓ', + 'ὕ' => 'Ὕ', + 'ὗ' => 'Ὗ', + 'ὠ' => 'Ὠ', + 'ὡ' => 'Ὡ', + 'ὢ' => 'Ὢ', + 'ὣ' => 'Ὣ', + 'ὤ' => 'Ὤ', + 'ὥ' => 'Ὥ', + 'ὦ' => 'Ὦ', + 'ὧ' => 'Ὧ', + 'ὰ' => 'Ὰ', + 'ά' => 'Ά', + 'ὲ' => 'Ὲ', + 'έ' => 'Έ', + 'ὴ' => 'Ὴ', + 'ή' => 'Ή', + 'ὶ' => 'Ὶ', + 'ί' => 'Ί', + 'ὸ' => 'Ὸ', + 'ό' => 'Ό', + 'ὺ' => 'Ὺ', + 'ύ' => 'Ύ', + 'ὼ' => 'Ὼ', + 'ώ' => 'Ώ', + 'ᾀ' => 'ἈΙ', + 'ᾁ' => 'ἉΙ', + 'ᾂ' => 'ἊΙ', + 'ᾃ' => 'ἋΙ', + 'ᾄ' => 'ἌΙ', + 'ᾅ' => 'ἍΙ', + 'ᾆ' => 'ἎΙ', + 'ᾇ' => 'ἏΙ', + 'ᾐ' => 'ἨΙ', + 'ᾑ' => 'ἩΙ', + 'ᾒ' => 'ἪΙ', + 'ᾓ' => 'ἫΙ', + 'ᾔ' => 'ἬΙ', + 'ᾕ' => 'ἭΙ', + 'ᾖ' => 'ἮΙ', + 'ᾗ' => 'ἯΙ', + 'ᾠ' => 'ὨΙ', + 'ᾡ' => 'ὩΙ', + 'ᾢ' => 'ὪΙ', + 'ᾣ' => 'ὫΙ', + 'ᾤ' => 'ὬΙ', + 'ᾥ' => 'ὭΙ', + 'ᾦ' => 'ὮΙ', + 'ᾧ' => 'ὯΙ', + 'ᾰ' => 'Ᾰ', + 'ᾱ' => 'Ᾱ', + 'ᾳ' => 'ΑΙ', + 'ι' => 'Ι', + 'ῃ' => 'ΗΙ', + 'ῐ' => 'Ῐ', + 'ῑ' => 'Ῑ', + 'ῠ' => 'Ῠ', + 'ῡ' => 'Ῡ', + 'ῥ' => 'Ῥ', + 'ῳ' => 'ΩΙ', + 'ⅎ' => 'Ⅎ', + 'ⅰ' => 'Ⅰ', + 'ⅱ' => 'Ⅱ', + 'ⅲ' => 'Ⅲ', + 'ⅳ' => 'Ⅳ', + 'ⅴ' => 'Ⅴ', + 'ⅵ' => 'Ⅵ', + 'ⅶ' => 'Ⅶ', + 'ⅷ' => 'Ⅷ', + 'ⅸ' => 'Ⅸ', + 'ⅹ' => 'Ⅹ', + 'ⅺ' => 'Ⅺ', + 'ⅻ' => 'Ⅻ', + 'ⅼ' => 'Ⅼ', + 'ⅽ' => 'Ⅽ', + 'ⅾ' => 'Ⅾ', + 'ⅿ' => 'Ⅿ', + 'ↄ' => 'Ↄ', + 'ⓐ' => 'Ⓐ', + 'ⓑ' => 'Ⓑ', + 'ⓒ' => 'Ⓒ', + 'ⓓ' => 'Ⓓ', + 'ⓔ' => 'Ⓔ', + 'ⓕ' => 'Ⓕ', + 'ⓖ' => 'Ⓖ', + 'ⓗ' => 'Ⓗ', + 'ⓘ' => 'Ⓘ', + 'ⓙ' => 'Ⓙ', + 'ⓚ' => 'Ⓚ', + 'ⓛ' => 'Ⓛ', + 'ⓜ' => 'Ⓜ', + 'ⓝ' => 'Ⓝ', + 'ⓞ' => 'Ⓞ', + 'ⓟ' => 'Ⓟ', + 'ⓠ' => 'Ⓠ', + 'ⓡ' => 'Ⓡ', + 'ⓢ' => 'Ⓢ', + 'ⓣ' => 'Ⓣ', + 'ⓤ' => 'Ⓤ', + 'ⓥ' => 'Ⓥ', + 'ⓦ' => 'Ⓦ', + 'ⓧ' => 'Ⓧ', + 'ⓨ' => 'Ⓨ', + 'ⓩ' => 'Ⓩ', + 'ⰰ' => 'Ⰰ', + 'ⰱ' => 'Ⰱ', + 'ⰲ' => 'Ⰲ', + 'ⰳ' => 'Ⰳ', + 'ⰴ' => 'Ⰴ', + 'ⰵ' => 'Ⰵ', + 'ⰶ' => 'Ⰶ', + 'ⰷ' => 'Ⰷ', + 'ⰸ' => 'Ⰸ', + 'ⰹ' => 'Ⰹ', + 'ⰺ' => 'Ⰺ', + 'ⰻ' => 'Ⰻ', + 'ⰼ' => 'Ⰼ', + 'ⰽ' => 'Ⰽ', + 'ⰾ' => 'Ⰾ', + 'ⰿ' => 'Ⰿ', + 'ⱀ' => 'Ⱀ', + 'ⱁ' => 'Ⱁ', + 'ⱂ' => 'Ⱂ', + 'ⱃ' => 'Ⱃ', + 'ⱄ' => 'Ⱄ', + 'ⱅ' => 'Ⱅ', + 'ⱆ' => 'Ⱆ', + 'ⱇ' => 'Ⱇ', + 'ⱈ' => 'Ⱈ', + 'ⱉ' => 'Ⱉ', + 'ⱊ' => 'Ⱊ', + 'ⱋ' => 'Ⱋ', + 'ⱌ' => 'Ⱌ', + 'ⱍ' => 'Ⱍ', + 'ⱎ' => 'Ⱎ', + 'ⱏ' => 'Ⱏ', + 'ⱐ' => 'Ⱐ', + 'ⱑ' => 'Ⱑ', + 'ⱒ' => 'Ⱒ', + 'ⱓ' => 'Ⱓ', + 'ⱔ' => 'Ⱔ', + 'ⱕ' => 'Ⱕ', + 'ⱖ' => 'Ⱖ', + 'ⱗ' => 'Ⱗ', + 'ⱘ' => 'Ⱘ', + 'ⱙ' => 'Ⱙ', + 'ⱚ' => 'Ⱚ', + 'ⱛ' => 'Ⱛ', + 'ⱜ' => 'Ⱜ', + 'ⱝ' => 'Ⱝ', + 'ⱞ' => 'Ⱞ', + 'ⱡ' => 'Ⱡ', + 'ⱥ' => 'Ⱥ', + 'ⱦ' => 'Ⱦ', + 'ⱨ' => 'Ⱨ', + 'ⱪ' => 'Ⱪ', + 'ⱬ' => 'Ⱬ', + 'ⱳ' => 'Ⱳ', + 'ⱶ' => 'Ⱶ', + 'ⲁ' => 'Ⲁ', + 'ⲃ' => 'Ⲃ', + 'ⲅ' => 'Ⲅ', + 'ⲇ' => 'Ⲇ', + 'ⲉ' => 'Ⲉ', + 'ⲋ' => 'Ⲋ', + 'ⲍ' => 'Ⲍ', + 'ⲏ' => 'Ⲏ', + 'ⲑ' => 'Ⲑ', + 'ⲓ' => 'Ⲓ', + 'ⲕ' => 'Ⲕ', + 'ⲗ' => 'Ⲗ', + 'ⲙ' => 'Ⲙ', + 'ⲛ' => 'Ⲛ', + 'ⲝ' => 'Ⲝ', + 'ⲟ' => 'Ⲟ', + 'ⲡ' => 'Ⲡ', + 'ⲣ' => 'Ⲣ', + 'ⲥ' => 'Ⲥ', + 'ⲧ' => 'Ⲧ', + 'ⲩ' => 'Ⲩ', + 'ⲫ' => 'Ⲫ', + 'ⲭ' => 'Ⲭ', + 'ⲯ' => 'Ⲯ', + 'ⲱ' => 'Ⲱ', + 'ⲳ' => 'Ⲳ', + 'ⲵ' => 'Ⲵ', + 'ⲷ' => 'Ⲷ', + 'ⲹ' => 'Ⲹ', + 'ⲻ' => 'Ⲻ', + 'ⲽ' => 'Ⲽ', + 'ⲿ' => 'Ⲿ', + 'ⳁ' => 'Ⳁ', + 'ⳃ' => 'Ⳃ', + 'ⳅ' => 'Ⳅ', + 'ⳇ' => 'Ⳇ', + 'ⳉ' => 'Ⳉ', + 'ⳋ' => 'Ⳋ', + 'ⳍ' => 'Ⳍ', + 'ⳏ' => 'Ⳏ', + 'ⳑ' => 'Ⳑ', + 'ⳓ' => 'Ⳓ', + 'ⳕ' => 'Ⳕ', + 'ⳗ' => 'Ⳗ', + 'ⳙ' => 'Ⳙ', + 'ⳛ' => 'Ⳛ', + 'ⳝ' => 'Ⳝ', + 'ⳟ' => 'Ⳟ', + 'ⳡ' => 'Ⳡ', + 'ⳣ' => 'Ⳣ', + 'ⳬ' => 'Ⳬ', + 'ⳮ' => 'Ⳮ', + 'ⳳ' => 'Ⳳ', + 'ⴀ' => 'Ⴀ', + 'ⴁ' => 'Ⴁ', + 'ⴂ' => 'Ⴂ', + 'ⴃ' => 'Ⴃ', + 'ⴄ' => 'Ⴄ', + 'ⴅ' => 'Ⴅ', + 'ⴆ' => 'Ⴆ', + 'ⴇ' => 'Ⴇ', + 'ⴈ' => 'Ⴈ', + 'ⴉ' => 'Ⴉ', + 'ⴊ' => 'Ⴊ', + 'ⴋ' => 'Ⴋ', + 'ⴌ' => 'Ⴌ', + 'ⴍ' => 'Ⴍ', + 'ⴎ' => 'Ⴎ', + 'ⴏ' => 'Ⴏ', + 'ⴐ' => 'Ⴐ', + 'ⴑ' => 'Ⴑ', + 'ⴒ' => 'Ⴒ', + 'ⴓ' => 'Ⴓ', + 'ⴔ' => 'Ⴔ', + 'ⴕ' => 'Ⴕ', + 'ⴖ' => 'Ⴖ', + 'ⴗ' => 'Ⴗ', + 'ⴘ' => 'Ⴘ', + 'ⴙ' => 'Ⴙ', + 'ⴚ' => 'Ⴚ', + 'ⴛ' => 'Ⴛ', + 'ⴜ' => 'Ⴜ', + 'ⴝ' => 'Ⴝ', + 'ⴞ' => 'Ⴞ', + 'ⴟ' => 'Ⴟ', + 'ⴠ' => 'Ⴠ', + 'ⴡ' => 'Ⴡ', + 'ⴢ' => 'Ⴢ', + 'ⴣ' => 'Ⴣ', + 'ⴤ' => 'Ⴤ', + 'ⴥ' => 'Ⴥ', + 'ⴧ' => 'Ⴧ', + 'ⴭ' => 'Ⴭ', + 'ꙁ' => 'Ꙁ', + 'ꙃ' => 'Ꙃ', + 'ꙅ' => 'Ꙅ', + 'ꙇ' => 'Ꙇ', + 'ꙉ' => 'Ꙉ', + 'ꙋ' => 'Ꙋ', + 'ꙍ' => 'Ꙍ', + 'ꙏ' => 'Ꙏ', + 'ꙑ' => 'Ꙑ', + 'ꙓ' => 'Ꙓ', + 'ꙕ' => 'Ꙕ', + 'ꙗ' => 'Ꙗ', + 'ꙙ' => 'Ꙙ', + 'ꙛ' => 'Ꙛ', + 'ꙝ' => 'Ꙝ', + 'ꙟ' => 'Ꙟ', + 'ꙡ' => 'Ꙡ', + 'ꙣ' => 'Ꙣ', + 'ꙥ' => 'Ꙥ', + 'ꙧ' => 'Ꙧ', + 'ꙩ' => 'Ꙩ', + 'ꙫ' => 'Ꙫ', + 'ꙭ' => 'Ꙭ', + 'ꚁ' => 'Ꚁ', + 'ꚃ' => 'Ꚃ', + 'ꚅ' => 'Ꚅ', + 'ꚇ' => 'Ꚇ', + 'ꚉ' => 'Ꚉ', + 'ꚋ' => 'Ꚋ', + 'ꚍ' => 'Ꚍ', + 'ꚏ' => 'Ꚏ', + 'ꚑ' => 'Ꚑ', + 'ꚓ' => 'Ꚓ', + 'ꚕ' => 'Ꚕ', + 'ꚗ' => 'Ꚗ', + 'ꚙ' => 'Ꚙ', + 'ꚛ' => 'Ꚛ', + 'ꜣ' => 'Ꜣ', + 'ꜥ' => 'Ꜥ', + 'ꜧ' => 'Ꜧ', + 'ꜩ' => 'Ꜩ', + 'ꜫ' => 'Ꜫ', + 'ꜭ' => 'Ꜭ', + 'ꜯ' => 'Ꜯ', + 'ꜳ' => 'Ꜳ', + 'ꜵ' => 'Ꜵ', + 'ꜷ' => 'Ꜷ', + 'ꜹ' => 'Ꜹ', + 'ꜻ' => 'Ꜻ', + 'ꜽ' => 'Ꜽ', + 'ꜿ' => 'Ꜿ', + 'ꝁ' => 'Ꝁ', + 'ꝃ' => 'Ꝃ', + 'ꝅ' => 'Ꝅ', + 'ꝇ' => 'Ꝇ', + 'ꝉ' => 'Ꝉ', + 'ꝋ' => 'Ꝋ', + 'ꝍ' => 'Ꝍ', + 'ꝏ' => 'Ꝏ', + 'ꝑ' => 'Ꝑ', + 'ꝓ' => 'Ꝓ', + 'ꝕ' => 'Ꝕ', + 'ꝗ' => 'Ꝗ', + 'ꝙ' => 'Ꝙ', + 'ꝛ' => 'Ꝛ', + 'ꝝ' => 'Ꝝ', + 'ꝟ' => 'Ꝟ', + 'ꝡ' => 'Ꝡ', + 'ꝣ' => 'Ꝣ', + 'ꝥ' => 'Ꝥ', + 'ꝧ' => 'Ꝧ', + 'ꝩ' => 'Ꝩ', + 'ꝫ' => 'Ꝫ', + 'ꝭ' => 'Ꝭ', + 'ꝯ' => 'Ꝯ', + 'ꝺ' => 'Ꝺ', + 'ꝼ' => 'Ꝼ', + 'ꝿ' => 'Ꝿ', + 'ꞁ' => 'Ꞁ', + 'ꞃ' => 'Ꞃ', + 'ꞅ' => 'Ꞅ', + 'ꞇ' => 'Ꞇ', + 'ꞌ' => 'Ꞌ', + 'ꞑ' => 'Ꞑ', + 'ꞓ' => 'Ꞓ', + 'ꞔ' => 'Ꞔ', + 'ꞗ' => 'Ꞗ', + 'ꞙ' => 'Ꞙ', + 'ꞛ' => 'Ꞛ', + 'ꞝ' => 'Ꞝ', + 'ꞟ' => 'Ꞟ', + 'ꞡ' => 'Ꞡ', + 'ꞣ' => 'Ꞣ', + 'ꞥ' => 'Ꞥ', + 'ꞧ' => 'Ꞧ', + 'ꞩ' => 'Ꞩ', + 'ꞵ' => 'Ꞵ', + 'ꞷ' => 'Ꞷ', + 'ꞹ' => 'Ꞹ', + 'ꞻ' => 'Ꞻ', + 'ꞽ' => 'Ꞽ', + 'ꞿ' => 'Ꞿ', + 'ꟃ' => 'Ꟃ', + 'ꟈ' => 'Ꟈ', + 'ꟊ' => 'Ꟊ', + 'ꟶ' => 'Ꟶ', + 'ꭓ' => 'Ꭓ', + 'ꭰ' => 'Ꭰ', + 'ꭱ' => 'Ꭱ', + 'ꭲ' => 'Ꭲ', + 'ꭳ' => 'Ꭳ', + 'ꭴ' => 'Ꭴ', + 'ꭵ' => 'Ꭵ', + 'ꭶ' => 'Ꭶ', + 'ꭷ' => 'Ꭷ', + 'ꭸ' => 'Ꭸ', + 'ꭹ' => 'Ꭹ', + 'ꭺ' => 'Ꭺ', + 'ꭻ' => 'Ꭻ', + 'ꭼ' => 'Ꭼ', + 'ꭽ' => 'Ꭽ', + 'ꭾ' => 'Ꭾ', + 'ꭿ' => 'Ꭿ', + 'ꮀ' => 'Ꮀ', + 'ꮁ' => 'Ꮁ', + 'ꮂ' => 'Ꮂ', + 'ꮃ' => 'Ꮃ', + 'ꮄ' => 'Ꮄ', + 'ꮅ' => 'Ꮅ', + 'ꮆ' => 'Ꮆ', + 'ꮇ' => 'Ꮇ', + 'ꮈ' => 'Ꮈ', + 'ꮉ' => 'Ꮉ', + 'ꮊ' => 'Ꮊ', + 'ꮋ' => 'Ꮋ', + 'ꮌ' => 'Ꮌ', + 'ꮍ' => 'Ꮍ', + 'ꮎ' => 'Ꮎ', + 'ꮏ' => 'Ꮏ', + 'ꮐ' => 'Ꮐ', + 'ꮑ' => 'Ꮑ', + 'ꮒ' => 'Ꮒ', + 'ꮓ' => 'Ꮓ', + 'ꮔ' => 'Ꮔ', + 'ꮕ' => 'Ꮕ', + 'ꮖ' => 'Ꮖ', + 'ꮗ' => 'Ꮗ', + 'ꮘ' => 'Ꮘ', + 'ꮙ' => 'Ꮙ', + 'ꮚ' => 'Ꮚ', + 'ꮛ' => 'Ꮛ', + 'ꮜ' => 'Ꮜ', + 'ꮝ' => 'Ꮝ', + 'ꮞ' => 'Ꮞ', + 'ꮟ' => 'Ꮟ', + 'ꮠ' => 'Ꮠ', + 'ꮡ' => 'Ꮡ', + 'ꮢ' => 'Ꮢ', + 'ꮣ' => 'Ꮣ', + 'ꮤ' => 'Ꮤ', + 'ꮥ' => 'Ꮥ', + 'ꮦ' => 'Ꮦ', + 'ꮧ' => 'Ꮧ', + 'ꮨ' => 'Ꮨ', + 'ꮩ' => 'Ꮩ', + 'ꮪ' => 'Ꮪ', + 'ꮫ' => 'Ꮫ', + 'ꮬ' => 'Ꮬ', + 'ꮭ' => 'Ꮭ', + 'ꮮ' => 'Ꮮ', + 'ꮯ' => 'Ꮯ', + 'ꮰ' => 'Ꮰ', + 'ꮱ' => 'Ꮱ', + 'ꮲ' => 'Ꮲ', + 'ꮳ' => 'Ꮳ', + 'ꮴ' => 'Ꮴ', + 'ꮵ' => 'Ꮵ', + 'ꮶ' => 'Ꮶ', + 'ꮷ' => 'Ꮷ', + 'ꮸ' => 'Ꮸ', + 'ꮹ' => 'Ꮹ', + 'ꮺ' => 'Ꮺ', + 'ꮻ' => 'Ꮻ', + 'ꮼ' => 'Ꮼ', + 'ꮽ' => 'Ꮽ', + 'ꮾ' => 'Ꮾ', + 'ꮿ' => 'Ꮿ', + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + '𐐨' => '𐐀', + '𐐩' => '𐐁', + '𐐪' => '𐐂', + '𐐫' => '𐐃', + '𐐬' => '𐐄', + '𐐭' => '𐐅', + '𐐮' => '𐐆', + '𐐯' => '𐐇', + '𐐰' => '𐐈', + '𐐱' => '𐐉', + '𐐲' => '𐐊', + '𐐳' => '𐐋', + '𐐴' => '𐐌', + '𐐵' => '𐐍', + '𐐶' => '𐐎', + '𐐷' => '𐐏', + '𐐸' => '𐐐', + '𐐹' => '𐐑', + '𐐺' => '𐐒', + '𐐻' => '𐐓', + '𐐼' => '𐐔', + '𐐽' => '𐐕', + '𐐾' => '𐐖', + '𐐿' => '𐐗', + '𐑀' => '𐐘', + '𐑁' => '𐐙', + '𐑂' => '𐐚', + '𐑃' => '𐐛', + '𐑄' => '𐐜', + '𐑅' => '𐐝', + '𐑆' => '𐐞', + '𐑇' => '𐐟', + '𐑈' => '𐐠', + '𐑉' => '𐐡', + '𐑊' => '𐐢', + '𐑋' => '𐐣', + '𐑌' => '𐐤', + '𐑍' => '𐐥', + '𐑎' => '𐐦', + '𐑏' => '𐐧', + '𐓘' => '𐒰', + '𐓙' => '𐒱', + '𐓚' => '𐒲', + '𐓛' => '𐒳', + '𐓜' => '𐒴', + '𐓝' => '𐒵', + '𐓞' => '𐒶', + '𐓟' => '𐒷', + '𐓠' => '𐒸', + '𐓡' => '𐒹', + '𐓢' => '𐒺', + '𐓣' => '𐒻', + '𐓤' => '𐒼', + '𐓥' => '𐒽', + '𐓦' => '𐒾', + '𐓧' => '𐒿', + '𐓨' => '𐓀', + '𐓩' => '𐓁', + '𐓪' => '𐓂', + '𐓫' => '𐓃', + '𐓬' => '𐓄', + '𐓭' => '𐓅', + '𐓮' => '𐓆', + '𐓯' => '𐓇', + '𐓰' => '𐓈', + '𐓱' => '𐓉', + '𐓲' => '𐓊', + '𐓳' => '𐓋', + '𐓴' => '𐓌', + '𐓵' => '𐓍', + '𐓶' => '𐓎', + '𐓷' => '𐓏', + '𐓸' => '𐓐', + '𐓹' => '𐓑', + '𐓺' => '𐓒', + '𐓻' => '𐓓', + '𐳀' => '𐲀', + '𐳁' => '𐲁', + '𐳂' => '𐲂', + '𐳃' => '𐲃', + '𐳄' => '𐲄', + '𐳅' => '𐲅', + '𐳆' => '𐲆', + '𐳇' => '𐲇', + '𐳈' => '𐲈', + '𐳉' => '𐲉', + '𐳊' => '𐲊', + '𐳋' => '𐲋', + '𐳌' => '𐲌', + '𐳍' => '𐲍', + '𐳎' => '𐲎', + '𐳏' => '𐲏', + '𐳐' => '𐲐', + '𐳑' => '𐲑', + '𐳒' => '𐲒', + '𐳓' => '𐲓', + '𐳔' => '𐲔', + '𐳕' => '𐲕', + '𐳖' => '𐲖', + '𐳗' => '𐲗', + '𐳘' => '𐲘', + '𐳙' => '𐲙', + '𐳚' => '𐲚', + '𐳛' => '𐲛', + '𐳜' => '𐲜', + '𐳝' => '𐲝', + '𐳞' => '𐲞', + '𐳟' => '𐲟', + '𐳠' => '𐲠', + '𐳡' => '𐲡', + '𐳢' => '𐲢', + '𐳣' => '𐲣', + '𐳤' => '𐲤', + '𐳥' => '𐲥', + '𐳦' => '𐲦', + '𐳧' => '𐲧', + '𐳨' => '𐲨', + '𐳩' => '𐲩', + '𐳪' => '𐲪', + '𐳫' => '𐲫', + '𐳬' => '𐲬', + '𐳭' => '𐲭', + '𐳮' => '𐲮', + '𐳯' => '𐲯', + '𐳰' => '𐲰', + '𐳱' => '𐲱', + '𐳲' => '𐲲', + '𑣀' => '𑢠', + '𑣁' => '𑢡', + '𑣂' => '𑢢', + '𑣃' => '𑢣', + '𑣄' => '𑢤', + '𑣅' => '𑢥', + '𑣆' => '𑢦', + '𑣇' => '𑢧', + '𑣈' => '𑢨', + '𑣉' => '𑢩', + '𑣊' => '𑢪', + '𑣋' => '𑢫', + '𑣌' => '𑢬', + '𑣍' => '𑢭', + '𑣎' => '𑢮', + '𑣏' => '𑢯', + '𑣐' => '𑢰', + '𑣑' => '𑢱', + '𑣒' => '𑢲', + '𑣓' => '𑢳', + '𑣔' => '𑢴', + '𑣕' => '𑢵', + '𑣖' => '𑢶', + '𑣗' => '𑢷', + '𑣘' => '𑢸', + '𑣙' => '𑢹', + '𑣚' => '𑢺', + '𑣛' => '𑢻', + '𑣜' => '𑢼', + '𑣝' => '𑢽', + '𑣞' => '𑢾', + '𑣟' => '𑢿', + '𖹠' => '𖹀', + '𖹡' => '𖹁', + '𖹢' => '𖹂', + '𖹣' => '𖹃', + '𖹤' => '𖹄', + '𖹥' => '𖹅', + '𖹦' => '𖹆', + '𖹧' => '𖹇', + '𖹨' => '𖹈', + '𖹩' => '𖹉', + '𖹪' => '𖹊', + '𖹫' => '𖹋', + '𖹬' => '𖹌', + '𖹭' => '𖹍', + '𖹮' => '𖹎', + '𖹯' => '𖹏', + '𖹰' => '𖹐', + '𖹱' => '𖹑', + '𖹲' => '𖹒', + '𖹳' => '𖹓', + '𖹴' => '𖹔', + '𖹵' => '𖹕', + '𖹶' => '𖹖', + '𖹷' => '𖹗', + '𖹸' => '𖹘', + '𖹹' => '𖹙', + '𖹺' => '𖹚', + '𖹻' => '𖹛', + '𖹼' => '𖹜', + '𖹽' => '𖹝', + '𖹾' => '𖹞', + '𖹿' => '𖹟', + '𞤢' => '𞤀', + '𞤣' => '𞤁', + '𞤤' => '𞤂', + '𞤥' => '𞤃', + '𞤦' => '𞤄', + '𞤧' => '𞤅', + '𞤨' => '𞤆', + '𞤩' => '𞤇', + '𞤪' => '𞤈', + '𞤫' => '𞤉', + '𞤬' => '𞤊', + '𞤭' => '𞤋', + '𞤮' => '𞤌', + '𞤯' => '𞤍', + '𞤰' => '𞤎', + '𞤱' => '𞤏', + '𞤲' => '𞤐', + '𞤳' => '𞤑', + '𞤴' => '𞤒', + '𞤵' => '𞤓', + '𞤶' => '𞤔', + '𞤷' => '𞤕', + '𞤸' => '𞤖', + '𞤹' => '𞤗', + '𞤺' => '𞤘', + '𞤻' => '𞤙', + '𞤼' => '𞤚', + '𞤽' => '𞤛', + '𞤾' => '𞤜', + '𞤿' => '𞤝', + '𞥀' => '𞤞', + '𞥁' => '𞤟', + '𞥂' => '𞤠', + '𞥃' => '𞤡', + 'ß' => 'SS', + 'ff' => 'FF', + 'fi' => 'FI', + 'fl' => 'FL', + 'ffi' => 'FFI', + 'ffl' => 'FFL', + 'ſt' => 'ST', + 'st' => 'ST', + 'և' => 'ԵՒ', + 'ﬓ' => 'ՄՆ', + 'ﬔ' => 'ՄԵ', + 'ﬕ' => 'ՄԻ', + 'ﬖ' => 'ՎՆ', + 'ﬗ' => 'ՄԽ', + 'ʼn' => 'ʼN', + 'ΐ' => 'Ϊ́', + 'ΰ' => 'Ϋ́', + 'ǰ' => 'J̌', + 'ẖ' => 'H̱', + 'ẗ' => 'T̈', + 'ẘ' => 'W̊', + 'ẙ' => 'Y̊', + 'ẚ' => 'Aʾ', + 'ὐ' => 'Υ̓', + 'ὒ' => 'Υ̓̀', + 'ὔ' => 'Υ̓́', + 'ὖ' => 'Υ̓͂', + 'ᾶ' => 'Α͂', + 'ῆ' => 'Η͂', + 'ῒ' => 'Ϊ̀', + 'ΐ' => 'Ϊ́', + 'ῖ' => 'Ι͂', + 'ῗ' => 'Ϊ͂', + 'ῢ' => 'Ϋ̀', + 'ΰ' => 'Ϋ́', + 'ῤ' => 'Ρ̓', + 'ῦ' => 'Υ͂', + 'ῧ' => 'Ϋ͂', + 'ῶ' => 'Ω͂', + 'ᾈ' => 'ἈΙ', + 'ᾉ' => 'ἉΙ', + 'ᾊ' => 'ἊΙ', + 'ᾋ' => 'ἋΙ', + 'ᾌ' => 'ἌΙ', + 'ᾍ' => 'ἍΙ', + 'ᾎ' => 'ἎΙ', + 'ᾏ' => 'ἏΙ', + 'ᾘ' => 'ἨΙ', + 'ᾙ' => 'ἩΙ', + 'ᾚ' => 'ἪΙ', + 'ᾛ' => 'ἫΙ', + 'ᾜ' => 'ἬΙ', + 'ᾝ' => 'ἭΙ', + 'ᾞ' => 'ἮΙ', + 'ᾟ' => 'ἯΙ', + 'ᾨ' => 'ὨΙ', + 'ᾩ' => 'ὩΙ', + 'ᾪ' => 'ὪΙ', + 'ᾫ' => 'ὫΙ', + 'ᾬ' => 'ὬΙ', + 'ᾭ' => 'ὭΙ', + 'ᾮ' => 'ὮΙ', + 'ᾯ' => 'ὯΙ', + 'ᾼ' => 'ΑΙ', + 'ῌ' => 'ΗΙ', + 'ῼ' => 'ΩΙ', + 'ᾲ' => 'ᾺΙ', + 'ᾴ' => 'ΆΙ', + 'ῂ' => 'ῊΙ', + 'ῄ' => 'ΉΙ', + 'ῲ' => 'ῺΙ', + 'ῴ' => 'ΏΙ', + 'ᾷ' => 'Α͂Ι', + 'ῇ' => 'Η͂Ι', + 'ῷ' => 'Ω͂Ι', +); diff --git a/plants/vendor/symfony/polyfill-mbstring/bootstrap.php b/plants/vendor/symfony/polyfill-mbstring/bootstrap.php new file mode 100644 index 0000000..ff51ae0 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language($language = null) { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + +if (!function_exists('mb_trim')) { + function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } +} + +if (!function_exists('mb_ltrim')) { + function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } +} + +if (!function_exists('mb_rtrim')) { + function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } +} + + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/plants/vendor/symfony/polyfill-mbstring/bootstrap80.php b/plants/vendor/symfony/polyfill-mbstring/bootstrap80.php new file mode 100644 index 0000000..5236e6d --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/bootstrap80.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (!function_exists('mb_convert_encoding')) { + function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); } +} +if (!function_exists('mb_decode_mimeheader')) { + function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); } +} +if (!function_exists('mb_encode_mimeheader')) { + function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); } +} +if (!function_exists('mb_decode_numericentity')) { + function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); } +} +if (!function_exists('mb_encode_numericentity')) { + function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); } +} +if (!function_exists('mb_convert_case')) { + function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); } +} +if (!function_exists('mb_internal_encoding')) { + function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); } +} +if (!function_exists('mb_language')) { + function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); } +} +if (!function_exists('mb_list_encodings')) { + function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); } +} +if (!function_exists('mb_encoding_aliases')) { + function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); } +} +if (!function_exists('mb_check_encoding')) { + function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); } +} +if (!function_exists('mb_detect_encoding')) { + function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); } +} +if (!function_exists('mb_detect_order')) { + function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); } +} +if (!function_exists('mb_parse_str')) { + function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; } +} +if (!function_exists('mb_strlen')) { + function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); } +} +if (!function_exists('mb_strpos')) { + function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strtolower')) { + function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); } +} +if (!function_exists('mb_strtoupper')) { + function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); } +} +if (!function_exists('mb_substitute_character')) { + function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); } +} +if (!function_exists('mb_substr')) { + function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); } +} +if (!function_exists('mb_stripos')) { + function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_stristr')) { + function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrchr')) { + function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strrichr')) { + function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_strripos')) { + function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strrpos')) { + function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); } +} +if (!function_exists('mb_strstr')) { + function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); } +} +if (!function_exists('mb_get_info')) { + function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); } +} +if (!function_exists('mb_http_output')) { + function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); } +} +if (!function_exists('mb_strwidth')) { + function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); } +} +if (!function_exists('mb_substr_count')) { + function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); } +} +if (!function_exists('mb_output_handler')) { + function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); } +} +if (!function_exists('mb_http_input')) { + function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); } +} + +if (!function_exists('mb_convert_variables')) { + function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); } +} + +if (!function_exists('mb_ord')) { + function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); } +} +if (!function_exists('mb_chr')) { + function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); } +} +if (!function_exists('mb_scrub')) { + function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); } +} +if (!function_exists('mb_str_split')) { + function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); } +} + +if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } +} + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + +if (!function_exists('mb_trim')) { + function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } +} + +if (!function_exists('mb_ltrim')) { + function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } +} + +if (!function_exists('mb_rtrim')) { + function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } +} + +if (extension_loaded('mbstring')) { + return; +} + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); +} +if (!defined('MB_CASE_LOWER')) { + define('MB_CASE_LOWER', 1); +} +if (!defined('MB_CASE_TITLE')) { + define('MB_CASE_TITLE', 2); +} diff --git a/plants/vendor/symfony/polyfill-mbstring/composer.json b/plants/vendor/symfony/polyfill-mbstring/composer.json new file mode 100644 index 0000000..daa07f8 --- /dev/null +++ b/plants/vendor/symfony/polyfill-mbstring/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/polyfill-mbstring", + "type": "library", + "description": "Symfony polyfill for the Mbstring extension", + "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2", + "ext-iconv": "*" + }, + "provide": { + "ext-mbstring": "*" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plants/vendor/symfony/polyfill-php80/LICENSE b/plants/vendor/symfony/polyfill-php80/LICENSE new file mode 100644 index 0000000..0ed3a24 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plants/vendor/symfony/polyfill-php80/Php80.php b/plants/vendor/symfony/polyfill-php80/Php80.php new file mode 100644 index 0000000..362dd1a --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Php80.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Ion Bazan + * @author Nico Oelgart + * @author Nicolas Grekas + * + * @internal + */ +final class Php80 +{ + public static function fdiv(float $dividend, float $divisor): float + { + return @($dividend / $divisor); + } + + public static function get_debug_type($value): string + { + switch (true) { + case null === $value: return 'null'; + case \is_bool($value): return 'bool'; + case \is_string($value): return 'string'; + case \is_array($value): return 'array'; + case \is_int($value): return 'int'; + case \is_float($value): return 'float'; + case \is_object($value): break; + case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; + default: + if (null === $type = @get_resource_type($value)) { + return 'unknown'; + } + + if ('Unknown' === $type) { + $type = 'closed'; + } + + return "resource ($type)"; + } + + $class = \get_class($value); + + if (false === strpos($class, '@')) { + return $class; + } + + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + } + + public static function get_resource_id($res): int + { + if (!\is_resource($res) && null === @get_resource_type($res)) { + throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); + } + + return (int) $res; + } + + public static function preg_last_error_msg(): string + { + switch (preg_last_error()) { + case \PREG_INTERNAL_ERROR: + return 'Internal error'; + case \PREG_BAD_UTF8_ERROR: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + case \PREG_BAD_UTF8_OFFSET_ERROR: + return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; + case \PREG_BACKTRACK_LIMIT_ERROR: + return 'Backtrack limit exhausted'; + case \PREG_RECURSION_LIMIT_ERROR: + return 'Recursion limit exhausted'; + case \PREG_JIT_STACKLIMIT_ERROR: + return 'JIT stack limit exhausted'; + case \PREG_NO_ERROR: + return 'No error'; + default: + return 'Unknown error'; + } + } + + public static function str_contains(string $haystack, string $needle): bool + { + return '' === $needle || false !== strpos($haystack, $needle); + } + + public static function str_starts_with(string $haystack, string $needle): bool + { + return 0 === strncmp($haystack, $needle, \strlen($needle)); + } + + public static function str_ends_with(string $haystack, string $needle): bool + { + if ('' === $needle || $needle === $haystack) { + return true; + } + + if ('' === $haystack) { + return false; + } + + $needleLength = \strlen($needle); + + return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); + } +} diff --git a/plants/vendor/symfony/polyfill-php80/PhpToken.php b/plants/vendor/symfony/polyfill-php80/PhpToken.php new file mode 100644 index 0000000..cd78c4c --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/PhpToken.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php80; + +/** + * @author Fedonyuk Anton + * + * @internal + */ +class PhpToken implements \Stringable +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + public $text; + + /** + * @var -1|positive-int + */ + public $line; + + /** + * @var int + */ + public $pos; + + /** + * @param -1|positive-int $line + */ + public function __construct(int $id, string $text, int $line = -1, int $position = -1) + { + $this->id = $id; + $this->text = $text; + $this->line = $line; + $this->pos = $position; + } + + public function getTokenName(): ?string + { + if ('UNKNOWN' === $name = token_name($this->id)) { + $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; + } + + return $name; + } + + /** + * @param int|string|array $kind + */ + public function is($kind): bool + { + foreach ((array) $kind as $value) { + if (\in_array($value, [$this->id, $this->text], true)) { + return true; + } + } + + return false; + } + + public function isIgnorable(): bool + { + return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); + } + + public function __toString(): string + { + return (string) $this->text; + } + + /** + * @return list + */ + public static function tokenize(string $code, int $flags = 0): array + { + $line = 1; + $position = 0; + $tokens = token_get_all($code, $flags); + foreach ($tokens as $index => $token) { + if (\is_string($token)) { + $id = \ord($token); + $text = $token; + } else { + [$id, $text, $line] = $token; + } + $tokens[$index] = new static($id, $text, $line, $position); + $position += \strlen($text); + } + + return $tokens; + } +} diff --git a/plants/vendor/symfony/polyfill-php80/README.md b/plants/vendor/symfony/polyfill-php80/README.md new file mode 100644 index 0000000..3816c55 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/README.md @@ -0,0 +1,25 @@ +Symfony Polyfill / Php80 +======================== + +This component provides features added to PHP 8.0 core: + +- [`Stringable`](https://php.net/stringable) interface +- [`fdiv`](https://php.net/fdiv) +- [`ValueError`](https://php.net/valueerror) class +- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class +- `FILTER_VALIDATE_BOOL` constant +- [`get_debug_type`](https://php.net/get_debug_type) +- [`PhpToken`](https://php.net/phptoken) class +- [`preg_last_error_msg`](https://php.net/preg_last_error_msg) +- [`str_contains`](https://php.net/str_contains) +- [`str_starts_with`](https://php.net/str_starts_with) +- [`str_ends_with`](https://php.net/str_ends_with) +- [`get_resource_id`](https://php.net/get_resource_id) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plants/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php b/plants/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php new file mode 100644 index 0000000..2b95542 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#[Attribute(Attribute::TARGET_CLASS)] +final class Attribute +{ + public const TARGET_CLASS = 1; + public const TARGET_FUNCTION = 2; + public const TARGET_METHOD = 4; + public const TARGET_PROPERTY = 8; + public const TARGET_CLASS_CONSTANT = 16; + public const TARGET_PARAMETER = 32; + public const TARGET_ALL = 63; + public const IS_REPEATABLE = 64; + + /** @var int */ + public $flags; + + public function __construct(int $flags = self::TARGET_ALL) + { + $this->flags = $flags; + } +} diff --git a/plants/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php b/plants/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php new file mode 100644 index 0000000..bd1212f --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { + class PhpToken extends Symfony\Polyfill\Php80\PhpToken + { + } +} diff --git a/plants/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php b/plants/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php new file mode 100644 index 0000000..7c62d75 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + interface Stringable + { + /** + * @return string + */ + public function __toString(); + } +} diff --git a/plants/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php b/plants/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php new file mode 100644 index 0000000..01c6c6c --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class UnhandledMatchError extends Error + { + } +} diff --git a/plants/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php b/plants/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php new file mode 100644 index 0000000..783dbc2 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80000) { + class ValueError extends Error + { + } +} diff --git a/plants/vendor/symfony/polyfill-php80/bootstrap.php b/plants/vendor/symfony/polyfill-php80/bootstrap.php new file mode 100644 index 0000000..e5f7dbc --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/bootstrap.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php80 as p; + +if (\PHP_VERSION_ID >= 80000) { + return; +} + +if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { + define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); +} + +if (!function_exists('fdiv')) { + function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } +} +if (!function_exists('preg_last_error_msg')) { + function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } +} +if (!function_exists('str_contains')) { + function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_starts_with')) { + function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('str_ends_with')) { + function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } +} +if (!function_exists('get_debug_type')) { + function get_debug_type($value): string { return p\Php80::get_debug_type($value); } +} +if (!function_exists('get_resource_id')) { + function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } +} diff --git a/plants/vendor/symfony/polyfill-php80/composer.json b/plants/vendor/symfony/polyfill-php80/composer.json new file mode 100644 index 0000000..a503b03 --- /dev/null +++ b/plants/vendor/symfony/polyfill-php80/composer.json @@ -0,0 +1,37 @@ +{ + "name": "symfony/polyfill-php80", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/plants/vendor/vlucas/phpdotenv/LICENSE b/plants/vendor/vlucas/phpdotenv/LICENSE new file mode 100644 index 0000000..922c552 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2014, Graham Campbell. +Copyright (c) 2013, Vance Lucas. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plants/vendor/vlucas/phpdotenv/composer.json b/plants/vendor/vlucas/phpdotenv/composer.json new file mode 100644 index 0000000..3636317 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/composer.json @@ -0,0 +1,60 @@ +{ + "name": "vlucas/phpdotenv", + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": ["env", "dotenv", "environment"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "require": { + "php": "^7.2.5 || ^8.0", + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "ext-filter": "*", + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit":"^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Dotenv\\Tests\\": "tests/Dotenv/" + } + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + }, + "preferred-install": "dist" + }, + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Dotenv.php b/plants/vendor/vlucas/phpdotenv/src/Dotenv.php new file mode 100644 index 0000000..34ca850 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Dotenv.php @@ -0,0 +1,267 @@ +store = $store; + $this->parser = $parser; + $this->loader = $loader; + $this->repository = $repository; + } + + /** + * Create a new dotenv instance. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + + foreach ((array) $paths as $path) { + $builder = $builder->addPath($path); + } + + foreach ((array) $names as $name) { + $builder = $builder->addName($name); + } + + if ($shortCircuit) { + $builder = $builder->shortCircuit(); + } + + return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); + } + + /** + * Create a new mutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new mutable dotenv instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenv instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new dotenv instance with an array backed repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Parse the given content and resolve nested variables. + * + * This method behaves just like load(), only without mutating your actual + * environment. We do this by using an array backed repository. + * + * @param string $content + * + * @throws \Dotenv\Exception\InvalidFileException + * + * @return array + */ + public static function parse(string $content) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); + + return $phpdotenv->load(); + } + + /** + * Read and load environment file(s). + * + * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException + * + * @return array + */ + public function load() + { + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); + } + + /** + * Read and load environment file(s), silently failing if no files can be read. + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException + * + * @return array + */ + public function safeLoad() + { + try { + return $this->load(); + } catch (InvalidPathException $e) { + // suppressing exception + return []; + } + } + + /** + * Required ensures that the specified variables exist, and returns a new validator object. + * + * @param string|string[] $variables + * + * @return \Dotenv\Validator + */ + public function required($variables) + { + return (new Validator($this->repository, (array) $variables))->required(); + } + + /** + * Returns a new validator object that won't check if the specified variables exist. + * + * @param string|string[] $variables + * + * @return \Dotenv\Validator + */ + public function ifPresent($variables) + { + return new Validator($this->repository, (array) $variables); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Exception/ExceptionInterface.php b/plants/vendor/vlucas/phpdotenv/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..1e80f53 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Exception/ExceptionInterface.php @@ -0,0 +1,12 @@ + + */ + public function load(RepositoryInterface $repository, array $entries) + { + /** @var array */ + return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { + $name = $entry->getName(); + + $value = $entry->getValue()->map(static function (Value $value) use ($repository) { + return Resolver::resolve($repository, $value); + }); + + if ($value->isDefined()) { + $inner = $value->get(); + if ($repository->set($name, $inner)) { + return \array_merge($vars, [$name => $inner]); + } + } else { + if ($repository->clear($name)) { + return \array_merge($vars, [$name => null]); + } + } + + return $vars; + }, []); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Loader/LoaderInterface.php b/plants/vendor/vlucas/phpdotenv/src/Loader/LoaderInterface.php new file mode 100644 index 0000000..f40d6ad --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Loader/LoaderInterface.php @@ -0,0 +1,20 @@ + + */ + public function load(RepositoryInterface $repository, array $entries); +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Loader/Resolver.php b/plants/vendor/vlucas/phpdotenv/src/Loader/Resolver.php new file mode 100644 index 0000000..ab5adf8 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Loader/Resolver.php @@ -0,0 +1,65 @@ +getVars(), static function (string $s, int $i) use ($repository) { + return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); + }, $value->getChars()); + } + + /** + * Resolve a single nested variable. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string $str + * + * @return string + */ + private static function resolveVariable(RepositoryInterface $repository, string $str) + { + return Regex::replaceCallback( + '/\A\${([a-zA-Z0-9_.]+)}/', + static function (array $matches) use ($repository) { + /** @var string */ + return Option::fromValue($repository->get($matches[1]))->getOrElse($matches[0]); + }, + $str, + 1 + )->success()->getOrElse($str); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/Entry.php b/plants/vendor/vlucas/phpdotenv/src/Parser/Entry.php new file mode 100644 index 0000000..716f422 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/Entry.php @@ -0,0 +1,59 @@ +name = $name; + $this->value = $value; + } + + /** + * Get the entry name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the entry value. + * + * @return \PhpOption\Option<\Dotenv\Parser\Value> + */ + public function getValue() + { + /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ + return Option::fromValue($this->value); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/EntryParser.php b/plants/vendor/vlucas/phpdotenv/src/Parser/EntryParser.php new file mode 100644 index 0000000..85e5fa3 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/EntryParser.php @@ -0,0 +1,300 @@ + + */ + public static function parse(string $entry) + { + return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { + [$name, $value] = $parts; + + return self::parseName($name)->flatMap(static function (string $name) use ($value) { + /** @var Result */ + $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); + + return $parsedValue->map(static function (?Value $value) use ($name) { + return new Entry($name, $value); + }); + }); + }); + } + + /** + * Split the compound string into parts. + * + * @param string $line + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function splitStringIntoParts(string $line) + { + /** @var array{string, string|null} */ + $result = Str::pos($line, '=')->map(static function () use ($line) { + return \array_map('trim', \explode('=', $line, 2)); + })->getOrElse([$line, null]); + + if ($result[0] === '') { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an unexpected equals', $line)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } + + /** + * Parse the given variable name. + * + * That is, strip the optional quotes and leading "export" from the + * variable name. We wrap the answer in a result type. + * + * @param string $name + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function parseName(string $name) + { + if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { + $name = \ltrim(Str::substr($name, 6)); + } + + if (self::isQuotedName($name)) { + $name = Str::substr($name, 1, -1); + } + + if (!self::isValidName($name)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an invalid name', $name)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($name); + } + + /** + * Is the given variable name quoted? + * + * @param string $name + * + * @return bool + */ + private static function isQuotedName(string $name) + { + if (Str::len($name) < 3) { + return false; + } + + $first = Str::substr($name, 0, 1); + $last = Str::substr($name, -1, 1); + + return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); + } + + /** + * Is the given variable name valid? + * + * @param string $name + * + * @return bool + */ + private static function isValidName(string $name) + { + return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false); + } + + /** + * Parse the given variable value. + * + * This has the effect of stripping quotes and comments, dealing with + * special characters, and locating nested variables, but not resolving + * them. Formally, we run a finite state automaton with an output tape: a + * transducer. We wrap the answer in a result type. + * + * @param string $value + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> + */ + private static function parseValue(string $value) + { + if (\trim($value) === '') { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Success::create(Value::blank()); + } + + return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { + return $data->flatMap(static function (array $data) use ($token) { + return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { + return [$data[0]->append($val[0], $val[1]), $val[2]]; + }); + }); + }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { + /** @psalm-suppress DocblockTypeContradiction */ + if (in_array($result[1], self::REJECT_STATES, true)) { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Error::create('a missing closing quote'); + } + + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ + return Success::create($result[0]); + })->mapError(static function (string $err) use ($value) { + return self::getErrorMessage($err, $value); + }); + } + + /** + * Process the given token. + * + * @param int $state + * @param string $token + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function processToken(int $state, string $token) + { + switch ($state) { + case self::INITIAL_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::SINGLE_QUOTED_STATE]); + } elseif ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::UNQUOTED_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::SINGLE_QUOTED_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); + } + case self::DOUBLE_QUOTED_STATE: + if ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } + case self::ESCAPE_SEQUENCE_STATE: + if ($token === '"' || $token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } else { + $first = Str::substr($token, 0, 1); + if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('an unexpected escape sequence'); + } + } + case self::WHITESPACE_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (!\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('unexpected whitespace'); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } + case self::COMMENT_STATE: + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + default: + throw new \Error('Parser entered invalid state.'); + } + } + + /** + * Generate a friendly error message. + * + * @param string $cause + * @param string $subject + * + * @return string + */ + private static function getErrorMessage(string $cause, string $subject) + { + return \sprintf( + 'Encountered %s at [%s].', + $cause, + \strtok($subject, "\n") + ); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/Lexer.php b/plants/vendor/vlucas/phpdotenv/src/Parser/Lexer.php new file mode 100644 index 0000000..981af24 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/Lexer.php @@ -0,0 +1,58 @@ + + */ + public static function lex(string $content) + { + static $regex; + + if ($regex === null) { + $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; + } + + $offset = 0; + + while (isset($content[$offset])) { + if (!\preg_match($regex, $content, $matches, 0, $offset)) { + throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); + } + + $offset += \strlen($matches[0]); + + yield $matches[0]; + } + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/Lines.php b/plants/vendor/vlucas/phpdotenv/src/Parser/Lines.php new file mode 100644 index 0000000..b3af160 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/Lines.php @@ -0,0 +1,127 @@ +map(static function () use ($line) { + return self::looksLikeMultilineStop($line, true) === false; + })->getOrElse(false); + } + + /** + * Determine if the given line can be the start of a multiline variable. + * + * @param string $line + * @param bool $started + * + * @return bool + */ + private static function looksLikeMultilineStop(string $line, bool $started) + { + if ($line === '"') { + return true; + } + + return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { + return $started ? $count > 1 : $count >= 1; + })->success()->getOrElse(false); + } + + /** + * Determine if the line in the file is a comment or whitespace. + * + * @param string $line + * + * @return bool + */ + private static function isCommentOrWhitespace(string $line) + { + $line = \trim($line); + + return $line === '' || (isset($line[0]) && $line[0] === '#'); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/Parser.php b/plants/vendor/vlucas/phpdotenv/src/Parser/Parser.php new file mode 100644 index 0000000..bca8ec5 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/Parser.php @@ -0,0 +1,53 @@ +mapError(static function () { + return 'Could not split into separate lines.'; + })->flatMap(static function (array $lines) { + return self::process(Lines::process($lines)); + })->mapError(static function (string $error) { + throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); + })->success()->get(); + } + + /** + * Convert the raw entries into proper entries. + * + * @param string[] $entries + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> + */ + private static function process(array $entries) + { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> */ + return \array_reduce($entries, static function (Result $result, string $raw) { + return $result->flatMap(static function (array $entries) use ($raw) { + return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { + /** @var \Dotenv\Parser\Entry[] */ + return \array_merge($entries, [$entry]); + }); + }); + }, Success::create([])); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Parser/ParserInterface.php b/plants/vendor/vlucas/phpdotenv/src/Parser/ParserInterface.php new file mode 100644 index 0000000..17cc42a --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Parser/ParserInterface.php @@ -0,0 +1,19 @@ +chars = $chars; + $this->vars = $vars; + } + + /** + * Create an empty value instance. + * + * @return \Dotenv\Parser\Value + */ + public static function blank() + { + return new self('', []); + } + + /** + * Create a new value instance, appending the characters. + * + * @param string $chars + * @param bool $var + * + * @return \Dotenv\Parser\Value + */ + public function append(string $chars, bool $var) + { + return new self( + $this->chars.$chars, + $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars + ); + } + + /** + * Get the string representation of the parsed value. + * + * @return string + */ + public function getChars() + { + return $this->chars; + } + + /** + * Get the locations of the variables in the value. + * + * @return int[] + */ + public function getVars() + { + $vars = $this->vars; + + \rsort($vars); + + return $vars; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/AdapterInterface.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/AdapterInterface.php new file mode 100644 index 0000000..5604398 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/AdapterInterface.php @@ -0,0 +1,15 @@ + + */ + public static function create(); +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ApacheAdapter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ApacheAdapter.php new file mode 100644 index 0000000..af0aae1 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ApacheAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * This happens if PHP is running as an Apache module. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('apache_getenv') && \function_exists('apache_setenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(apache_getenv($name))->filter(static function ($value) { + return \is_string($value) && $value !== ''; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + return apache_setenv($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + return apache_setenv($name, ''); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ArrayAdapter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ArrayAdapter.php new file mode 100644 index 0000000..7c3740d --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ArrayAdapter.php @@ -0,0 +1,80 @@ + + */ + private $variables; + + /** + * Create a new array adapter instance. + * + * @return void + */ + private function __construct() + { + $this->variables = []; + } + + /** + * Create a new instance of the adapter, if it is available. + * + * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + return Option::fromArraysValue($this->variables, $name); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $this->variables[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($this->variables[$name]); + + return true; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php new file mode 100644 index 0000000..9eb1947 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_ENV, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_ENV[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_ENV[$name]); + + return true; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/GuardedWriter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/GuardedWriter.php new file mode 100644 index 0000000..fed8b9b --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/GuardedWriter.php @@ -0,0 +1,85 @@ +writer = $writer; + $this->allowList = $allowList; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't set non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->write($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->delete($name); + } + + /** + * Determine if the given variable is allowed. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isAllowed(string $name) + { + return \in_array($name, $this->allowList, true); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ImmutableWriter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ImmutableWriter.php new file mode 100644 index 0000000..3b279b8 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ImmutableWriter.php @@ -0,0 +1,110 @@ + + */ + private $loaded; + + /** + * Create a new immutable writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->loaded = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't overwrite existing environment variables + // Ruby's dotenv does this with `ENV[key] ||= value` + if ($this->isExternallyDefined($name)) { + return false; + } + + // Set the value on the inner writer + if (!$this->writer->write($name, $value)) { + return false; + } + + // Record that we have loaded the variable + $this->loaded[$name] = ''; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear existing environment variables + if ($this->isExternallyDefined($name)) { + return false; + } + + // Clear the value on the inner writer + if (!$this->writer->delete($name)) { + return false; + } + + // Leave the variable as fair game + unset($this->loaded[$name]); + + return true; + } + + /** + * Determine if the given variable is externally defined. + * + * That is, is it an "existing" variable. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isExternallyDefined(string $name) + { + return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiReader.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiReader.php new file mode 100644 index 0000000..0cfda6f --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiReader.php @@ -0,0 +1,48 @@ +readers = $readers; + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + foreach ($this->readers as $reader) { + $result = $reader->read($name); + if ($result->isDefined()) { + return $result; + } + } + + return None::create(); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiWriter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiWriter.php new file mode 100644 index 0000000..15a9d8f --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiWriter.php @@ -0,0 +1,64 @@ +writers = $writers; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + foreach ($this->writers as $writers) { + if (!$writers->write($name, $value)) { + return false; + } + } + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + foreach ($this->writers as $writers) { + if (!$writers->delete($name)) { + return false; + } + } + + return true; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/PutenvAdapter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/PutenvAdapter.php new file mode 100644 index 0000000..6d017cd --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/PutenvAdapter.php @@ -0,0 +1,91 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('getenv') && \function_exists('putenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\getenv($name), false)->filter(static function ($value) { + return \is_string($value); + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + \putenv("$name=$value"); + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + \putenv($name); + + return true; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReaderInterface.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReaderInterface.php new file mode 100644 index 0000000..306a63f --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReaderInterface.php @@ -0,0 +1,17 @@ + + */ + public function read(string $name); +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReplacingWriter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReplacingWriter.php new file mode 100644 index 0000000..4c92a4f --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReplacingWriter.php @@ -0,0 +1,104 @@ + + */ + private $seen; + + /** + * Create a new replacement writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->seen = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + if ($this->exists($name)) { + return $this->writer->write($name, $value); + } + + // succeed if nothing to do + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + if ($this->exists($name)) { + return $this->writer->delete($name); + } + + // succeed if nothing to do + return true; + } + + /** + * Does the given environment variable exist. + * + * Returns true if it currently exists, or existed at any point in the past + * that we are aware of. + * + * @param non-empty-string $name + * + * @return bool + */ + private function exists(string $name) + { + if (isset($this->seen[$name])) { + return true; + } + + if ($this->reader->read($name)->isDefined()) { + $this->seen[$name] = ''; + + return true; + } + + return false; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php new file mode 100644 index 0000000..f93b6e5 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_SERVER, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_SERVER[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_SERVER[$name]); + + return true; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/WriterInterface.php b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/WriterInterface.php new file mode 100644 index 0000000..4cb3d61 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/Adapter/WriterInterface.php @@ -0,0 +1,27 @@ +reader = $reader; + $this->writer = $writer; + } + + /** + * Determine if the given environment variable is defined. + * + * @param string $name + * + * @return bool + */ + public function has(string $name) + { + return '' !== $name && $this->reader->read($name)->isDefined(); + } + + /** + * Get an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return string|null + */ + public function get(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->reader->read($name)->getOrElse(null); + } + + /** + * Set an environment variable. + * + * @param string $name + * @param string $value + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function set(string $name, string $value) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->write($name, $value); + } + + /** + * Clear an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function clear(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->delete($name); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php b/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php new file mode 100644 index 0000000..7607924 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php @@ -0,0 +1,272 @@ +readers = $readers; + $this->writers = $writers; + $this->immutable = $immutable; + $this->allowList = $allowList; + } + + /** + * Create a new repository builder instance with no adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithNoAdapters() + { + return new self(); + } + + /** + * Create a new repository builder instance with the default adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithDefaultAdapters() + { + $adapters = \iterator_to_array(self::defaultAdapters()); + + return new self($adapters, $adapters); + } + + /** + * Return the array of default adapters. + * + * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> + */ + private static function defaultAdapters() + { + foreach (self::DEFAULT_ADAPTERS as $adapter) { + $instance = $adapter::create(); + if ($instance->isDefined()) { + yield $instance->get(); + } + } + } + + /** + * Determine if the given name if of an adapterclass. + * + * @param string $name + * + * @return bool + */ + private static function isAnAdapterClass(string $name) + { + if (!\class_exists($name)) { + return false; + } + + return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); + } + + /** + * Creates a repository builder with the given reader added. + * + * Accepts either a reader instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addReader($reader) + { + if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + ReaderInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($reader)->flatMap(static function ($reader) { + return \is_string($reader) ? $reader::create() : Some::create($reader); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + + return new self($readers, $this->writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given writer added. + * + * Accepts either a writer instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addWriter($writer) + { + if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($writer)->flatMap(static function ($writer) { + return \is_string($writer) ? $writer::create() : Some::create($writer); + }); + + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($this->readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given adapter added. + * + * Accepts either an adapter instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. We will + * add the adapter as both a reader and a writer. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addAdapter($adapter) + { + if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($adapter)->flatMap(static function ($adapter) { + return \is_string($adapter) ? $adapter::create() : Some::create($adapter); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with mutability enabled. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function immutable() + { + return new self($this->readers, $this->writers, true, $this->allowList); + } + + /** + * Creates a repository builder with the given allow list. + * + * @param string[]|null $allowList + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function allowList(?array $allowList = null) + { + return new self($this->readers, $this->writers, $this->immutable, $allowList); + } + + /** + * Creates a new repository instance. + * + * @return \Dotenv\Repository\RepositoryInterface + */ + public function make() + { + $reader = new MultiReader($this->readers); + $writer = new MultiWriter($this->writers); + + if ($this->immutable) { + $writer = new ImmutableWriter($writer, $reader); + } + + if ($this->allowList !== null) { + $writer = new GuardedWriter($writer, $this->allowList); + } + + return new AdapterRepository($reader, $writer); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryInterface.php b/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryInterface.php new file mode 100644 index 0000000..d9b18a4 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Repository/RepositoryInterface.php @@ -0,0 +1,51 @@ + + */ + public static function read(array $filePaths, bool $shortCircuit = true, ?string $fileEncoding = null) + { + $output = []; + + foreach ($filePaths as $filePath) { + $content = self::readFromFile($filePath, $fileEncoding); + if ($content->isDefined()) { + $output[$filePath] = $content->get(); + if ($shortCircuit) { + break; + } + } + } + + return $output; + } + + /** + * Read the given file. + * + * @param string $path + * @param string|null $encoding + * + * @throws \Dotenv\Exception\InvalidEncodingException + * + * @return \PhpOption\Option + */ + private static function readFromFile(string $path, ?string $encoding = null) + { + /** @var Option */ + $content = Option::fromValue(@\file_get_contents($path), false); + + return $content->flatMap(static function (string $content) use ($encoding) { + return Str::utf8($content, $encoding)->mapError(static function (string $error) { + throw new InvalidEncodingException($error); + })->success(); + }); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Store/FileStore.php b/plants/vendor/vlucas/phpdotenv/src/Store/FileStore.php new file mode 100644 index 0000000..e7a4d3f --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Store/FileStore.php @@ -0,0 +1,72 @@ +filePaths = $filePaths; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Read the content of the environment file(s). + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException + * + * @return string + */ + public function read() + { + if ($this->filePaths === []) { + throw new InvalidPathException('At least one environment file path must be provided.'); + } + + $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); + + if (\count($contents) > 0) { + return \implode("\n", $contents); + } + + throw new InvalidPathException( + \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) + ); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Store/StoreBuilder.php b/plants/vendor/vlucas/phpdotenv/src/Store/StoreBuilder.php new file mode 100644 index 0000000..2ce0851 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Store/StoreBuilder.php @@ -0,0 +1,141 @@ +paths = $paths; + $this->names = $names; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Create a new store builder instance with no names. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithNoNames() + { + return new self(); + } + + /** + * Create a new store builder instance with the default name. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithDefaultName() + { + return new self([], [self::DEFAULT_NAME]); + } + + /** + * Creates a store builder with the given path added. + * + * @param string $path + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addPath(string $path) + { + return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with the given name added. + * + * @param string $name + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addName(string $name) + { + return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with short circuit mode enabled. + * + * @return \Dotenv\Store\StoreBuilder + */ + public function shortCircuit() + { + return new self($this->paths, $this->names, true, $this->fileEncoding); + } + + /** + * Creates a store builder with the specified file encoding. + * + * @param string|null $fileEncoding + * + * @return \Dotenv\Store\StoreBuilder + */ + public function fileEncoding(?string $fileEncoding = null) + { + return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); + } + + /** + * Creates a new store instance. + * + * @return \Dotenv\Store\StoreInterface + */ + public function make() + { + return new FileStore( + Paths::filePaths($this->paths, $this->names), + $this->shortCircuit, + $this->fileEncoding + ); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Store/StoreInterface.php b/plants/vendor/vlucas/phpdotenv/src/Store/StoreInterface.php new file mode 100644 index 0000000..6f5b986 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Store/StoreInterface.php @@ -0,0 +1,17 @@ +content = $content; + } + + /** + * Read the content of the environment file(s). + * + * @return string + */ + public function read() + { + return $this->content; + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Util/Regex.php b/plants/vendor/vlucas/phpdotenv/src/Util/Regex.php new file mode 100644 index 0000000..599f09c --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Util/Regex.php @@ -0,0 +1,112 @@ + + */ + public static function matches(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return @\preg_match($pattern, $subject) === 1; + }, $subject); + } + + /** + * Perform a preg match all, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function occurrences(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return (int) @\preg_match_all($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg replace callback, wrapping up the result. + * + * @param string $pattern + * @param callable(string[]): string $callback + * @param string $subject + * @param int|null $limit + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function replaceCallback(string $pattern, callable $callback, string $subject, ?int $limit = null) + { + return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { + return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); + }, $subject); + } + + /** + * Perform a preg split, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function split(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + /** @var string[] */ + return (array) @\preg_split($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg operation, wrapping up the result. + * + * @template V + * + * @param callable(string): V $operation + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function pregAndWrap(callable $operation, string $subject) + { + $result = $operation($subject); + + if (\preg_last_error() !== \PREG_NO_ERROR) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(\preg_last_error_msg()); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Util/Str.php b/plants/vendor/vlucas/phpdotenv/src/Util/Str.php new file mode 100644 index 0000000..b113d78 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Util/Str.php @@ -0,0 +1,108 @@ + + */ + public static function utf8(string $input, ?string $encoding = null) + { + if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create( + \sprintf('Illegal character encoding [%s] specified.', $encoding) + ); + } + + $converted = $encoding === null ? + @\mb_convert_encoding($input, 'UTF-8') : + @\mb_convert_encoding($input, 'UTF-8', $encoding); + + if (!is_string($converted)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create( + \sprintf('Conversion from encoding [%s] failed.', $encoding ?? 'NULL') + ); + } + + /** + * this is for support UTF-8 with BOM encoding + * @see https://en.wikipedia.org/wiki/Byte_order_mark + * @see https://github.com/vlucas/phpdotenv/issues/500 + */ + if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { + $converted = \substr($converted, 3); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($converted); + } + + /** + * Search for a given substring of the input. + * + * @param string $haystack + * @param string $needle + * + * @return \PhpOption\Option + */ + public static function pos(string $haystack, string $needle) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); + } + + /** + * Grab the specified substring of the input. + * + * @param string $input + * @param int $start + * @param int|null $length + * + * @return string + */ + public static function substr(string $input, int $start, ?int $length = null) + { + return \mb_substr($input, $start, $length, 'UTF-8'); + } + + /** + * Compute the length of the given string. + * + * @param string $input + * + * @return int + */ + public static function len(string $input) + { + return \mb_strlen($input, 'UTF-8'); + } +} diff --git a/plants/vendor/vlucas/phpdotenv/src/Validator.php b/plants/vendor/vlucas/phpdotenv/src/Validator.php new file mode 100644 index 0000000..d5580c7 --- /dev/null +++ b/plants/vendor/vlucas/phpdotenv/src/Validator.php @@ -0,0 +1,207 @@ +repository = $repository; + $this->variables = $variables; + } + + /** + * Assert that each variable is present. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function required() + { + return $this->assert( + static function (?string $value) { + return $value !== null; + }, + 'is missing' + ); + } + + /** + * Assert that each variable is not empty. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function notEmpty() + { + return $this->assertNullable( + static function (string $value) { + return Str::len(\trim($value)) > 0; + }, + 'is empty' + ); + } + + /** + * Assert that each specified variable is an integer. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function isInteger() + { + return $this->assertNullable( + static function (string $value) { + return \ctype_digit($value); + }, + 'is not an integer' + ); + } + + /** + * Assert that each specified variable is a boolean. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function isBoolean() + { + return $this->assertNullable( + static function (string $value) { + if ($value === '') { + return false; + } + + return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; + }, + 'is not a boolean' + ); + } + + /** + * Assert that each variable is amongst the given choices. + * + * @param string[] $choices + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function allowedValues(array $choices) + { + return $this->assertNullable( + static function (string $value) use ($choices) { + return \in_array($value, $choices, true); + }, + \sprintf('is not one of [%s]', \implode(', ', $choices)) + ); + } + + /** + * Assert that each variable matches the given regular expression. + * + * @param string $regex + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function allowedRegexValues(string $regex) + { + return $this->assertNullable( + static function (string $value) use ($regex) { + return Regex::matches($regex, $value)->success()->getOrElse(false); + }, + \sprintf('does not match "%s"', $regex) + ); + } + + /** + * Assert that the callback returns true for each variable. + * + * @param callable(?string):bool $callback + * @param string $message + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function assert(callable $callback, string $message) + { + $failing = []; + + foreach ($this->variables as $variable) { + if ($callback($this->repository->get($variable)) === false) { + $failing[] = \sprintf('%s %s', $variable, $message); + } + } + + if (\count($failing) > 0) { + throw new ValidationException(\sprintf( + 'One or more environment variables failed assertions: %s.', + \implode(', ', $failing) + )); + } + + return $this; + } + + /** + * Assert that the callback returns true for each variable. + * + * Skip checking null variable values. + * + * @param callable(string):bool $callback + * @param string $message + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function assertNullable(callable $callback, string $message) + { + return $this->assert( + static function (?string $value) use ($callback) { + if ($value === null) { + return true; + } + + return $callback($value); + }, + $message + ); + } +} diff --git a/storymachine/.env.example b/storymachine/.env.example new file mode 100644 index 0000000..c094e45 --- /dev/null +++ b/storymachine/.env.example @@ -0,0 +1 @@ +GEMINI_API_KEY="your_gemini_api_key_here" diff --git a/storymachine/api_proxy.php b/storymachine/api_proxy.php new file mode 100644 index 0000000..b05812b --- /dev/null +++ b/storymachine/api_proxy.php @@ -0,0 +1,99 @@ + + 'Nur POST-Anfragen sind erlaubt.']); + exit; +} + +// 3. API-Schlüssel sicher aus .env laden +$apiKey = ''; +$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env']; +foreach ($envFiles as $envFile) { + if (file_exists($envFile)) { + foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + if (strpos($line, '=') !== false) { + list($k, $v) = explode('=', $line, 2); + if (trim($k) === 'GEMINI_API_KEY') { + $apiKey = trim(trim($v), '"\''); + break 2; + } + } + } + } +} + +if (empty($apiKey)) { + http_response_code(500); + echo json_encode(['error' => 'API-Schlüssel nicht konfiguriert.']); + exit; +} + +// 4. Lese die JSON-Daten, die vom Frontend (JavaScript) gesendet wurden. +$json_input = file_get_contents('php://input'); +$request_data = json_decode($json_input, true); + +// Überprüfen, ob die Daten korrekt sind +if (json_last_error() !== JSON_ERROR_NONE || !isset($request_data['apiUrl']) || !isset($request_data['payload'])) { + http_response_code(400); // Bad Request + echo json_encode(['error' => 'Ungültige Anfrage-Daten.']); + exit; +} + +$apiUrl = $request_data['apiUrl']; +$payload = $request_data['payload']; + +// SSRF-Schutz: Nur generativelanguage.googleapis.com erlauben +$parsed = parse_url($apiUrl); +if (!isset($parsed['host']) || $parsed['host'] !== 'generativelanguage.googleapis.com') { + http_response_code(403); + echo json_encode(['error' => 'Ungültiges API-Ziel.']); + exit; +} + +// 5. Baue die vollständige URL für die Google API zusammen. +$fullApiUrl = $apiUrl . '?key=' . $apiKey; + +// 6. Sende die Anfrage mit cURL an die Google API (Standardmethode in PHP). +$ch = curl_init(); + +curl_setopt_array($ch, [ + CURLOPT_URL => $fullApiUrl, + CURLOPT_RETURNTRANSFER => true, // Gib die Antwort als String zurück + CURLOPT_POST => true, // Es ist eine POST-Anfrage + CURLOPT_POSTFIELDS => json_encode($payload), // Der JSON-Body der Anfrage + CURLOPT_HTTPHEADER => [ // Wichtige Header setzen + 'Content-Type: application/json', + ], +]); + +// Führe die Anfrage aus +$response_body = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Hole den HTTP-Statuscode der Antwort + +// Prüfe auf Fehler bei der cURL-Anfrage selbst +if (curl_errno($ch)) { + http_response_code(500); + echo json_encode(['error' => 'Fehler bei der Weiterleitung der Anfrage: ' . curl_error($ch)]); + exit; +} + +curl_close($ch); + +// 7. Leite die Antwort von Google direkt an dein Frontend weiter. +// Setze den HTTP-Statuscode auf den gleichen, den wir von Google bekommen haben. +http_response_code($http_code); +echo $response_body; + +?> \ No newline at end of file diff --git a/storymachine/index.php b/storymachine/index.php new file mode 100644 index 0000000..29be0f0 --- /dev/null +++ b/storymachine/index.php @@ -0,0 +1,877 @@ + + + + + + + + Deine magische Geschichten-Maschine + + + + + + + + + + + + + +
+ +
+

+ Die magische Geschichten-Maschine +

+ +
+ +
+

Für wen ist die Geschichte?

+
+ + + + +
+
+ +
+

Wähle aus, worum es gehen soll!

+
+
+

👆 Wähle zuerst eine Altersgruppe aus, um Ideen zu laden!

+
+ +
+
+ +
+ + +
+
+ + + + +
+ + + + + \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..090d6b2 --- /dev/null +++ b/test/index.html @@ -0,0 +1,22 @@ + + + + + + Test Projekt + + +

Zugriff gewährt!

+

Willkommen im Test-Projekt, .

+ + \ No newline at end of file diff --git a/test/index.php b/test/index.php new file mode 100644 index 0000000..090d6b2 --- /dev/null +++ b/test/index.php @@ -0,0 +1,22 @@ + + + + + + Test Projekt + + +

Zugriff gewährt!

+

Willkommen im Test-Projekt, .

+ + \ No newline at end of file