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&&}
{/* Output */}
{/* View toggle */}
{['tree','raw'].map(v=>(
))}
{parsed&&view==='tree'&&<>
>}
{formatted&&}
{error && !formatted ? (
⚠ {error}
) : (<>
{warning && (
{error ? '⚠ ' : '✓ '}{warning}
)}
{error && formatted && (
⚠ {error}
)}
{view==='raw' ? (
{formatted||'# Ergebnis erscheint hier'}
) : parsed!==null ? (
) : (
YAML eingeben → wird live formatiert
)}
>)}
Powered by js-yaml · 2-Space-Einrückung · YAML 1.2 kompatibel
);
}
// ── Tool-Definitionen ─────────────────────────────────────────────────────────
const TOOL_DEFS = [
{ id:'yaml', label:'📄 YAML Formatter', desc:'YAML korrekt einrücken und formatieren', ready:true, component:YamlTool },
{ id:'json', label:'{ } JSON Formatter', desc:'JSON formatieren und validieren', ready:false },
{ id:'base64', label:'🔐 Base64', desc:'Text ↔ Base64 kodieren/dekodieren', ready:false },
{ id:'regex', label:'🔍 Regex Tester', desc:'Reguläre Ausdrücke live testen', ready:false },
{ id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:false },
{ id:'jwt', label:'🔑 JWT Decoder', desc:'JSON Web Tokens dekodieren', ready:false },
{ id:'timestamp', label:'🕐 Timestamp', desc:'Unix Timestamps umrechnen', ready:false },
{ id:'color', label:'🎨 Farben', desc:'HEX ↔ RGB ↔ HSL umrechnen', ready:false },
];
export default function DevTools({ toast, mobile }) {
const [activeTool, setActiveTool] = useState('yaml');
const active = TOOL_DEFS.find(t=>t.id===activeTool);
return (
Dev-Tools
Konverter · Formatter · Helfer
{TOOL_DEFS.map(t=>(
))}
{active?.label}
{active?.desc}
{active?.component &&
}
);
}