diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx
index 1b62d05..031288e 100644
--- a/frontend/src/tools/devtools.jsx
+++ b/frontend/src/tools/devtools.jsx
@@ -1898,7 +1898,75 @@ function RegexBuilder({ toast, mobile }) {
// ──────────────────────────────────────────────────────────── 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,'&').replace(//g,'>')}${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 }) {
const [open, setOpen] = useState(depth < 2);
const indent = depth * 16;
@@ -1906,223 +1974,196 @@ function JsonNode({ data, depth, keyName }) {
const isArr = Array.isArray(data);
const isPrim = !isObj && !isArr;
- const typeColor = {
- string: '#a8f0eb',
- number: '#ffe66d',
- 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);
- }
-
+ const typeColor = { string:'#a8f0eb', number:'#ffe66d', 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) {
- if (v === null) return 'null';
+ if (v===null) return 'null';
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;
}
const keyStyle = {color:'#7eb8ff', fontFamily:"'Courier New',monospace", fontSize:12};
- const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0',
- marginLeft:indent, lineHeight:1.7};
+ const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0', marginLeft:indent, lineHeight:1.7};
- if (isPrim) {
- return (
-
-
setOpen(o=>!o)}>
-
- {open ? '▾' : '▸'}
-
- {keyName !== undefined &&
"{keyName}" }
- {keyName !== undefined &&
: }
-
- {bracket[0]}
-
- {!open && (
-
- {isArr ? `${entries.length} Einträge` : `${entries.length} Felder`}
-
- )}
- {!open &&
{bracket[1]} }
-
- {typeTag(data)}
-
+
setOpen(x=>!x)}>
+ {open?'▾':'▸'}
+ {keyName!==undefined && <>"{keyName}" : >}
+ {o}
+ {!open && {isArr?`${entries.length} Einträge`:`${entries.length} Felder`} }
+ {!open && {c} }
+ {typeTag(data)}
- {open && (
- <>
- {entries.map(([k,v]) => (
-
- ))}
-
- {bracket[1]}
-
- >
- )}
+ {open && <>
+ {entries.map(([k,v]) => )}
+ {c}
+ >}
);
}
-// ── JSON Builder: Baumstruktur erstellen ──
-let _nodeId = 1;
-function newNode(type='string', key='', value='') {
- return { id: _nodeId++, type, key, value, children:[] };
+// ── JSON Builder ──
+let _nid = 1;
+function newNode(type='string') { return {id:_nid++, type, key:'', value:'', children:[]}; }
+
+function nodeToJson(node) {
+ if (node.type==='object') { const o={}; node.children.forEach(c=>{o[c.key||`feld${c.id}`]=nodeToJson(c);}); return o; }
+ 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;
+ return node.value;
}
-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 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 typeColors = {object:'#4ecdc4', array:'#ffe66d', string:'#a8f0eb', number:'#ff6b9d', boolean:'#c3b1e1', null:'rgba(255,255,255,0.3)'};
+ 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 (
-
+
- {/* Key (only for object children) */}
- {depth > 0 && node.type !== '__root' && (
- onChange({...node, key:e.target.value})}/>
)}
- {depth > 0 && : }
- {/* Type */}
- :}
+ onChange({...node, type:e.target.value, children:[], value:''})}>
- string
- number
- boolean
- null
- object {'{}'}
- array []
+ Text (string)
+ Zahl (number)
+ Ja/Nein (boolean)
+ Leer (null)
+ Objekt {'{ }'}
+ Liste [ ]
- {/* Value for primitives */}
- {node.type==='string' && onChange({...node, value:e.target.value})}/>}
- {node.type==='number' && onChange({...node, value:e.target.value})}/>}
- {node.type==='boolean' && (
- onChange({...node, value:e.target.value})}>
- true
- false
-
- )}
- {/* Add child button */}
- {canHaveChildren && (
- { if(e.target.value) addChild(e.target.value); }}>
+ {node.type==='string' && onChange({...node,value:e.target.value})}/>}
+ {node.type==='number' && onChange({...node,value:e.target.value})}/>}
+ {node.type==='boolean' && onChange({...node,value:e.target.value})}>true (ja) false (nein) }
+ {node.type==='null' && null (kein Wert) }
+ {canKids && (
+ {if(e.target.value)addChild(e.target.value);}}>
+ Feld hinzufügen
- string
- number
- boolean
- null
- object
- array
+ Text
+ Zahl
+ Ja/Nein
+ Leer (null)
+ Objekt
+ Liste
)}
- {/* Delete */}
- {depth > 0 && (
- ✕
- )}
+ {!isRoot && ✕ }
- {/* Children */}
- {canHaveChildren && node.children.map(child=>(
-
deleteChild(child.id)}/>
+ {canKids && node.children.map(child=>(
+ delChild(child.id)}/>
))}
);
}
-function nodeToJson(node) {
- if (node.type==='object') {
- const obj = {};
- node.children.forEach(c => { obj[c.key || `field${c.id}`] = nodeToJson(c); });
- return obj;
- }
- 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;
- return node.value;
-}
-
+// ── Main Tool ──
function JsonTool({ toast, mobile }) {
- const [tab, setTab] = useState('view');
- // View tab
- const [jsonInput, setJsonInput] = useState('');
- const [parsed, setParsed] = useState(null);
- const [parseErr, setParseErr] = useState('');
+ const [tab, setTab] = useState('view');
+ const [fmt, setFmt] = useState('json'); // 'json' | 'xml'
- function parseJson(val) {
- setJsonInput(val);
- if (!val.trim()) { setParsed(null); setParseErr(''); return; }
+ // View / Convert
+ const [rawInput, setRawInput] = useState('');
+ const [parsed, setParsed] = useState(null);
+ const [parseErr, setParseErr] = useState('');
+ const [xmlOut, setXmlOut] = useState('');
+ const [copiedOut, setCopiedOut] = useState(false);
+
+ function doParseJson(val) {
+ setRawInput(val);
+ setParseErr(''); setParsed(null); setXmlOut('');
+ if (!val.trim()) return;
+ const fixed = sanitizeJson(val);
try {
- setParsed(JSON.parse(val));
- setParseErr('');
+ const p = JSON.parse(fixed);
+ setParsed(p);
+ setXmlOut(jsonToXml(p));
} catch(e) {
- setParsed(null);
setParseErr(e.message);
}
}
- function formatJson() {
+ function doParseXml(val) {
+ setRawInput(val);
+ setParseErr(''); setParsed(null); setXmlOut('');
+ if (!val.trim()) return;
try {
- const p = JSON.parse(jsonInput);
- setJsonInput(JSON.stringify(p, null, 2));
- setParsed(p);
- setParseErr('');
- } catch {}
+ const obj = xmlToJson(val);
+ setParsed(obj);
+ setXmlOut(val);
+ } catch(e) {
+ setParseErr(e.message);
+ }
}
- // Build tab
- const [root, setRoot] = useState(() => ({...newNode('object'), key:'root'}));
- const generatedJson = JSON.stringify(nodeToJson(root), null, 2);
- const [copiedJson, setCopiedJson] = useState(false);
+ function handleInput(val) {
+ if (fmt==='json') doParseJson(val);
+ else doParseXml(val);
+ }
- async function copyJson(text) {
- try { await navigator.clipboard.writeText(text); setCopiedJson(true); setTimeout(()=>setCopiedJson(false),1800); }
+ function formatJson() {
+ 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'); }
}
- const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
- const tabs = [{id:'view',label:'👁 JSON ansehen'},{id:'build',label:'🔨 JSON bauen'}];
+ // 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'); }
+ }
+
+ const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
+ const tabs = [{id:'view',label:'👁 Ansehen & Konvertieren'},{id:'build',label:'🔨 Struktur bauen'}];
+
+ const FmtToggle = ({value, onChange}) => (
+
+ {['json','xml'].map(f=>(
+ 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()}
+ ))}
+
+ );
return (
@@ -2139,28 +2180,78 @@ function JsonTool({ toast, mobile }) {
{tab==='view' && (
-
-
- {!!jsonInput && ⟳ Formatieren }
- {!!jsonInput && {setJsonInput('');setParsed(null);setParseErr('');}}>✕ Leeren }
-
- {parsed !== null && (
-
-
+ {/* Anleitung */}
+
+
SO FUNKTIONIERT ES
+
+ JSON oder XML einfügen → der Baum unten zeigt die Struktur auf/zuklappbar.
+ Pfeile ▸ anklicken zum Auf-/Zuklappen. Konvertierung in das andere Format erscheint automatisch.
+ Tipp: Typografische Anführungszeichen „" werden automatisch korrigiert.
+
+
+
+ EINGABE-FORMAT:
+ {setFmt(f);setRawInput('');setParsed(null);setParseErr('');setXmlOut('');}}/>
+ {!!rawInput && {setRawInput('');setParsed(null);setParseErr('');setXmlOut('');}}>✕ Leeren }
+
+
+