diff --git a/frontend/package.json b/frontend/package.json index 1fbca73..9c7c6cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "build": "vite build" }, "dependencies": { + "js-yaml": "^4.1.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index 119fe40..47e6a67 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -1,203 +1,245 @@ -import { useState, useRef } from 'react'; +import { useState, useCallback } from 'react'; import { S } from '../lib.js'; +import jsyaml from 'js-yaml'; -// ── 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; +// ── YAML Tree Node (collapsible, mit Einrück-Linien) ───────────────────────── +function YamlNode({ data, depth = 0, keyName = null, collapsed, onToggle, path }) { + const indent = depth * 18; // px pro Level + const isObj = data !== null && typeof data === 'object' && !Array.isArray(data); + const isArr = Array.isArray(data); + const isLeaf = !isObj && !isArr; - for (let i = 0; i < lines.length; i++) { - const raw = lines[i]; - const trimmed = raw.trim(); + const nodeKey = path; + const isCollapsed = collapsed.has(nodeKey); + const hasChildren = isObj || isArr; - // Leerzeilen beibehalten - if (!trimmed) { out.push(''); continue; } + const keyColor = '#7ecaff'; + const strColor = '#ce9178'; + const numColor = '#b5cea8'; + const boolColor = '#569cd6'; + const nullColor = '#808080'; + const indentColor= 'rgba(255,255,255,0.08)'; - // 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 valueColor = (v) => { + if (v === null || v === undefined) return nullColor; + if (typeof v === 'boolean') return boolColor; + if (typeof v === 'number') return numColor; + return strColor; }; + const renderValue = (v) => { + if (v === null) return null; + if (v === undefined) return ~; + if (typeof v === 'boolean') return {String(v)}; + if (typeof v === 'number') return {v}; + const s = String(v); + // Quote strings that need it + const needsQuotes = /[:#\[\]{}&*!|>'"%@`]/.test(s) || s.trim()!==s || s==='' || /^(true|false|null|yes|no|on|off)$/i.test(s); + return {needsQuotes ? `"${s.replace(/"/g,'\\"')}"` : s}; + }; + + // Leaf: key: value + if (isLeaf) { + return ( +
+ {formatted||'# Ergebnis erscheint hier'}
+
+ ) : parsed!==null ? (
+
- {output || '# Ergebnis erscheint hier'}
-
+