From b065c5350a629d837d247afefb99957781b2b4e7 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 29 May 2026 19:13:04 +0200 Subject: [PATCH] Dev-Tools: neues Tool mit YAML Formatter, Skizze BETA label --- frontend/src/icons.jsx | 3 + frontend/src/toolRegistry.js | 15 +- frontend/src/tools/devtools.jsx | 309 ++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 frontend/src/tools/devtools.jsx diff --git a/frontend/src/icons.jsx b/frontend/src/icons.jsx index e6b4b15..c41b695 100644 --- a/frontend/src/icons.jsx +++ b/frontend/src/icons.jsx @@ -150,3 +150,6 @@ export const SkizzeIcon = ({size=20,color='currentColor',sw}) => base(<> , size, color, sw); +export const WrenchIcon = ({size=20,color='currentColor',sw}) => base(<> + +, size, color, sw); diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js index 9b8793b..1f3e252 100644 --- a/frontend/src/toolRegistry.js +++ b/frontend/src/toolRegistry.js @@ -5,7 +5,8 @@ import Nachrichten from './tools/nachrichten.jsx'; import Linkliste from './tools/linkliste.jsx'; import CodeSchnipsel from './tools/codeschnipsel.jsx'; import Skizze from './tools/skizze.jsx'; -import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon } from './icons.jsx'; +import DevTools from './tools/devtools.jsx'; +import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon } from './icons.jsx'; export const TOOLS = [ { @@ -67,11 +68,19 @@ export const TOOLS = [ { id: 'skizze', Icon: SkizzeIcon, - label: 'Skizzen-Rechner', - navLabel: 'Skizze', + label: 'Skizzen-Rechner (β)', + navLabel: 'Skizze β', group: 'Werkzeuge', component: Skizze, }, + { + id: 'devtools', + Icon: WrenchIcon, + label: 'Dev-Tools', + navLabel: 'Dev-Tools', + group: 'Werkzeuge', + component: DevTools, + }, // Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... }, ]; diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx new file mode 100644 index 0000000..119fe40 --- /dev/null +++ b/frontend/src/tools/devtools.jsx @@ -0,0 +1,309 @@ +import { useState, useRef } from 'react'; +import { S } from '../lib.js'; + +// ── YAML Formatter ──────────────────────────────────────────────────────────── +function parseAndFormatYaml(input) { + // YAML-konformes Einrücken mit 2 Spaces + // Wir parsen manuell: kein externes package, aber korrekte Einrückung + const lines = input.split('\n'); + const out = []; + let inMultiline = false; + let multilineIndent = 0; + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const trimmed = raw.trim(); + + // Leerzeilen beibehalten + if (!trimmed) { out.push(''); continue; } + + // Kommentare unverändert + if (trimmed.startsWith('#')) { + const depth = detectDepth(raw); + out.push(' '.repeat(depth) + trimmed); + continue; + } + + // Multiline-Block (| oder >) – Zeilen danach unverändert einrücken + if (inMultiline) { + // Endet wenn Einrückung nicht mehr tiefer ist + const currentIndent = raw.search(/\S/); + if (currentIndent <= multilineIndent && trimmed) { + inMultiline = false; + } else { + out.push(raw); // Multiline-Inhalt unverändert + continue; + } + } + + const depth = detectDepth(raw); + const indent = ' '.repeat(depth); + + // Key: Value + const colonIdx = findKeyColon(trimmed); + if (colonIdx > 0) { + const key = trimmed.slice(0, colonIdx).trim(); + const value = trimmed.slice(colonIdx + 1).trim(); + + if (value === '|' || value === '>' || value === '|-' || value === '>-') { + inMultiline = true; + multilineIndent = raw.search(/\S/); + out.push(`${indent}${key}: ${value}`); + } else if (value === '') { + out.push(`${indent}${key}:`); + } else { + out.push(`${indent}${key}: ${value}`); + } + continue; + } + + // Listenelement (- ...) + if (trimmed.startsWith('- ') || trimmed === '-') { + out.push(`${indent}${trimmed}`); + continue; + } + + // Alles andere + out.push(`${indent}${trimmed}`); + } + + return out.join('\n'); +} + +// Berechnet die logische Einrückungstiefe (2 Spaces pro Level) +function detectDepth(line) { + const spaces = line.search(/\S/); + if (spaces < 0) return 0; + return Math.round(spaces / 2); +} + +// Findet den Doppelpunkt eines Keys (nicht innerhalb von Strings) +function findKeyColon(trimmed) { + let inSingle = false, inDouble = false; + for (let i = 0; i < trimmed.length; i++) { + const c = trimmed[i]; + if (c === "'" && !inDouble) inSingle = !inSingle; + if (c === '"' && !inSingle) inDouble = !inDouble; + if (c === ':' && !inSingle && !inDouble) { + // Muss von Leerzeichen oder End gefolgt sein + const next = trimmed[i+1]; + if (next === undefined || next === ' ' || next === '\t') return i; + } + } + return -1; +} + +function YamlTool() { + const [input, setInput] = useState(''); + const [output, setOutput] = useState(''); + const [error, setError] = useState(''); + const [copied, setCopied] = useState(false); + const outputRef = useRef(null); + + const format = () => { + if (!input.trim()) { setError('Bitte YAML eingeben.'); setOutput(''); return; } + try { + const result = parseAndFormatYaml(input); + setOutput(result); + setError(''); + } catch(e) { + setError('Fehler beim Formatieren: ' + e.message); + setOutput(''); + } + }; + + const copy = async () => { + try { + await navigator.clipboard.writeText(output); + setCopied(true); + setTimeout(()=>setCopied(false), 1500); + } catch { } + }; + + const clear = () => { setInput(''); setOutput(''); setError(''); }; + + // Auto-format on paste + const onPaste = e => { + setTimeout(()=>{ + const val = e.target.value; + if (val.trim()) { + const result = parseAndFormatYaml(val); + setOutput(result); + setError(''); + } + }, 0); + }; + + return ( +
+
+ {/* Input */} +
+
+ + {input && } +
+