DevTools: Regex-Builder (visuell + Erklärungsmodus) hinzugefügt
This commit is contained in:
@@ -1409,13 +1409,469 @@ function NetzwerkRechner({ mobile }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────── Regex ──
|
||||
|
||||
const RULE_TYPES = [
|
||||
{ id:'contains', label:'Enthält', fields:['text'], hint:'abc' },
|
||||
{ id:'notcontains', label:'Enthält nicht', fields:['text'], hint:'abc' },
|
||||
{ id:'startswith', label:'Beginnt mit', fields:['text'], hint:'KD-' },
|
||||
{ id:'endswith', label:'Endet mit', fields:['text'], hint:'.pdf' },
|
||||
{ id:'exact', label:'Ist genau', fields:['text'], hint:'hallo' },
|
||||
{ id:'digits_n', label:'Genau N Ziffern', fields:['count'], hint:'6' },
|
||||
{ id:'digits_min', label:'Mindestens N Ziffern', fields:['count'], hint:'3' },
|
||||
{ id:'digits_range', label:'Ziffern (von–bis)', fields:['min','max'], hint:'' },
|
||||
{ id:'letters_n', label:'Genau N Buchstaben', fields:['count'], hint:'5' },
|
||||
{ id:'alnum_n', label:'Genau N Alphanumerisch',fields:['count'], hint:'8' },
|
||||
{ id:'minlength', label:'Mindestlänge', fields:['count'], hint:'5' },
|
||||
{ id:'maxlength', label:'Höchstlänge', fields:['count'], hint:'20' },
|
||||
{ id:'numrange', label:'Zahlenbereich', fields:['min','max'], hint:'' },
|
||||
{ id:'onlynums', label:'Nur Zahlen', fields:[], hint:'' },
|
||||
{ id:'onlyletters', label:'Nur Buchstaben', fields:[], hint:'' },
|
||||
{ id:'onlyalnum', label:'Nur Alphanumerisch', fields:[], hint:'' },
|
||||
{ id:'date_de', label:'Datum (TT.MM.JJJJ)', fields:[], hint:'' },
|
||||
{ id:'date_iso', label:'Datum (JJJJ-MM-TT)', fields:[], hint:'' },
|
||||
{ id:'time', label:'Uhrzeit (HH:MM)', fields:[], hint:'' },
|
||||
{ id:'email', label:'E-Mail-Adresse', fields:[], hint:'' },
|
||||
{ id:'url', label:'URL (http/https)', fields:[], hint:'' },
|
||||
{ id:'plz', label:'PLZ (5 Ziffern)', fields:[], hint:'' },
|
||||
{ id:'ipv4', label:'IPv4-Adresse', fields:[], hint:'' },
|
||||
{ id:'hex_color', label:'Hex-Farbe (#rrggbb)', fields:[], hint:'' },
|
||||
{ id:'custom', label:'Benutzerdefiniert', fields:['pattern'], hint:'\\d+' },
|
||||
];
|
||||
|
||||
function escapeRegex(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Build regex fragment + description for a single rule
|
||||
function ruleToRegex(rule) {
|
||||
const t = rule.type, f = rule.fields || {};
|
||||
const esc = escapeRegex(f.text || '');
|
||||
switch (t) {
|
||||
case 'contains': return { pat: esc, desc: `enthält "${f.text}"` };
|
||||
case 'notcontains': return { pat: `(?!.*${esc})`, desc: `enthält NICHT "${f.text}"` };
|
||||
case 'startswith': return { pat: `^${esc}`, desc: `beginnt mit "${f.text}"` };
|
||||
case 'endswith': return { pat: `${esc}$`, desc: `endet mit "${f.text}"` };
|
||||
case 'exact': return { pat: `^${esc}$`, desc: `ist genau "${f.text}"` };
|
||||
case 'digits_n': return { pat: `\\d{${f.count||1}}`, desc: `genau ${f.count||1} Ziffer(n)` };
|
||||
case 'digits_min': return { pat: `\\d{${f.count||1},}`, desc: `mindestens ${f.count||1} Ziffer(n)` };
|
||||
case 'digits_range': return { pat: `\\d{${f.min||1},${f.max||9}}`, desc: `${f.min||1}–${f.max||9} Ziffern` };
|
||||
case 'letters_n': return { pat: `[a-zA-ZäöüÄÖÜß]{${f.count||1}}`, desc: `genau ${f.count||1} Buchstabe(n)` };
|
||||
case 'alnum_n': return { pat: `[a-zA-Z0-9]{${f.count||1}}`, desc: `genau ${f.count||1} alphanumerische Zeichen` };
|
||||
case 'minlength': return { pat: `.{${f.count||1},}`, desc: `mindestens ${f.count||1} Zeichen lang` };
|
||||
case 'maxlength': return { pat: `.{0,${f.count||999}}`, desc: `höchstens ${f.count||999} Zeichen lang` };
|
||||
case 'numrange': return { pat: buildNumRange(Number(f.min||0), Number(f.max||9)), desc: `Zahl zwischen ${f.min||0} und ${f.max||9}` };
|
||||
case 'onlynums': return { pat: `^\\d+$`, desc: `nur Ziffern (0–9)` };
|
||||
case 'onlyletters': return { pat: `^[a-zA-ZäöüÄÖÜß]+$`, desc: `nur Buchstaben` };
|
||||
case 'onlyalnum': return { pat: `^[a-zA-Z0-9]+$`, desc: `nur Buchstaben und Ziffern` };
|
||||
case 'date_de': return { pat: `^(0[1-9]|[12]\\d|3[01])\\.(0[1-9]|1[0-2])\\.\\d{4}$`, desc: `Datum im Format TT.MM.JJJJ` };
|
||||
case 'date_iso': return { pat: `^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$`, desc: `Datum im Format JJJJ-MM-TT` };
|
||||
case 'time': return { pat: `^([01]\\d|2[0-3]):[0-5]\\d$`, desc: `Uhrzeit im Format HH:MM` };
|
||||
case 'email': return { pat: `^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$`, desc: `gültige E-Mail-Adresse` };
|
||||
case 'url': return { pat: `^https?:\\/\\/[^\\s/$.?#].[^\\s]*$`, desc: `URL beginnend mit http:// oder https://` };
|
||||
case 'plz': return { pat: `^\\d{5}$`, desc: `deutsche Postleitzahl (5 Ziffern)` };
|
||||
case 'ipv4': return { pat: `^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$`, desc: `IPv4-Adresse` };
|
||||
case 'hex_color': return { pat: `^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`, desc: `Hex-Farbe (#rgb oder #rrggbb)` };
|
||||
case 'custom': return { pat: f.pattern || '', desc: `benutzerdefiniert: ${f.pattern||''}` };
|
||||
default: return { pat: '', desc: '' };
|
||||
}
|
||||
}
|
||||
|
||||
// Simple numeric range pattern builder
|
||||
function buildNumRange(lo, hi) {
|
||||
if (lo === hi) return String(lo);
|
||||
if (lo < 0 || hi < 0) return `(-?\\d+)`; // fallback for negative
|
||||
return `(${lo}|${lo+1 <= hi ? `[${lo}-${hi}]` : lo})`.replace(/\((\d+)\)/, '$1');
|
||||
// For proper range, just do a simple approach:
|
||||
}
|
||||
|
||||
// Combine all rules into final regex
|
||||
function buildRegex(rules) {
|
||||
if (!rules.length) return '';
|
||||
const parts = rules.map(r => ruleToRegex(r).pat).filter(Boolean);
|
||||
// If any rule has ^ or $ anchors at both ends alone → just combine sequentially
|
||||
// Wrap in ^ ... $ only if all rules together form a complete match pattern
|
||||
const hasFullAnchors = parts.some(p => /^\^/.test(p) && /\$$/.test(p));
|
||||
if (rules.length === 1) return parts[0];
|
||||
// Multiple rules: concat sequentially (user is building a sequence)
|
||||
const combined = parts.join('');
|
||||
return combined;
|
||||
}
|
||||
|
||||
function buildDescription(rules) {
|
||||
if (!rules.length) return '';
|
||||
const descs = rules.map(r => ruleToRegex(r).desc).filter(Boolean);
|
||||
if (descs.length === 1) return `Der Text ${descs[0]}.`;
|
||||
return `Der Text ${descs.slice(0,-1).join(', dann ')} und ${descs[descs.length-1]}.`;
|
||||
}
|
||||
|
||||
// ── Regex Erklärung (Reverse) ──
|
||||
function explainRegex(pattern) {
|
||||
if (!pattern) return [];
|
||||
const explanations = [];
|
||||
let p = pattern;
|
||||
|
||||
const rules = [
|
||||
[/^\^/, 'Muss am Anfang beginnen'],
|
||||
[/\$$/, 'Muss am Ende enden'],
|
||||
[/\\\./g, 'Literaler Punkt (.)'],
|
||||
[/\\d\+/g, 'Eine oder mehr Ziffern (0–9)'],
|
||||
[/\\d\*/g, 'Beliebig viele Ziffern (0–9)'],
|
||||
[/\\d\?/g, 'Optionale Ziffer (0–9)'],
|
||||
[/\\d\{(\d+)\}/g, (m,n)=>`Genau ${n} Ziffer(n)`],
|
||||
[/\\d\{(\d+),(\d+)\}/g, (m,a,b)=>`${a} bis ${b} Ziffern`],
|
||||
[/\\d\{(\d+),\}/g, (m,n)=>`Mindestens ${n} Ziffer(n)`],
|
||||
[/\\d/g, 'Eine Ziffer (0–9)'],
|
||||
[/\\w\+/g, 'Ein oder mehr Wortzeichen (a-z, A-Z, 0-9, _)'],
|
||||
[/\\w/g, 'Ein Wortzeichen (a-z, A-Z, 0-9, _)'],
|
||||
[/\\s/g, 'Ein Leerzeichen/Whitespace'],
|
||||
[/\\S/g, 'Kein Leerzeichen'],
|
||||
[/\.\+/g, 'Ein oder mehr beliebige Zeichen'],
|
||||
[/\.\*/g, 'Beliebig viele beliebige Zeichen (auch keine)'],
|
||||
[/\./g, 'Ein beliebiges Zeichen'],
|
||||
[/\[([^\]]+)\]/g, (m,c)=>`Eines der Zeichen: ${c}`],
|
||||
[/\(([^)]+)\)\?/g, (m,c)=>`Optional: ${c}`],
|
||||
[/\(([^)]+)\)\+/g, (m,c)=>`Eines oder mehr von: ${c}`],
|
||||
[/\(([^)]+)\)\*/g, (m,c)=>`Beliebig viele von: ${c}`],
|
||||
[/\(([^)]+)\)/g, (m,c)=>`Gruppe: ${c}`],
|
||||
[/\{(\d+)\}/g, (m,n)=>`Genau ${n} Mal wiederholt`],
|
||||
[/\{(\d+),(\d+)\}/g, (m,a,b)=>`${a} bis ${b} Mal`],
|
||||
[/\{(\d+),\}/g, (m,n)=>`Mindestens ${n} Mal`],
|
||||
[/\?/g, 'Optional (0 oder 1 Mal)'],
|
||||
[/\+/g, '1 oder mehr Mal'],
|
||||
[/\*/g, '0 oder mehr Mal'],
|
||||
[/\|/g, 'ODER-Verknüpfung'],
|
||||
[/\(\?!/g, 'Negativer Lookahead: das Folgende darf NICHT vorkommen'],
|
||||
[/\(\?=/g, 'Positiver Lookahead: das Folgende muss vorkommen'],
|
||||
[/\\b/g, 'Wortgrenze'],
|
||||
[/i$/, 'Groß-/Kleinschreibung ignorieren (Flag: i)'],
|
||||
[/g$/, 'Alle Treffer suchen (Flag: g)'],
|
||||
];
|
||||
|
||||
// Tokenize step by step
|
||||
const tokens = [];
|
||||
let i = 0;
|
||||
while (i < p.length) {
|
||||
// Skip anchors
|
||||
if ((p[i]==='^'||p[i]==='$') && (i===0||i===p.length-1)) {
|
||||
tokens.push({raw:p[i], label: p[i]==='^'?'Anfang des Textes':'Ende des Textes'});
|
||||
i++; continue;
|
||||
}
|
||||
// Escaped sequences
|
||||
if (p[i]==='\\' && i+1<p.length) {
|
||||
const seq = p[i]+p[i+1];
|
||||
const next2 = p[i+2];
|
||||
let quant = '';
|
||||
let qLabel = '';
|
||||
let j = i+2;
|
||||
if (next2==='{') {
|
||||
const end = p.indexOf('}', j);
|
||||
if (end>j) { quant = p.slice(j,end+1); j=end+1; }
|
||||
} else if (next2==='+'||next2==='*'||next2==='?') { quant=next2; j=i+3; }
|
||||
const labels = {'\\d':'Ziffer (0–9)','\\w':'Wortzeichen','\\s':'Whitespace','\\S':'kein Whitespace','\\b':'Wortgrenze','\\n':'Zeilenumbruch','\\t':'Tab','\\r':'CR','\\+':'Plus (+)','\\*':'Stern (*)','\\?':'Fragezeichen (?)','\\(':'Klammer auf','\\)':'Klammer zu','\\[':'Eckige Klammer [','\\]':'Eckige Klammer ]','\\{':'Geschweifte Klammer {','\\}':'Geschweifte Klammer }','\\^':'Dach (^)','\\$':'Dollar ($)','\\|':'Pipe (|)','\\\\':'Backslash (\\)','\\-':'Bindestrich (-)','\\@':'@-Zeichen','\\.':'Punkt (.)'};
|
||||
let lbl = labels[seq] || `Zeichen: ${seq}`;
|
||||
if (quant==='{') lbl += ` — quantifier:${quant}`;
|
||||
else if (quant) {
|
||||
const qmap = {'+':'ein oder mehrfach','*':'beliebig oft (auch 0)','?':'optional'};
|
||||
lbl += ` (${qmap[quant]||quant})`;
|
||||
// Parse {n} or {n,m}
|
||||
const qm = quant.match(/^\{(\d+),(\d+)\}$/);
|
||||
const qm1 = quant.match(/^\{(\d+)\}$/);
|
||||
const qm2 = quant.match(/^\{(\d+),\}$/);
|
||||
if (qm) lbl += ` — ${qm[1]} bis ${qm[2]} Mal`;
|
||||
else if (qm1) lbl += ` — genau ${qm1[1]} Mal`;
|
||||
else if (qm2) lbl += ` — mindestens ${qm2[1]} Mal`;
|
||||
}
|
||||
tokens.push({raw:seq+(quant||''), label:lbl});
|
||||
i = j; continue;
|
||||
}
|
||||
// Character class
|
||||
if (p[i]==='[') {
|
||||
const end = p.indexOf(']',i);
|
||||
if (end>i) {
|
||||
const cls = p.slice(i,end+1);
|
||||
const neg = cls[1]==='!';
|
||||
const inner = cls.slice(neg?2:1,-1);
|
||||
tokens.push({raw:cls, label:`${neg?'Kein':'Eines der'} Zeichen: ${inner}`});
|
||||
i = end+1; continue;
|
||||
}
|
||||
}
|
||||
// Group
|
||||
if (p[i]==='(') {
|
||||
// find matching )
|
||||
let depth=1, j=i+1;
|
||||
while (j<p.length && depth>0) { if(p[j]==='(') depth++; else if(p[j]===')') depth--; j++; }
|
||||
const group = p.slice(i,j);
|
||||
let label = `Gruppe`;
|
||||
if (group.startsWith('(?!')) label = `Negativer Lookahead (darf NICHT folgen)`;
|
||||
else if (group.startsWith('(?=')) label = `Positiver Lookahead (muss folgen)`;
|
||||
else if (group.startsWith('(?:')) label = `Nicht-fangende Gruppe`;
|
||||
tokens.push({raw:group, label});
|
||||
i=j; continue;
|
||||
}
|
||||
// Quantifiers standalone
|
||||
if (p[i]==='+') { tokens.push({raw:'+', label:'Das Vorherige: 1 oder mehr Mal'}); i++; continue; }
|
||||
if (p[i]==='*') { tokens.push({raw:'*', label:'Das Vorherige: 0 oder mehr Mal'}); i++; continue; }
|
||||
if (p[i]==='?') { tokens.push({raw:'?', label:'Das Vorherige: optional'}); i++; continue; }
|
||||
if (p[i]==='|') { tokens.push({raw:'|', label:'ODER — eine der Alternativen'}); i++; continue; }
|
||||
if (p[i]==='.') { tokens.push({raw:'.', label:'Ein beliebiges Zeichen (außer Zeilenumbruch)'}); i++; continue; }
|
||||
// Literal
|
||||
tokens.push({raw:p[i], label:`Literales Zeichen: "${p[i]}"`}); i++;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function RegexBuilder({ toast, mobile }) {
|
||||
const [mode, setMode] = useState('build'); // 'build' | 'explain'
|
||||
// ── Build mode ──
|
||||
const [rules, setRules] = useState([]);
|
||||
const [testVal, setTestVal] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
function addRule() {
|
||||
setRules(r => [...r, {id: Date.now(), type: 'contains', fields: {text:''}}]);
|
||||
}
|
||||
|
||||
function updateRule(id, changes) {
|
||||
setRules(r => r.map(rule => rule.id===id ? {...rule, ...changes} : rule));
|
||||
}
|
||||
|
||||
function deleteRule(id) {
|
||||
setRules(r => r.filter(rule => rule.id !== id));
|
||||
}
|
||||
|
||||
function moveRule(id, dir) {
|
||||
setRules(r => {
|
||||
const idx = r.findIndex(x=>x.id===id);
|
||||
const next = idx+dir;
|
||||
if (next<0||next>=r.length) return r;
|
||||
const arr = [...r];
|
||||
[arr[idx], arr[next]] = [arr[next], arr[idx]];
|
||||
return arr;
|
||||
});
|
||||
}
|
||||
|
||||
const regex = buildRegex(rules);
|
||||
const description = buildDescription(rules);
|
||||
|
||||
let testResult = null;
|
||||
if (regex && testVal !== '') {
|
||||
try {
|
||||
testResult = new RegExp(regex).test(testVal);
|
||||
} catch(e) { testResult = null; }
|
||||
}
|
||||
|
||||
async function copyRegex() {
|
||||
if (!regex) return;
|
||||
try { await navigator.clipboard.writeText(regex); setCopied(true); setTimeout(()=>setCopied(false),1800); }
|
||||
catch { toast('Kopieren fehlgeschlagen'); }
|
||||
}
|
||||
|
||||
// ── Explain mode ──
|
||||
const [explainInput, setExplainInput] = useState('');
|
||||
const [explainTestVal, setExplainTestVal] = useState('');
|
||||
const explained = explainInput ? explainRegex(explainInput) : [];
|
||||
let explainTestResult = null;
|
||||
if (explainInput && explainTestVal !== '') {
|
||||
try { explainTestResult = new RegExp(explainInput).test(explainTestVal); } catch {}
|
||||
}
|
||||
|
||||
const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
|
||||
const lbl = {color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginBottom:3};
|
||||
|
||||
const FIELD_LABELS = {text:'Text', count:'Anzahl', min:'Von', max:'Bis', pattern:'Muster'};
|
||||
|
||||
function RuleCard({rule, idx}) {
|
||||
const rt = RULE_TYPES.find(r=>r.id===rule.type);
|
||||
return (
|
||||
<div style={{borderRadius:9, background:'rgba(255,255,255,0.04)',
|
||||
border:'1px solid rgba(255,255,255,0.09)', padding:'10px 12px', marginBottom:8}}>
|
||||
<div style={{display:'flex', gap:8, alignItems:'flex-start', flexWrap:'wrap'}}>
|
||||
{/* Move buttons */}
|
||||
<div style={{display:'flex', flexDirection:'column', gap:2, paddingTop:2}}>
|
||||
<button onClick={()=>moveRule(rule.id,-1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
|
||||
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}>▲</button>
|
||||
<button onClick={()=>moveRule(rule.id,+1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
|
||||
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}>▼</button>
|
||||
</div>
|
||||
{/* Rule number badge */}
|
||||
<div style={{minWidth:22,height:22,borderRadius:11,background:'rgba(78,205,196,0.15)',
|
||||
color:'#4ecdc4',fontFamily:'monospace',fontSize:10,fontWeight:700,
|
||||
display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,marginTop:2}}>
|
||||
{idx+1}
|
||||
</div>
|
||||
{/* Type select */}
|
||||
<div style={{flex:'0 0 auto'}}>
|
||||
<div style={lbl}>Regeltyp</div>
|
||||
<select style={{...inp, width:'auto', cursor:'pointer'}} value={rule.type}
|
||||
onChange={e=>updateRule(rule.id,{type:e.target.value, fields:{}})}>
|
||||
{RULE_TYPES.map(rt=>(
|
||||
<option key={rt.id} value={rt.id}>{rt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* Dynamic fields */}
|
||||
{rt?.fields.map(f=>(
|
||||
<div key={f} style={{flex:'1 1 80px', minWidth:80}}>
|
||||
<div style={lbl}>{FIELD_LABELS[f]||f}</div>
|
||||
<input style={{...inp, width:'100%'}} type={f==='pattern'?'text':'text'}
|
||||
placeholder={rt.hint||''}
|
||||
value={rule.fields[f]||''}
|
||||
onChange={e=>updateRule(rule.id,{fields:{...rule.fields,[f]:e.target.value}})}/>
|
||||
</div>
|
||||
))}
|
||||
{/* Preview of this rule's regex */}
|
||||
<div style={{flex:'1 1 100px', minWidth:100}}>
|
||||
<div style={lbl}>Regex-Teil</div>
|
||||
<code style={{display:'block',background:'rgba(0,0,0,0.3)',borderRadius:6,
|
||||
padding:'7px 9px',color:'#4ecdc4',fontFamily:"'Courier New',monospace",
|
||||
fontSize:12,wordBreak:'break-all',minHeight:34}}>
|
||||
{ruleToRegex(rule).pat || <span style={{color:'rgba(255,255,255,0.2)'}}>—</span>}
|
||||
</code>
|
||||
</div>
|
||||
{/* Delete */}
|
||||
<button onClick={()=>deleteRule(rule.id)}
|
||||
style={{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.2)',
|
||||
borderRadius:6,color:'#ff6b9d',cursor:'pointer',padding:'4px 10px',fontSize:13,
|
||||
marginTop:18,flexShrink:0}}>✕</button>
|
||||
</div>
|
||||
{/* Description of this rule */}
|
||||
<div style={{marginTop:6,marginLeft:60,color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
|
||||
{ruleToRegex(rule).desc}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Mode tabs */}
|
||||
<div style={{display:'flex',gap:6,marginBottom:20}}>
|
||||
{[['build','🔨 Regex bauen'],['explain','🔍 Regex erklären']].map(([id,lbl])=>(
|
||||
<button key={id} onClick={()=>setMode(id)} style={{
|
||||
padding:'7px 18px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||||
background:mode===id?'#4ecdc4':'rgba(255,255,255,0.05)',
|
||||
color:mode===id?'#0d0d0f':'rgba(255,255,255,0.65)',
|
||||
border:mode===id?'none':'1px solid rgba(255,255,255,0.12)',
|
||||
fontWeight:mode===id?700:400}}>{lbl}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{mode==='build' && (
|
||||
<div>
|
||||
{/* Regel-Karten */}
|
||||
{rules.map((rule,idx)=>(
|
||||
<RuleCard key={rule.id} rule={rule} idx={idx}/>
|
||||
))}
|
||||
|
||||
<button style={{...S.btn('#4ecdc4',false), fontSize:12, padding:'8px 18px', marginBottom:20}}
|
||||
onClick={addRule}>+ Regel hinzufügen</button>
|
||||
|
||||
{!!rules.length && (
|
||||
<>
|
||||
{/* Regex-Ausgabe */}
|
||||
<div style={{borderRadius:9,background:'rgba(255,255,255,0.03)',
|
||||
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px',marginBottom:12}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}>
|
||||
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10}}>GENERIERTER REGEX</span>
|
||||
<button onClick={copyRegex} style={{
|
||||
background:'rgba(78,205,196,0.1)',border:'1px solid rgba(78,205,196,0.2)',
|
||||
borderRadius:6,color:copied?'#4ecdc4':'rgba(255,255,255,0.5)',
|
||||
cursor:'pointer',padding:'2px 10px',fontSize:11}}>
|
||||
{copied?'✓ Kopiert':'⎘ Kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
<code style={{display:'block',color:'#4ecdc4',fontFamily:"'Courier New',monospace",
|
||||
fontSize:15,fontWeight:700,wordBreak:'break-all',lineHeight:1.5}}>
|
||||
{regex || <span style={{color:'rgba(255,255,255,0.2)'}}>—</span>}
|
||||
</code>
|
||||
{!!description && (
|
||||
<div style={{marginTop:8,color:'rgba(255,255,255,0.6)',fontSize:12,lineHeight:1.6}}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Test-Bereich */}
|
||||
<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={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginBottom:8}}>TESTEN</div>
|
||||
<div style={{display:'flex',gap:10,alignItems:'center',flexWrap:'wrap'}}>
|
||||
<input style={{...inp,flex:1,minWidth:160}} placeholder="Testwert eingeben…"
|
||||
value={testVal} onChange={e=>setTestVal(e.target.value)}/>
|
||||
{testResult===true && <span style={{color:'#4ecdc4',fontFamily:'monospace',fontSize:14,fontWeight:700}}>✓ Treffer</span>}
|
||||
{testResult===false && <span style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:14,fontWeight:700}}>✗ Kein Treffer</span>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!rules.length && (
|
||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,
|
||||
padding:'16px 0',textAlign:'center'}}>
|
||||
Noch keine Regeln — klicke „+ Regel hinzufügen"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode==='explain' && (
|
||||
<div>
|
||||
<div style={{marginBottom:14}}>
|
||||
<div style={{...lbl,fontSize:11,marginBottom:6}}>Regex eingeben</div>
|
||||
<input style={{...inp,width:'100%',boxSizing:'border-box',fontSize:14,fontFamily:"'Courier New',monospace"}}
|
||||
placeholder="z.B. ^[a-zA-Z0-9]{6,20}$"
|
||||
value={explainInput} onChange={e=>setExplainInput(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
{!!explained.length && (
|
||||
<>
|
||||
<div style={{marginBottom:14}}>
|
||||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginBottom:8}}>ERKLÄRUNG BAUSTEIN FÜR BAUSTEIN</div>
|
||||
{explained.map((tok,i)=>(
|
||||
<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'6px 10px',
|
||||
borderRadius:7,background:'rgba(255,255,255,0.03)',marginBottom:4,
|
||||
border:'1px solid rgba(255,255,255,0.06)'}}>
|
||||
<code style={{color:'#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:13,
|
||||
fontWeight:700,minWidth:60,flexShrink:0}}>{tok.raw}</code>
|
||||
<span style={{color:'rgba(255,255,255,0.7)',fontSize:12}}>{tok.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Test */}
|
||||
<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={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginBottom:8}}>TESTEN</div>
|
||||
<div style={{display:'flex',gap:10,alignItems:'center',flexWrap:'wrap'}}>
|
||||
<input style={{...inp,flex:1,minWidth:160}} placeholder="Testwert eingeben…"
|
||||
value={explainTestVal} onChange={e=>setExplainTestVal(e.target.value)}/>
|
||||
{explainTestResult===true && <span style={{color:'#4ecdc4',fontFamily:'monospace',fontSize:14,fontWeight:700}}>✓ Treffer</span>}
|
||||
{explainTestResult===false && <span style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:14,fontWeight:700}}>✗ Kein Treffer</span>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!explainInput && (
|
||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,padding:'16px 0',textAlign:'center'}}>
|
||||
Einen bestehenden Regex oben eingeben → jeder Baustein wird erklärt
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const TOOL_DEFS = [
|
||||
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
||||
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
||||
components:{explain:CronExplainer,build:CronBuilder}},
|
||||
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
|
||||
{id:'json', label:'{ } JSON', desc:'JSON formatieren', ready:false},
|
||||
{id:'regex', label:'🔍 Regex', desc:'Regex testen', ready:false},
|
||||
{id:'regex', label:'🔍 Regex', desc:'Regex bauen & erklären', ready:true, component:RegexBuilder},
|
||||
{id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff},
|
||||
{id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner},
|
||||
{id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner},
|
||||
|
||||
Reference in New Issue
Block a user