mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
refactor/fix: regex highlighter (#13)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user