refactor/fix: regex highlighter (#13)

This commit is contained in:
Sam Chau
2024-12-06 02:10:57 +10:30
committed by Sam Chau
parent fe7d1c52fe
commit 4dbe23b7d6
3 changed files with 128 additions and 202 deletions

View File

@@ -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

View File

@@ -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 (
<div className='flex flex-col h-full'>
{/* Header Section with Progress Bar */}
{/* Header with Progress Bar */}
<div className='flex items-center justify-between pb-4 pr-2'>
<div>
<h2 className='text-xl font-semibold text-gray-900 dark:text-white mb-3'>
@@ -132,10 +140,7 @@ const RegexTestingTab = ({
{tests.map(test => (
<UnitTest
key={test.id}
test={{
...test,
lastRun: formatTestDate(test.lastRun)
}}
test={test}
pattern={pattern}
onDelete={() => 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,

View File

@@ -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 <span className='font-mono'>{input}</span>;
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 <span className='font-mono text-gray-100'>{input}</span>;
}
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 (
<span className='font-mono'>
<span className='bg-green-900/20 rounded px-0.5'>
{segments.map((segment, idx) => (
<span
key={idx}
className={
segment.highlight
? test.passes
? 'bg-emerald-200 dark:bg-emerald-600 text-emerald-900 dark:text-emerald-100 px-0.5 rounded'
: 'bg-red-200 dark:bg-red-600 text-red-900 dark:text-red-100 px-0.5 rounded'
: 'text-gray-100'
}>
{segment.text}
</span>
))}
</span>
</span>
);
} catch (error) {
console.error('Regex error:', error);
return <span className='font-mono text-gray-100'>{input}</span>;
}
};
const UnitTest = ({test, pattern, onDelete, onEdit}) => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const handleDeleteClick = () => {
setShowDeleteModal(true);
};
const renderHighlightedInput = () => {
if (!test.matchSpan) {
return (
<span className='font-mono text-gray-100'>{test.input}</span>
);
}
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 (
<span className='font-mono'>
<span className='text-gray-100'>{preMatch}</span>
<span
className={`px-0.5 rounded ${
test.passes
? 'bg-emerald-200 dark:bg-emerald-600 text-emerald-900 dark:text-emerald-100'
: 'bg-red-200 dark:bg-red-600 text-red-900 dark:text-red-100'
}`}>
{match}
</span>
<span className='text-gray-100'>{postMatch}</span>
</span>
);
};
return (
<>
<div
className={`
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'
}
`}>
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 */}
<div className='px-4 py-2 pr-2 flex items-center justify-between border-b border-inherit'>
<div className='flex items-center gap-2'>
<div
className={`
w-2 h-2 rounded-full
${
test.passes
? 'bg-emerald-500 shadow-sm shadow-emerald-500/50'
: 'bg-red-500 shadow-sm shadow-red-500/50'
}
`}
w-2 h-2 rounded-full
${
test.passes
? 'bg-emerald-500 shadow-sm shadow-emerald-500/50'
: 'bg-red-500 shadow-sm shadow-red-500/50'
}
`}
/>
<span
className={`text-xs font-medium
${
test.passes
? 'text-emerald-700 dark:text-emerald-300'
: 'text-red-700 dark:text-red-300'
}
`}>
${
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}) => {
<Pencil className='w-4 h-4 text-gray-500 dark:text-gray-400' />
</button>
<button
onClick={handleDeleteClick}
onClick={() => setShowDeleteModal(true)}
className='p-1 rounded shrink-0 transition-transform transform hover:scale-110'>
<Trash2 className='w-4 h-4 text-gray-500 dark:text-gray-400' />
</button>
@@ -179,11 +96,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => {
<div className='p-2 flex items-start gap-3'>
<div className='flex-1 min-w-0'>
<div className='rounded bg-white/75 dark:bg-black/25 px-2 py-1.5 text-xs'>
<MatchHighlight
input={test.input}
pattern={pattern}
test={test}
/>
{renderHighlightedInput()}
</div>
</div>
</div>
@@ -192,7 +105,7 @@ const UnitTest = ({test, pattern, onDelete, onEdit}) => {
<DeleteConfirmationModal
isOpen={showDeleteModal}
onClose={() => 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;