From 20c82f2fedec1a6b123a3f11e4df7aad5c6ea6a1 Mon Sep 17 00:00:00 2001 From: Dicken Date: Mon, 1 Jun 2026 14:23:25 +0200 Subject: [PATCH] DevTools JSON: typografische Anfuehrungszeichen fix, XML-Konvertierung, Builder-Anleitung --- frontend/src/tools/devtools.jsx | 504 +++++++++++++++++++------------- 1 file changed, 306 insertions(+), 198 deletions(-) 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,'>')}`; + } + 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}`; +} + +// 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 ( -
- {keyName !== undefined && "{keyName}"} - {keyName !== undefined && :} - - {valLabel(data)} - - {typeof data} -
- ); - } + if (isPrim) return ( +
+ {keyName!==undefined && <>"{keyName}":} + {valLabel(data)} + {typeof data==='object'?'null':typeof data} +
+ ); const entries = isArr ? data.map((v,i)=>[i,v]) : Object.entries(data); - const bracket = isArr ? ['[',']'] : ['{','}']; + const [o,c] = isArr ? ['[',']'] : ['{','}']; 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:''})}> - - - - - - + + + + + + - {/* Value for primitives */} - {node.type==='string' && onChange({...node, value:e.target.value})}/>} - {node.type==='number' && onChange({...node, value:e.target.value})}/>} - {node.type==='boolean' && ( - - )} - {/* Add child button */} - {canHaveChildren && ( - onChange({...node,value:e.target.value})}/>} + {node.type==='number' && onChange({...node,value:e.target.value})}/>} + {node.type==='boolean' && } + {node.type==='null' && null (kein Wert)} + {canKids && ( + )} - {/* 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=>( + + ))} +
+ ); return (
@@ -2139,28 +2180,78 @@ function JsonTool({ toast, mobile }) { {tab==='view' && (
-
-
JSON einfügen
-