import { useState, useCallback } from 'react'; import { S } from '../lib.js'; import jsyaml from 'js-yaml'; // ── 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; const nodeKey = path; const isCollapsed = collapsed.has(nodeKey); const hasChildren = isObj || isArr; const keyColor = '#7ecaff'; const strColor = '#ce9178'; const numColor = '#b5cea8'; const boolColor = '#569cd6'; const nullColor = '#808080'; const indentColor= 'rgba(255,255,255,0.08)'; 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 (
{/* Vertical indent lines */} {Array.from({length:depth},(_,i)=>(
))}
{keyName!==null&&{keyName}: } {renderValue(data)}
); } const count = isObj ? Object.keys(data).length : data.length; const summary = isObj ? `{${count}}` : `[${count}]`; return (
{/* Vertical indent lines for this level */} {Array.from({length:depth},(_,i)=>(
))} {/* Header row */}
hasChildren&&onToggle(nodeKey)}> {hasChildren&&( )} {keyName!==null&&{keyName}: } {isCollapsed&&{summary}} {!isCollapsed&&isArr&&# {count} Einträge}
{/* Children */} {!isCollapsed && (
{isObj && Object.entries(data).map(([k,v],i)=>( ))} {isArr && data.map((v,i)=>(
- {typeof v === 'object' && v !== null ? :
{v===null?null:typeof v==='boolean'?{String(v)}:typeof v==='number'?{v}:{String(v)}}
}
))}
)}
); } function collectPaths(data, path='root', paths=[]) { if (data !== null && typeof data === 'object') { paths.push(path); if (Array.isArray(data)) data.forEach((_,i)=>collectPaths(data[i],`${path}[${i}]`,paths)); else Object.entries(data).forEach(([k,v])=>collectPaths(v,`${path}.${k}`,paths)); } return paths; } // ── YAML Auto-Fix Stufe 1: Einheits-Normalisierung (kleine Fehler) ──────────── function fixByUnit(text) { const lines = text.split('\n').map(l => l.replace(/\t/g, ' ')); const indents = lines .filter(l => l.trim() && !l.trim().startsWith('#')) .map(l => l.search(/\S/)) .filter(n => n > 0); if (!indents.length) return text; const unit = Math.min(...indents); if (unit === 2) return lines.join('\n'); // bereits 2-space return lines.map(line => { if (!line.trim()) return ''; if (line.trim().startsWith('#')) return line.trimEnd(); const spaces = line.search(/\S/); if (spaces <= 0) return line.trimEnd(); return ' '.repeat(Math.round(spaces / unit)) + line.trimStart().trimEnd(); }).join('\n'); } // ── YAML Auto-Fix Stufe 2: Stack-basiert (starke Schäden) ──────────────────── function fixByStack(text) { const lines = text.split('\n').map(l => l.replace(/\t/g, ' ')); const stack = [{ indent: -1, depth: -1 }]; const result = []; for (const line of lines) { if (!line.trim()) { result.push(''); continue; } const indent = line.search(/\S/); const content = line.trim(); if (content.startsWith('#')) { result.push(' '.repeat(Math.max(0, stack[stack.length-1].depth + 1)) + content); continue; } while (stack.length > 1 && stack[stack.length-1].indent >= indent) stack.pop(); const depth = stack[stack.length-1].depth + 1; stack.push({ indent, depth }); result.push(' '.repeat(depth) + content); } return result.join('\n'); } function shortError(e) { const first = e.message.split('\n')[0]; return first.length > 120 ? first.slice(0, 120) + '…' : first; } // ── YAML Tool ───────────────────────────────────────────────────────────────── function YamlTool({ mobile }) { const [input, setInput] = useState(''); const [parsed, setParsed] = useState(null); const [formatted,setFormatted]= useState(''); const [error, setError] = useState(''); const [warning, setWarning] = useState(''); const [copied, setCopied] = useState(false); const [view, setView] = useState('tree'); const [collapsed,setCollapsed]= useState(new Set()); const process = useCallback((text) => { if (!text.trim()) { setParsed(null); setFormatted(''); setError(''); setWarning(''); return; } const tryParse = (t) => { try { return { obj: jsyaml.load(t), err: null }; } catch(e) { return { obj: null, err: e }; } }; // Stufe 1: direkt const r1 = tryParse(text); if (!r1.err) { setParsed(r1.obj); setFormatted(jsyaml.dump(r1.obj, {indent:2,lineWidth:-1,noRefs:true})); setError(''); setWarning(''); setCollapsed(new Set()); return; } // Stufe 2: Unit-Normalisierung (für kleine Fehler wie 4-space statt 2-space) const fixed1 = fixByUnit(text); const r2 = tryParse(fixed1); if (!r2.err) { setParsed(r2.obj); setFormatted(jsyaml.dump(r2.obj, {indent:2,lineWidth:-1,noRefs:true})); setWarning('✓ Einrückung normalisiert'); setError(''); setCollapsed(new Set()); return; } // Stufe 3: Stack-Fix (für stärker beschädigte Struktur) const fixed2 = fixByStack(text); const r3 = tryParse(fixed2); if (!r3.err) { setParsed(r3.obj); setFormatted(jsyaml.dump(r3.obj, {indent:2,lineWidth:-1,noRefs:true})); setWarning('⚠ Einrückung stark korrigiert – bitte Ergebnis prüfen'); setError(''); setCollapsed(new Set()); return; } // Stufe 4: best-effort – zeige korrigierten Text zum manuellen Nachbessern setError(shortError(r1.err)); setWarning('Struktur nicht automatisch reparierbar – normalisierter Text zum manuellen Korrigieren:'); setParsed(null); setFormatted(fixed2); }, []); const onInput = v => { setInput(v); process(v); }; const copy = async () => { try { await navigator.clipboard.writeText(formatted); setCopied(true); setTimeout(()=>setCopied(false),1500); } catch {} }; const toggleCollapse = path => { setCollapsed(prev => { const n = new Set(prev); n.has(path) ? n.delete(path) : n.add(path); return n; }); }; const collapseAll = () => { if(parsed) setCollapsed(new Set(collectPaths(parsed))); }; const expandAll = () => setCollapsed(new Set()); const colCols = mobile ? '1fr' : '1fr 1fr'; return (
{/* Input */}
{input&&}