DevTools JSON: typografische Anfuehrungszeichen fix, XML-Konvertierung, Builder-Anleitung

This commit is contained in:
2026-06-01 14:23:25 +02:00
parent 3cf5a689d6
commit 20c82f2fed

View File

@@ -1898,7 +1898,75 @@ function RegexBuilder({ toast, mobile }) {
// ──────────────────────────────────────────────────────────── JSON Tool ── // ──────────────────────────────────────────────────────────── JSON Tool ──
// ── JSON Viewer: rekursiver Baum mit auf/zuklappen ── // ──────────────────────────────────────────────────────────── JSON / XML Tool ──
// Typografische Anführungszeichen und andere häufige Probleme beheben
function sanitizeJson(raw) {
return raw
.replace(/[\u201C\u201D\u201E\u201F\u2018\u2019\u02BC]/g, '"') // „" "" usw → "
.replace(/[\u2018\u2019\u02BC\u0060]/g, '"') // ' ` → "
.replace(/,\s*([}\]])/g, '$1') // trailing commas
.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":'); // unquoted keys
}
// JSON → XML string
function jsonToXml(data, tag = 'root', indent = 0) {
const pad = ' '.repeat(indent);
if (data === null) return `${pad}<${tag} null="true"/>`;
if (typeof data !== 'object') {
return `${pad}<${tag}>${String(data).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</${tag}>`;
}
if (Array.isArray(data)) {
return data.map((v,i) => jsonToXml(v, 'item', indent)).join('\n');
}
const children = Object.entries(data)
.map(([k,v]) => jsonToXml(v, k.replace(/\s+/g,'_'), indent+1))
.join('\n');
return `${pad}<${tag}>\n${children}\n${pad}</${tag}>`;
}
// XML → JSON object (basic parser)
function xmlToJson(xml) {
const parser = new DOMParser();
const doc = parser.parseFromString(xml.trim(), 'application/xml');
const err = doc.querySelector('parsererror');
if (err) throw new Error('Ungültiges XML: ' + err.textContent.split('\n')[0]);
function nodeToObj(node) {
if (node.nodeType === 3) { // text
const t = node.textContent.trim();
return t === '' ? null : t;
}
if (node.childNodes.length === 0) return null;
if (node.childNodes.length === 1 && node.firstChild.nodeType === 3) {
return node.firstChild.textContent;
}
const obj = {};
const arr = {};
node.childNodes.forEach(child => {
if (child.nodeType !== 1) return;
const val = nodeToObj(child);
const name = child.nodeName;
if (name in arr) { arr[name]++; } else { arr[name] = 1; }
});
// build result
const seen = {};
node.childNodes.forEach(child => {
if (child.nodeType !== 1) return;
const name = child.nodeName;
const val = nodeToObj(child);
if (arr[name] > 1) {
if (!obj[name]) obj[name] = [];
obj[name].push(val);
} else {
obj[name] = val;
}
});
return obj;
}
return nodeToObj(doc.documentElement);
}
// ── JSON Viewer ──
function JsonNode({ data, depth, keyName }) { function JsonNode({ data, depth, keyName }) {
const [open, setOpen] = useState(depth < 2); const [open, setOpen] = useState(depth < 2);
const indent = depth * 16; const indent = depth * 16;
@@ -1906,223 +1974,196 @@ function JsonNode({ data, depth, keyName }) {
const isArr = Array.isArray(data); const isArr = Array.isArray(data);
const isPrim = !isObj && !isArr; const isPrim = !isObj && !isArr;
const typeColor = { const typeColor = { string:'#a8f0eb', number:'#ffe66d', boolean:'#ff6b9d', null:'rgba(255,255,255,0.3)' };
string: '#a8f0eb', function valColor(v) { if (v===null) return typeColor.null; return typeColor[typeof v]||'#fff'; }
number: '#ffe66d', function valLabel(v) { if (v===null) return 'null'; if (typeof v==='string') return `"${v}"`; return String(v); }
boolean: '#ff6b9d',
null: 'rgba(255,255,255,0.3)',
};
function valColor(v) {
if (v === null) return typeColor.null;
return typeColor[typeof v] || '#fff';
}
function valLabel(v) {
if (v === null) return 'null';
if (typeof v === 'string') return `"${v}"`;
return String(v);
}
function typeTag(v) { function typeTag(v) {
if (v === null) return 'null'; if (v===null) return 'null';
if (Array.isArray(v)) return `Array[${v.length}]`; if (Array.isArray(v)) return `Array[${v.length}]`;
if (typeof v === 'object') return `Object{${Object.keys(v).length}}`; if (typeof v==='object') return `Objekt {${Object.keys(v).length} Felder}`;
return typeof v; return typeof v;
} }
const keyStyle = {color:'#7eb8ff', fontFamily:"'Courier New',monospace", fontSize:12}; const keyStyle = {color:'#7eb8ff', fontFamily:"'Courier New',monospace", fontSize:12};
const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0', const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0', marginLeft:indent, lineHeight:1.7};
marginLeft:indent, lineHeight:1.7};
if (isPrim) { if (isPrim) return (
return (
<div style={lineBase}> <div style={lineBase}>
{keyName !== undefined && <span style={keyStyle}>"{keyName}"</span>} {keyName!==undefined && <><span style={keyStyle}>"{keyName}"</span><span style={{color:'rgba(255,255,255,0.3)'}}>:</span></>}
{keyName !== undefined && <span style={{color:'rgba(255,255,255,0.3)'}}>:</span>} <span style={{color:valColor(data), fontFamily:"'Courier New',monospace", fontSize:12}}>{valLabel(data)}</span>
<span style={{color:valColor(data), fontFamily:"'Courier New',monospace", fontSize:12}}> <span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>{typeof data==='object'?'null':typeof data}</span>
{valLabel(data)}
</span>
<span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>{typeof data}</span>
</div> </div>
); );
}
const entries = isArr ? data.map((v,i)=>[i,v]) : Object.entries(data); const entries = isArr ? data.map((v,i)=>[i,v]) : Object.entries(data);
const bracket = isArr ? ['[',']'] : ['{','}']; const [o,c] = isArr ? ['[',']'] : ['{','}'];
return ( return (
<div> <div>
<div style={{...lineBase, cursor:'pointer', userSelect:'none'}} onClick={()=>setOpen(o=>!o)}> <div style={{...lineBase, cursor:'pointer', userSelect:'none'}} onClick={()=>setOpen(x=>!x)}>
<span style={{color:'rgba(255,255,255,0.3)', fontSize:11, minWidth:14}}> <span style={{color:'rgba(255,255,255,0.3)', fontSize:11, minWidth:14}}>{open?'▾':'▸'}</span>
{open ? '▾' : '▸'} {keyName!==undefined && <><span style={keyStyle}>"{keyName}"</span><span style={{color:'rgba(255,255,255,0.3)'}}>:</span></>}
</span> <span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>{o}</span>
{keyName !== undefined && <span style={keyStyle}>"{keyName}"</span>} {!open && <span style={{color:'rgba(255,255,255,0.25)', fontFamily:"'Courier New',monospace", fontSize:11}}>{isArr?`${entries.length} Einträge`:`${entries.length} Felder`}</span>}
{keyName !== undefined && <span style={{color:'rgba(255,255,255,0.3)'}}>:</span>} {!open && <span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>{c}</span>}
<span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}> <span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>{typeTag(data)}</span>
{bracket[0]}
</span>
{!open && (
<span style={{color:'rgba(255,255,255,0.25)', fontFamily:"'Courier New',monospace", fontSize:11}}>
{isArr ? `${entries.length} Einträge` : `${entries.length} Felder`}
</span>
)}
{!open && <span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>{bracket[1]}</span>}
<span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>
{typeTag(data)}
</span>
</div> </div>
{open && ( {open && <>
<> {entries.map(([k,v]) => <JsonNode key={k} data={v} depth={depth+1} keyName={k}/>)}
{entries.map(([k,v]) => ( <div style={{marginLeft:indent, color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>{c}</div>
<JsonNode key={k} data={v} depth={depth+1} keyName={k}/> </>}
))}
<div style={{marginLeft:indent, color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>
{bracket[1]}
</div>
</>
)}
</div> </div>
); );
} }
// ── JSON Builder: Baumstruktur erstellen ── // ── JSON Builder ──
let _nodeId = 1; let _nid = 1;
function newNode(type='string', key='', value='') { function newNode(type='string') { return {id:_nid++, type, key:'', value:'', children:[]}; }
return { id: _nodeId++, type, key, value, children:[] };
}
function BuilderNode({ node, depth, onChange, onDelete, onAddSibling }) {
const indent = depth * 20;
const canHaveChildren = node.type==='object' || node.type==='array';
function addChild(type) {
const child = newNode(type);
onChange({...node, children:[...node.children, child]});
}
function updateChild(updated) {
onChange({...node, children: node.children.map(c => c.id===updated.id ? updated : c)});
}
function deleteChild(id) {
onChange({...node, children: node.children.filter(c => c.id!==id)});
}
const inp = {...S.inp, fontSize:12, padding:'4px 7px', boxSizing:'border-box'};
const typeColors = {object:'#4ecdc4', array:'#ffe66d', string:'#a8f0eb', number:'#ff6b9d', boolean:'#c3b1e1', null:'rgba(255,255,255,0.3)'};
return (
<div style={{marginLeft:indent, marginBottom:3}}>
<div style={{display:'flex', alignItems:'center', gap:6, flexWrap:'wrap'}}>
{/* Key (only for object children) */}
{depth > 0 && node.type !== '__root' && (
<input style={{...inp, width:90, color:'#7eb8ff'}} placeholder='Key'
value={node.key} onChange={e=>onChange({...node, key:e.target.value})}/>
)}
{depth > 0 && <span style={{color:'rgba(255,255,255,0.25)', fontSize:11}}>:</span>}
{/* Type */}
<select style={{...inp, width:'auto', color: typeColors[node.type]||'#fff', cursor:'pointer'}}
value={node.type} onChange={e=>onChange({...node, type:e.target.value, children:[], value:''})}>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="null">null</option>
<option value="object">object {'{}'}</option>
<option value="array">array []</option>
</select>
{/* Value for primitives */}
{node.type==='string' && <input style={{...inp, flex:1, minWidth:80}} placeholder='"Wert"' value={node.value} onChange={e=>onChange({...node, value:e.target.value})}/>}
{node.type==='number' && <input style={{...inp, width:100}} type="number" placeholder='0' value={node.value} onChange={e=>onChange({...node, value:e.target.value})}/>}
{node.type==='boolean' && (
<select style={{...inp, width:'auto', cursor:'pointer'}} value={node.value} onChange={e=>onChange({...node, value:e.target.value})}>
<option value="true">true</option>
<option value="false">false</option>
</select>
)}
{/* Add child button */}
{canHaveChildren && (
<select style={{...inp, width:'auto', cursor:'pointer', color:'#4ecdc4'}}
value="" onChange={e=>{ if(e.target.value) addChild(e.target.value); }}>
<option value="">+ Feld hinzufügen</option>
<option value="string">string</option>
<option value="number">number</option>
<option value="boolean">boolean</option>
<option value="null">null</option>
<option value="object">object</option>
<option value="array">array</option>
</select>
)}
{/* Delete */}
{depth > 0 && (
<button onClick={onDelete} style={{background:'rgba(255,107,157,0.12)',border:'none',
borderRadius:5,color:'#ff6b9d',cursor:'pointer',padding:'3px 8px',fontSize:12}}>✕</button>
)}
</div>
{/* Children */}
{canHaveChildren && node.children.map(child=>(
<BuilderNode key={child.id} node={child} depth={depth+1}
onChange={updateChild} onDelete={()=>deleteChild(child.id)}/>
))}
</div>
);
}
function nodeToJson(node) { function nodeToJson(node) {
if (node.type==='object') { if (node.type==='object') { const o={}; node.children.forEach(c=>{o[c.key||`feld${c.id}`]=nodeToJson(c);}); return o; }
const obj = {}; if (node.type==='array') { return node.children.map(c=>nodeToJson(c)); }
node.children.forEach(c => { obj[c.key || `field${c.id}`] = nodeToJson(c); }); if (node.type==='number') return isNaN(Number(node.value))?0:Number(node.value);
return obj; if (node.type==='boolean') return node.value!=='false';
}
if (node.type==='array') {
return node.children.map(c => nodeToJson(c));
}
if (node.type==='number') return isNaN(Number(node.value)) ? 0 : Number(node.value);
if (node.type==='boolean') return node.value !== 'false';
if (node.type==='null') return null; if (node.type==='null') return null;
return node.value; return node.value;
} }
const TC = {object:'#4ecdc4', array:'#ffe66d', string:'#a8f0eb', number:'#ff6b9d', boolean:'#c3b1e1', null:'rgba(255,255,255,0.4)'};
function BuilderNode({node, depth, onChange, onDelete}) {
const inp = {...S.inp, fontSize:12, padding:'4px 7px', boxSizing:'border-box'};
const canKids = node.type==='object'||node.type==='array';
const isRoot = depth===0;
function addChild(type) { onChange({...node, children:[...node.children, newNode(type)]}); }
function updChild(u) { onChange({...node, children:node.children.map(c=>c.id===u.id?u:c)}); }
function delChild(id) { onChange({...node, children:node.children.filter(c=>c.id!==id)}); }
return (
<div style={{marginLeft:depth*18, marginBottom:4}}>
<div style={{display:'flex', alignItems:'center', gap:6, flexWrap:'wrap'}}>
{!isRoot && (
<input style={{...inp, width:90, color:'#7eb8ff'}} placeholder="Key / Name"
value={node.key} onChange={e=>onChange({...node, key:e.target.value})}/>
)}
{!isRoot && <span style={{color:'rgba(255,255,255,0.25)', fontSize:11}}>:</span>}
<select style={{...inp, width:'auto', color:TC[node.type], cursor:'pointer'}}
value={node.type} onChange={e=>onChange({...node, type:e.target.value, children:[], value:''})}>
<option value="string">Text (string)</option>
<option value="number">Zahl (number)</option>
<option value="boolean">Ja/Nein (boolean)</option>
<option value="null">Leer (null)</option>
<option value="object">Objekt {'{ }'}</option>
<option value="array">Liste [ ]</option>
</select>
{node.type==='string' && <input style={{...inp,flex:1,minWidth:80}} placeholder="Wert eingeben" value={node.value} onChange={e=>onChange({...node,value:e.target.value})}/>}
{node.type==='number' && <input style={{...inp,width:100}} type="number" placeholder="0" value={node.value} onChange={e=>onChange({...node,value:e.target.value})}/>}
{node.type==='boolean' && <select style={{...inp,width:'auto',cursor:'pointer'}} value={node.value} onChange={e=>onChange({...node,value:e.target.value})}><option value="true">true (ja)</option><option value="false">false (nein)</option></select>}
{node.type==='null' && <span style={{color:'rgba(255,255,255,0.3)',fontSize:11,fontFamily:'monospace'}}>null (kein Wert)</span>}
{canKids && (
<select style={{...inp,width:'auto',cursor:'pointer',color:'#4ecdc4'}} value="" onChange={e=>{if(e.target.value)addChild(e.target.value);}}>
<option value="">+ Feld hinzufügen</option>
<option value="string">Text</option>
<option value="number">Zahl</option>
<option value="boolean">Ja/Nein</option>
<option value="null">Leer (null)</option>
<option value="object">Objekt</option>
<option value="array">Liste</option>
</select>
)}
{!isRoot && <button onClick={onDelete} style={{background:'rgba(255,107,157,0.12)',border:'none',borderRadius:5,color:'#ff6b9d',cursor:'pointer',padding:'3px 8px',fontSize:12}}></button>}
</div>
{canKids && node.children.map(child=>(
<BuilderNode key={child.id} node={child} depth={depth+1} onChange={updChild} onDelete={()=>delChild(child.id)}/>
))}
</div>
);
}
// ── Main Tool ──
function JsonTool({ toast, mobile }) { function JsonTool({ toast, mobile }) {
const [tab, setTab] = useState('view'); const [tab, setTab] = useState('view');
// View tab const [fmt, setFmt] = useState('json'); // 'json' | 'xml'
const [jsonInput, setJsonInput] = useState('');
// View / Convert
const [rawInput, setRawInput] = useState('');
const [parsed, setParsed] = useState(null); const [parsed, setParsed] = useState(null);
const [parseErr, setParseErr] = useState(''); const [parseErr, setParseErr] = useState('');
const [xmlOut, setXmlOut] = useState('');
const [copiedOut, setCopiedOut] = useState(false);
function parseJson(val) { function doParseJson(val) {
setJsonInput(val); setRawInput(val);
if (!val.trim()) { setParsed(null); setParseErr(''); return; } setParseErr(''); setParsed(null); setXmlOut('');
if (!val.trim()) return;
const fixed = sanitizeJson(val);
try { try {
setParsed(JSON.parse(val)); const p = JSON.parse(fixed);
setParseErr(''); setParsed(p);
setXmlOut(jsonToXml(p));
} catch(e) { } catch(e) {
setParsed(null);
setParseErr(e.message); setParseErr(e.message);
} }
} }
function formatJson() { function doParseXml(val) {
setRawInput(val);
setParseErr(''); setParsed(null); setXmlOut('');
if (!val.trim()) return;
try { try {
const p = JSON.parse(jsonInput); const obj = xmlToJson(val);
setJsonInput(JSON.stringify(p, null, 2)); setParsed(obj);
setParsed(p); setXmlOut(val);
setParseErr(''); } catch(e) {
} catch {} setParseErr(e.message);
}
} }
// Build tab function handleInput(val) {
const [root, setRoot] = useState(() => ({...newNode('object'), key:'root'})); if (fmt==='json') doParseJson(val);
const generatedJson = JSON.stringify(nodeToJson(root), null, 2); else doParseXml(val);
const [copiedJson, setCopiedJson] = useState(false); }
async function copyJson(text) { function formatJson() {
try { await navigator.clipboard.writeText(text); setCopiedJson(true); setTimeout(()=>setCopiedJson(false),1800); } if (!parsed) return;
setRawInput(JSON.stringify(parsed, null, 2));
}
async function copyText(text) {
try { await navigator.clipboard.writeText(text); setCopiedOut(true); setTimeout(()=>setCopiedOut(false),1800); }
catch { toast('Kopieren fehlgeschlagen'); }
}
// Builder
const [root, setRoot] = useState(() => ({...newNode('object'), key:'root'}));
const [buildFmt, setBuildFmt] = useState('json');
const [copiedBld, setCopiedBld] = useState(false);
const builtJson = JSON.stringify(nodeToJson(root), null, 2);
const builtXml = (() => { try { return jsonToXml(nodeToJson(root)); } catch { return ''; } })();
const builtOut = buildFmt==='json' ? builtJson : builtXml;
async function copyBuilt() {
try { await navigator.clipboard.writeText(builtOut); setCopiedBld(true); setTimeout(()=>setCopiedBld(false),1800); }
catch { toast('Kopieren fehlgeschlagen'); } catch { toast('Kopieren fehlgeschlagen'); }
} }
const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'}; const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
const tabs = [{id:'view',label:'👁 JSON ansehen'},{id:'build',label:'🔨 JSON bauen'}]; const tabs = [{id:'view',label:'👁 Ansehen & Konvertieren'},{id:'build',label:'🔨 Struktur bauen'}];
const FmtToggle = ({value, onChange}) => (
<div style={{display:'flex',gap:4}}>
{['json','xml'].map(f=>(
<button key={f} onClick={()=>onChange(f)} style={{
padding:'4px 12px',borderRadius:16,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background:value===f?'#4ecdc4':'rgba(255,255,255,0.05)',
color:value===f?'#0d0d0f':'rgba(255,255,255,0.55)',
border:value===f?'none':'1px solid rgba(255,255,255,0.1)',
fontWeight:value===f?700:400}}>{f.toUpperCase()}</button>
))}
</div>
);
return ( return (
<div> <div>
@@ -2139,28 +2180,78 @@ function JsonTool({ toast, mobile }) {
{tab==='view' && ( {tab==='view' && (
<div> <div>
<div style={{marginBottom:10}}> {/* Anleitung */}
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:5}}>JSON einfügen</div> <div style={{padding:'10px 12px',borderRadius:8,background:'rgba(78,205,196,0.06)',
<textarea style={{...inp, width:'100%', minHeight:120, fontFamily:"'Courier New',monospace", border:'1px solid rgba(78,205,196,0.15)',marginBottom:14}}>
<div style={{color:'#4ecdc4',fontFamily:'monospace',fontSize:10,fontWeight:700,marginBottom:4}}>SO FUNKTIONIERT ES</div>
<div style={{color:'rgba(255,255,255,0.6)',fontSize:12,lineHeight:1.7}}>
JSON oder XML einfügen der Baum unten zeigt die Struktur auf/zuklappbar.<br/>
Pfeile anklicken zum Auf-/Zuklappen. Konvertierung in das andere Format erscheint automatisch.<br/>
<span style={{color:'rgba(255,255,255,0.4)',fontSize:11}}>Tipp: Typografische Anführungszeichen " werden automatisch korrigiert.</span>
</div>
</div>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8,flexWrap:'wrap'}}>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>EINGABE-FORMAT:</span>
<FmtToggle value={fmt} onChange={f=>{setFmt(f);setRawInput('');setParsed(null);setParseErr('');setXmlOut('');}}/>
{!!rawInput && <button style={{...S.btn('#ff6b9d',false),fontSize:10,padding:'4px 12px'}} onClick={()=>{setRawInput('');setParsed(null);setParseErr('');setXmlOut('');}}>✕ Leeren</button>}
</div>
<textarea style={{...inp, width:'100%', minHeight:130, fontFamily:"'Courier New',monospace",
fontSize:12, resize:'vertical', lineHeight:1.5, fontSize:12, resize:'vertical', lineHeight:1.5,
border: parseErr ? '1px solid rgba(255,107,157,0.5)' : undefined}} border: parseErr?'1px solid rgba(255,107,157,0.5)':undefined}}
placeholder='{"name":"Max","age":30}' placeholder={fmt==='json'
value={jsonInput} onChange={e=>parseJson(e.target.value)}/> ? '{\n "name": "Max",\n "alter": 30,\n "aktiv": true\n}'
{!!parseErr && <div style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:11,marginTop:4}}>⚠ {parseErr}</div>} : '<person>\n <name>Max</name>\n <alter>30</alter>\n</person>'}
</div> value={rawInput} onChange={e=>handleInput(e.target.value)}/>
<div style={{display:'flex',gap:8,marginBottom:14}}>
{!!jsonInput && <button style={{...S.btn('#4ecdc4',false),fontSize:11,padding:'5px 14px'}} onClick={formatJson}>⟳ Formatieren</button>} {!!parseErr && <div style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:11,marginTop:4,padding:'6px 10px',background:'rgba(255,107,157,0.08)',borderRadius:6}}>
{!!jsonInput && <button style={{...S.btn('#ff6b9d',false),fontSize:11,padding:'5px 14px'}} onClick={()=>{setJsonInput('');setParsed(null);setParseErr('');}}>✕ Leeren</button>} ⚠ {parseErr}
</div> </div>}
{!!rawInput && !parseErr && fmt==='json' && (
<button style={{...S.btn('#4ecdc4',false),fontSize:11,padding:'5px 14px',marginTop:8}}
onClick={formatJson}>⟳ Formatieren / Einrücken</button>
)}
{parsed !== null && ( {parsed !== null && (
<div style={{borderRadius:9,background:'rgba(0,0,0,0.2)',border:'1px solid rgba(255,255,255,0.08)', <>
padding:'10px 14px',overflowX:'auto'}}> {/* Interaktiver Baum */}
<div style={{marginTop:14,borderRadius:9,background:'rgba(0,0,0,0.2)',
border:'1px solid rgba(255,255,255,0.08)',padding:'10px 14px',overflowX:'auto',marginBottom:14}}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:8}}>
STRUKTUR — Pfeile anklicken zum Auf-/Zuklappen
</div>
<JsonNode data={parsed} depth={0}/> <JsonNode data={parsed} depth={0}/>
</div> </div>
{/* Konvertierung */}
<div style={{borderRadius:9,background:'rgba(255,255,255,0.03)',
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px'}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8,flexWrap:'wrap'}}>
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
KONVERTIERT ALS {fmt==='json'?'XML':'JSON'}
</span>
<button onClick={()=>copyText(fmt==='json'?xmlOut:JSON.stringify(parsed,null,2))} style={{
background:'rgba(78,205,196,0.1)',border:'1px solid rgba(78,205,196,0.2)',
borderRadius:6,color:copiedOut?'#4ecdc4':'rgba(255,255,255,0.5)',
cursor:'pointer',padding:'2px 10px',fontSize:11}}>
{copiedOut?'✓ Kopiert':'⎘ Kopieren'}
</button>
</div>
<pre style={{margin:0,color:'rgba(255,255,255,0.75)',fontFamily:"'Courier New',monospace",
fontSize:12,lineHeight:1.6,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-word',
maxHeight:300}}>
{fmt==='json' ? xmlOut : JSON.stringify(parsed,null,2)}
</pre>
</div>
</>
)} )}
{!jsonInput && (
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,padding:'16px 0',textAlign:'center'}}> {!rawInput && (
JSON oben einfügen → interaktiver Baum erscheint hier <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,
padding:'16px 0',textAlign:'center'}}>
{fmt==='json'?'JSON':'XML'} oben einfügen → interaktiver Baum + Konvertierung erscheint
</div> </div>
)} )}
</div> </div>
@@ -2168,32 +2259,48 @@ function JsonTool({ toast, mobile }) {
{tab==='build' && ( {tab==='build' && (
<div> <div>
{/* Anleitung */}
<div style={{padding:'10px 12px',borderRadius:8,background:'rgba(78,205,196,0.06)',
border:'1px solid rgba(78,205,196,0.15)',marginBottom:14}}>
<div style={{color:'#4ecdc4',fontFamily:'monospace',fontSize:10,fontWeight:700,marginBottom:4}}>SO FUNKTIONIERT DER BUILDER</div>
<div style={{color:'rgba(255,255,255,0.6)',fontSize:12,lineHeight:1.7}}>
Klicke „+ Feld hinzufügen" um Felder zum Objekt oder zur Liste hinzuzufügen.<br/>
<strong style={{color:'rgba(255,255,255,0.8)'}}>Typen:</strong> <span style={{color:'#a8f0eb'}}>Text</span> = normaler Text · <span style={{color:'#ffe66d'}}>Zahl</span> = Nummer · <span style={{color:'#ff6b9d'}}>Ja/Nein</span> = true/false · <span style={{color:'rgba(255,255,255,0.4)'}}>Leer</span> = null<br/>
<span style={{color:'#4ecdc4'}}>Objekt {'{ }'}</span> = verschachtelter Datensatz (z.B. Adresse mit Straße, PLZ) ·
<span style={{color:'#ffe66d'}}> Liste [ ]</span> = mehrere gleichartige Einträge (z.B. Mitarbeiterliste)<br/>
<span style={{color:'rgba(255,255,255,0.4)',fontSize:11}}>Das fertige JSON/XML erscheint unten und kann kopiert werden.</span>
</div>
</div>
<div style={{borderRadius:9,background:'rgba(255,255,255,0.03)', <div style={{borderRadius:9,background:'rgba(255,255,255,0.03)',
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px',marginBottom:14, border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px',marginBottom:14,overflowX:'auto'}}>
overflowX:'auto'}}> <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:10}}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:10}}>STRUKTUR AUFBAUEN</div> STRUKTUR AUFBAUEN Felder hinzufügen, benennen, verschachteln
</div>
<BuilderNode node={root} depth={0} onChange={setRoot} onDelete={()=>{}}/> <BuilderNode node={root} depth={0} onChange={setRoot} onDelete={()=>{}}/>
</div> </div>
<div style={{borderRadius:9,background:'rgba(0,0,0,0.2)', <div style={{borderRadius:9,background:'rgba(0,0,0,0.2)',
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px'}}> border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px'}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}> <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8,flexWrap:'wrap'}}>
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>GENERIERTES JSON</span> <span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>AUSGABE:</span>
<button onClick={()=>copyJson(generatedJson)} style={{ <FmtToggle value={buildFmt} onChange={setBuildFmt}/>
<button onClick={copyBuilt} style={{
background:'rgba(78,205,196,0.1)',border:'1px solid rgba(78,205,196,0.2)', background:'rgba(78,205,196,0.1)',border:'1px solid rgba(78,205,196,0.2)',
borderRadius:6,color:copiedJson?'#4ecdc4':'rgba(255,255,255,0.5)', borderRadius:6,color:copiedBld?'#4ecdc4':'rgba(255,255,255,0.5)',
cursor:'pointer',padding:'2px 10px',fontSize:11}}> cursor:'pointer',padding:'2px 10px',fontSize:11}}>
{copiedJson?'✓ Kopiert':'⎘ Kopieren'} {copiedBld?'✓ Kopiert':'⎘ Kopieren'}
</button> </button>
<button onClick={()=>{setTab('view');parseJson(generatedJson);}} style={{ <button onClick={()=>{setTab('view');setFmt('json');doParseJson(builtJson);}} style={{
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)', background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:6,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'2px 10px',fontSize:11}}> borderRadius:6,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'2px 10px',fontSize:11}}>
👁 Ansehen 👁 Im Viewer öffnen
</button> </button>
</div> </div>
<pre style={{margin:0,color:'rgba(255,255,255,0.8)',fontFamily:"'Courier New',monospace", <pre style={{margin:0,color:'rgba(255,255,255,0.8)',fontFamily:"'Courier New',monospace",
fontSize:12,lineHeight:1.6,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-all'}}> fontSize:12,lineHeight:1.6,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-word',
{generatedJson} maxHeight:400}}>
{builtOut}
</pre> </pre>
</div> </div>
</div> </div>
@@ -2203,6 +2310,7 @@ function JsonTool({ toast, mobile }) {
} }
const TOOL_DEFS = [ const TOOL_DEFS = [
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true, {id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}], tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],