From 4dbe23b7d6cf986aa603cc1f3a306b2441a96dfe Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Fri, 6 Dec 2024 02:10:57 +1030 Subject: [PATCH] refactor/fix: regex highlighter (#13) --- backend/app/data/utils.py | 54 ++--- .../src/components/regex/RegexTestingTab.jsx | 73 ++++--- frontend/src/components/regex/UnitTest.jsx | 203 +++++------------- 3 files changed, 128 insertions(+), 202 deletions(-) diff --git a/backend/app/data/utils.py b/backend/app/data/utils.py index 1cdea1b..c2ade3e 100644 --- a/backend/app/data/utils.py +++ b/backend/app/data/utils.py @@ -184,18 +184,11 @@ def test_regex_pattern( tests: List[Dict[str, Any]]) -> Tuple[bool, str, List[Dict[str, Any]]]: """ Test a regex pattern against a list of test cases using PCRE2 compatible engine. - - Args: - pattern: The regex pattern to test - tests: List of test dictionaries with 'input', 'expected', 'id', and 'passes' fields - - Returns: - Tuple of (success, message, updated_tests) + Returns match information along with test results. """ logger.info(f"Starting regex pattern test - Pattern: {pattern}") try: - # Try to compile the regex with PCRE2 compatibility try: compiled_pattern = regex.compile(pattern, regex.V1 | regex.IGNORECASE) @@ -208,35 +201,45 @@ def test_regex_pattern( current_time = datetime.now().isoformat() logger.info(f"Processing {len(tests)} test cases") - # Run each test for test in tests: test_id = test.get('id', 'unknown') test_input = test.get('input', '') expected = test.get('expected', False) - logger.info( - f"Running test {test_id} - Input: {test_input}, Expected: {expected}" - ) - try: - # Test if pattern matches input - matches = bool(compiled_pattern.search(test_input)) - # Update test result + match = compiled_pattern.search(test_input) + matches = bool(match) + + # Update test result with basic fields test['passes'] = matches == expected test['lastRun'] = current_time - if test['passes']: - logger.info( - f"Test {test_id} passed - Match result: {matches}") + # Add match information + if match: + test['matchedContent'] = match.group(0) + test['matchSpan'] = { + 'start': match.start(), + 'end': match.end() + } + # Get all capture groups if they exist + test['matchedGroups'] = [g for g in match.groups() + ] if match.groups() else [] else: - logger.warning( - f"Test {test_id} failed - Expected {expected}, got {matches}" - ) + test['matchedContent'] = None + test['matchSpan'] = None + test['matchedGroups'] = [] + + logger.info( + f"Test {test_id} {'passed' if test['passes'] else 'failed'} - Match: {matches}, Expected: {expected}" + ) except Exception as e: - logger.warning(f"Error running test {test_id}: {str(e)}") + logger.error(f"Error running test {test_id}: {str(e)}") test['passes'] = False test['lastRun'] = current_time + test['matchedContent'] = None + test['matchSpan'] = None + test['matchedGroups'] = [] # Log overall results passed_tests = sum(1 for test in tests if test.get('passes', False)) @@ -245,9 +248,10 @@ def test_regex_pattern( ) return True, "", tests + except Exception as e: - logger.warning(f"Unexpected error in test_regex_pattern: {str(e)}", - exc_info=True) + logger.error(f"Unexpected error in test_regex_pattern: {str(e)}", + exc_info=True) return False, str(e), tests diff --git a/frontend/src/components/regex/RegexTestingTab.jsx b/frontend/src/components/regex/RegexTestingTab.jsx index 33879a4..cad5c62 100644 --- a/frontend/src/components/regex/RegexTestingTab.jsx +++ b/frontend/src/components/regex/RegexTestingTab.jsx @@ -1,28 +1,9 @@ -// RegexTestingTab.jsx -import React, {useState, useCallback} from 'react'; +import React, {useState, useCallback, useEffect} from 'react'; import PropTypes from 'prop-types'; import {Plus, Loader, Play} from 'lucide-react'; import UnitTest from './UnitTest'; import AddUnitTestModal from './AddUnitTestModal'; -const formatTestDate = dateString => { - if (!dateString) return null; - - try { - return new Date(dateString).toLocaleString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - } catch (error) { - console.error('Error formatting date:', error); - return dateString; - } -}; - const RegexTestingTab = ({ pattern, tests, @@ -33,17 +14,45 @@ const RegexTestingTab = ({ const [isModalOpen, setIsModalOpen] = useState(false); const [editingTest, setEditingTest] = useState(null); + useEffect(() => { + const needsAutoRun = + tests?.length > 0 && + pattern && + tests.some(test => test.passes !== undefined && !test.matchSpan); + + if (needsAutoRun && !isRunningTests) { + onRunTests(pattern, tests); + } + }, []); + const handleAddOrUpdateTest = useCallback( testData => { let updatedTests; if (editingTest) { - // Update existing test updatedTests = tests.map(test => - test.id === testData.id ? testData : test + test.id === testData.id + ? { + ...testData, + passes: false, + lastRun: null, + matchedContent: null, + matchSpan: null, + matchedGroups: [] + } + : test ); } else { - // Add new test - updatedTests = [...tests, testData]; + updatedTests = [ + ...tests, + { + ...testData, + passes: false, + lastRun: null, + matchedContent: null, + matchSpan: null, + matchedGroups: [] + } + ]; } onTestsChange(updatedTests); onRunTests(pattern, updatedTests); @@ -70,13 +79,12 @@ const RegexTestingTab = ({ setEditingTest(null); }, []); - // Calculate test statistics const totalTests = tests?.length || 0; const passedTests = tests?.filter(test => test.passes)?.length || 0; return (
- {/* Header Section with Progress Bar */} + {/* Header with Progress Bar */}

@@ -132,10 +140,7 @@ const RegexTestingTab = ({ {tests.map(test => ( handleDeleteTest(test.id)} onEdit={() => handleEditTest(test)} @@ -170,7 +175,13 @@ RegexTestingTab.propTypes = { input: PropTypes.string.isRequired, expected: PropTypes.bool.isRequired, passes: PropTypes.bool.isRequired, - lastRun: PropTypes.string + lastRun: PropTypes.string, + matchedContent: PropTypes.string, + matchedGroups: PropTypes.arrayOf(PropTypes.string), + matchSpan: PropTypes.shape({ + start: PropTypes.number, + end: PropTypes.number + }) }) ), onTestsChange: PropTypes.func.isRequired, diff --git a/frontend/src/components/regex/UnitTest.jsx b/frontend/src/components/regex/UnitTest.jsx index d0430a6..6181f73 100644 --- a/frontend/src/components/regex/UnitTest.jsx +++ b/frontend/src/components/regex/UnitTest.jsx @@ -3,154 +3,71 @@ import PropTypes from 'prop-types'; import {Trash2, Pencil} from 'lucide-react'; import DeleteConfirmationModal from '@ui/DeleteConfirmationModal'; -const MatchHighlight = ({input, pattern, test}) => { - if (!pattern) return {input}; - - try { - const regex = new RegExp(pattern, 'g'); - const matches = []; - let match; - - while ((match = regex.exec(input)) !== null) { - // Avoid infinite loops with zero-length matches - if (match.index === regex.lastIndex) { - regex.lastIndex++; - } - matches.push(match); - } - - if (!matches.length) { - return {input}; - } - - let segments = []; - let lastIndex = 0; - - matches.forEach(match => { - let matchText = ''; - let matchStart = match.index; - - // Use capturing groups if they exist - let capturingGroupIndex = 1; - while ( - capturingGroupIndex < match.length && - !match[capturingGroupIndex] - ) { - capturingGroupIndex++; - } - if (capturingGroupIndex < match.length) { - matchText = match[capturingGroupIndex]; - - // Find the position of matchText in the input, starting from match.index - matchStart = input.indexOf(matchText, match.index); - - if (matchStart === -1) { - // If not found, skip this match - return; - } - } else { - // No capturing group match, use full match - matchText = match[0]; - } - - // Add non-highlighted segment before the match - if (matchStart > lastIndex) { - segments.push({ - text: input.slice(lastIndex, matchStart), - highlight: false - }); - } - - // Add the highlighted match - if (matchText.length > 0) { - segments.push({ - text: matchText, - highlight: true - }); - lastIndex = matchStart + matchText.length; - } else { - // Handle zero-length matches - lastIndex = matchStart; - } - }); - - // Add any remaining non-highlighted text - if (lastIndex < input.length) { - segments.push({ - text: input.slice(lastIndex), - highlight: false - }); - } - - return ( - - - {segments.map((segment, idx) => ( - - {segment.text} - - ))} - - - ); - } catch (error) { - console.error('Regex error:', error); - return {input}; - } -}; - const UnitTest = ({test, pattern, onDelete, onEdit}) => { const [showDeleteModal, setShowDeleteModal] = useState(false); - const handleDeleteClick = () => { - setShowDeleteModal(true); - }; + const renderHighlightedInput = () => { + if (!test.matchSpan) { + return ( + {test.input} + ); + } - const handleConfirmDelete = () => { - onDelete(); - setShowDeleteModal(false); + const preMatch = test.input.slice(0, test.matchSpan.start); + const match = test.input.slice( + test.matchSpan.start, + test.matchSpan.end + ); + const postMatch = test.input.slice(test.matchSpan.end); + + return ( + + {preMatch} + + {match} + + {postMatch} + + ); }; return ( <>
+ relative rounded-lg border group + ${ + test.passes + ? 'border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/20' + : 'border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20' + } + `}> {/* Header */}
+ ${ + test.passes + ? 'text-emerald-700 dark:text-emerald-300' + : 'text-red-700 dark:text-red-300' + } + `}> {test.expected ? 'Should Match' : 'Should Not Match'} @@ -167,7 +84,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => { @@ -179,11 +96,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => {
- + {renderHighlightedInput()}
@@ -192,7 +105,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => { setShowDeleteModal(false)} - onConfirm={handleConfirmDelete} + onConfirm={onDelete} /> ); @@ -204,19 +117,17 @@ UnitTest.propTypes = { input: PropTypes.string.isRequired, expected: PropTypes.bool.isRequired, passes: PropTypes.bool.isRequired, - lastRun: PropTypes.string + lastRun: PropTypes.string, + matchedContent: PropTypes.string, + matchedGroups: PropTypes.arrayOf(PropTypes.string), + matchSpan: PropTypes.shape({ + start: PropTypes.number, + end: PropTypes.number + }) }).isRequired, pattern: PropTypes.string.isRequired, onDelete: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired }; -MatchHighlight.propTypes = { - input: PropTypes.string.isRequired, - pattern: PropTypes.string.isRequired, - test: PropTypes.shape({ - passes: PropTypes.bool.isRequired - }).isRequired -}; - export default UnitTest;