diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index ab52223..7b3114f 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -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
+ {ruleToRegex(rule).pat || —}
+
+
+ {regex || —}
+
+ {!!description && (
+ {tok.raw}
+ {tok.label}
+