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: normalisiert Einrückung bevor js-yaml parst ─────────────── function fixYamlIndentation(text) { const lines = text.split('\n'); // 1. Tabs → Spaces (4 oder 2) const detabbed = lines.map(l => l.replace(/\t/g, ' ')); // 2. Finde die kleinste vorkommende Einrückung > 0 (das ist die Einheit) const indents = detabbed .filter(l => l.trim() && !l.trim().startsWith('#')) .map(l => l.search(/\S/)) .filter(n => n > 0); const unit = indents.length ? Math.min(...indents) : 2; // 3. Remap: jede Einrückung → Niveau * 2 Spaces const remapped = detabbed.map(line => { if (!line.trim()) return ''; if (line.trim().startsWith('#')) return line.trimEnd(); const spaces = line.search(/\S/); if (spaces <= 0) return line.trimEnd(); const level = Math.round(spaces / unit); return ' '.repeat(level) + line.trimStart().trimEnd(); }); return remapped.join('\n'); } // ── 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; } let obj, parseError = null; // Versuch 1: direkt parsen try { obj = jsyaml.load(text); } catch(e) { parseError = e; } // Versuch 2: wenn Fehler → Auto-Fix, dann nochmal if (parseError) { try { const fixed = fixYamlIndentation(text); obj = jsyaml.load(fixed); setWarning(`⚠ Einrückung automatisch korrigiert: ${parseError.message.split('\n')[0]}`); } catch(e2) { // Fix hat auch nicht geholfen – originalen Fehler zeigen setError(parseError.message); setWarning(''); setParsed(null); setFormatted(''); return; } } else { setWarning(''); } try { setParsed(obj); setFormatted(jsyaml.dump(obj, { indent:2, lineWidth:-1, noRefs:true })); setError(''); setCollapsed(new Set()); } catch(e) { setError(e.message); setParsed(null); setFormatted(''); } },[]); 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&&}