From faabbe3fd2ac886e8f4530cec5547298a938ec92 Mon Sep 17 00:00:00 2001 From: Vuong <3168632+vuon9@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:04:43 +0700 Subject: [PATCH 1/2] feat: enhance RegExpTester with live highlighting and improved UX Major improvements to the RegExp Tester tool: - Live match highlighting in test string with layered textarea+backdrop - Color-coded capture groups (8 distinct colors) in both test string and match details - Hover tooltips showing match index, groups, and captures - Auto-expanding regex input (1-10 lines) with theme-aware scrollbar - Unified input group styling with visual connection between pattern/flags/copy button - Flags popover accessible via clicking flags input - Premium regex syntax highlighting for better readability - Filtered match details to show only captured groups with values - Copy button to copy full regex pattern (/pattern/flags) UI/UX: - Reduced spacing throughout for more compact layout - Consistent color scheme using rgba values for better visibility - Scrollbar only appears when content exceeds max height - Removed separate highlighted matches section (integrated into live view) Technical: - Removed unused Settings icon import - Fixed syntax error (extra closing div tag) - Updated TOOL_STATUS.md with completion date Closes: RegExpTester enhancement request --- TOOL_STATUS.md | 2 +- frontend/src/pages/RegExpTester.jsx | 1044 +++++++++++++++++++++------ 2 files changed, 807 insertions(+), 239 deletions(-) diff --git a/TOOL_STATUS.md b/TOOL_STATUS.md index b53e049..5d664a4 100644 --- a/TOOL_STATUS.md +++ b/TOOL_STATUS.md @@ -23,7 +23,7 @@ This document tracks the refactoring and development status of each tool compone | **CodeFormatter** | 🟢 Done | Unified code formatting tool supporting JSON (with jq filters), XML (with XPath), HTML (with CSS selectors), SQL, CSS, and JavaScript. Features: format/minify modes, filter/query support for structured data, auto-format on input change, persistent state. Backend: Go with gojq library for jq support. Replaces: JsonFormatter, SqlFormatter | Completed 2026-01-31 | | **ColorConverter** | 🟢 Done | Comprehensive color conversion tool with visual picker and eyedropper support. Features: 11 programming languages (CSS, Swift, .NET, Java, Android, Obj-C, Flutter, Unity, React Native, OpenGL, SVG), 5 color formats (HEX, RGB, HSL, HSV, CMYK), color history with 10 recent colors, random color generator, copy-to-clipboard for all code snippets. Uses Carbon Tabs for language selection. | Completed 2026-02-01 | | **CronJobParser** | 🟢 Done | Refactored to follow Carbon Design System. Features: Split-pane layout, 8 common examples in clickable tiles, real-time parsing, large centered output display, layout toggle. | Completed 2026-01-31 | -| **RegExpTester** | 🟡 In Progress | Refactored with improved UI. Features: Flag toggle tags (g, i, m, s, u, y), split-pane layout, match count in output label, error display with styling, layout toggle. | Updated 2026-01-31 | +| **RegExpTester** | 🟢 Done | Enhanced with live highlighting. Features: Unified regex input group with visual connection, expandable auto-resizing textarea (1-10 lines), theme-aware scrollbar, flags popover accessible via flags input, live match highlighting with group colors in test string and match details, hover tooltips, split-pane layout, copy full regex button, layout toggle. | Completed 2026-02-11 | | **TextDiffChecker** | 🟡 In Progress | Refactored with enhanced features. Features: Diff mode switcher (Lines/Words/Chars), auto-compare on input change, Clear button, improved diff view with color coding, layout toggle. | Updated 2026-01-31 | | **DateTimeConverter** | 🟢 Done | Complete redesign as unified DateTime Converter. All features on single screen - no tabs. Client-side only (no backend dependency). Features: Auto-detect input format (Unix timestamps: s/ms/μs/ns, ISO dates, SQL dates, US/EU formats), Quick presets (Now, Start/End of Day, Tomorrow, Yesterday, Next Week, Unix Epoch), Output format selector (ISO, RFC, SQL, US, EU, Compact), Timezone support, Main result display with relative time, All formats grid with copy buttons, Toggle-able sections: Visual Widgets (Calendar + Analog Clock), Time Calculator (Date A vs B with delta), Batch Converter (multi-line input with table results), Timezone Comparison (6 major cities), History persistence (localStorage, last 20), URL share support (?ts=). Unified, user-friendly interface designed for real-world datetime conversion needs. | Completed 2026-02-01 | diff --git a/frontend/src/pages/RegExpTester.jsx b/frontend/src/pages/RegExpTester.jsx index 6bcda49..cf55c1e 100644 --- a/frontend/src/pages/RegExpTester.jsx +++ b/frontend/src/pages/RegExpTester.jsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { TextInput, Tag } from '@carbon/react'; -import { ToolHeader, ToolControls, ToolPane, ToolSplitPane } from '../components/ToolUI'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { TextInput, CopyButton } from '@carbon/react'; +import { ChevronDown } from '@carbon/icons-react'; +import { ToolHeader, ToolSplitPane } from '../components/ToolUI'; import useLayoutToggle from '../hooks/useLayoutToggle'; const FLAG_OPTIONS = [ @@ -12,41 +13,161 @@ const FLAG_OPTIONS = [ { flag: 'y', label: 'Sticky', desc: 'Match from lastIndex' }, ]; -const HighlightedText = ({ text, regex, flags }) => { - if (!regex || !text) return {text}; +// Escape HTML special characters to prevent XSS and rendering issues +const escapeHtml = (text) => { + if (!text) return ''; + return text + .replace(/&/g, '&') + .replace(//g, '>'); +}; + +const GROUP_COLORS = [ + { bg: 'rgba(66, 190, 101, 0.15)', border: 'rgba(66, 190, 101, 0.3)', text: 'var(--cds-text-primary)' }, // Green (full match) + { bg: 'rgba(69, 137, 245, 0.15)', border: 'rgba(69, 137, 245, 0.3)', text: 'var(--cds-text-primary)' }, // Blue + { bg: 'rgba(241, 194, 27, 0.15)', border: 'rgba(241, 194, 27, 0.3)', text: 'var(--cds-text-primary)' }, // Yellow + { bg: 'rgba(218, 30, 40, 0.15)', border: 'rgba(218, 30, 40, 0.3)', text: 'var(--cds-text-primary)' }, // Red + { bg: 'rgba(165, 110, 255, 0.15)', border: 'rgba(165, 110, 255, 0.3)', text: 'var(--cds-text-primary)' }, // Purple + { bg: 'rgba(0, 189, 186, 0.15)', border: 'rgba(0, 189, 186, 0.3)', text: 'var(--cds-text-primary)' }, // Teal + { bg: 'rgba(255, 118, 178, 0.15)', border: 'rgba(255, 118, 178, 0.3)', text: 'var(--cds-text-primary)' }, // Pink + { bg: 'rgba(255, 140, 0, 0.15)', border: 'rgba(255, 140, 0, 0.3)', text: 'var(--cds-text-primary)' }, // Orange +]; + +// Premium Regex Syntax Highlighter +const highlightRegex = (regexStr) => { + if (!regexStr) return ''; + + const tokenRegex = /(\\.)|(\[(?:\\.|[^\]])*\])|(\((?:\\.|[^\)])*\))|([\*\+\?]\??|\{\d+(,\d*)?\}\??)|([\^$\|])/g; + + let lastIndex = 0; + let result = ''; + let match; + + while ((match = tokenRegex.exec(regexStr)) !== null) { + // Add text before match + result += escapeHtml(regexStr.slice(lastIndex, match.index)); + + const [full, escaped, charClass, group, quantifier, operator] = match; + + if (escaped) { + result += `${escapeHtml(escaped)}`; + } else if (charClass) { + result += `${escapeHtml(charClass)}`; + } else if (group) { + result += `${escapeHtml(group)}`; + } else if (quantifier) { + result += `${escapeHtml(quantifier)}`; + } else if (operator) { + result += `${escapeHtml(operator)}`; + } + + lastIndex = tokenRegex.lastIndex; + } + + result += escapeHtml(regexStr.slice(lastIndex)); + return result; +}; + +// Generate highlighted HTML with match data attributes for tooltips and group colors +const generateHighlightedHtml = (text, regex, flags) => { + if (!regex || !text) return escapeHtml(text); try { const re = new RegExp(regex, flags.includes('g') ? flags : flags + 'g'); - const parts = []; + let result = ''; let lastIndex = 0; let match; + let matchCount = 0; while ((match = re.exec(text)) !== null) { // Add text before match if (match.index > lastIndex) { - parts.push( - {text.slice(lastIndex, match.index)} + result += escapeHtml(text.slice(lastIndex, match.index)); + } + + matchCount++; + const matchIndex = match.index; + const fullMatchText = match[0]; + + // Build tooltip data + let tooltipData = `Match #${matchCount} at index ${matchIndex}`; + if (match.groups && Object.keys(match.groups).length > 0) { + const groups = Object.entries(match.groups) + .map(([k, v]) => `${k}: "${escapeHtml(v || '')}"`) + .join('\n'); + tooltipData += `\nGroups:\n${groups}`; + } + if (match.length > 1) { + const unnamedGroups = match.slice(1).filter((g, idx) => + !match.groups || !Object.values(match.groups).includes(g) ); + if (unnamedGroups.length > 0) { + const groups = unnamedGroups + .map((g, i) => `${i + 1}: "${escapeHtml(g || '')}"`) + .join('\n'); + tooltipData += `\nCaptures:\n${groups}`; + } } - // Add highlighted match - parts.push( - - {match[0]} - - ); + // If there are capture groups, render them with different colors + if (match.length > 1) { + // Find all group positions in the match + const groupPositions = []; + let currentPos = 0; + + for (let i = 1; i < match.length; i++) { + const groupText = match[i]; + if (groupText !== undefined) { + const groupIndex = fullMatchText.indexOf(groupText, currentPos); + if (groupIndex !== -1) { + groupPositions.push({ + index: groupIndex, + text: groupText, + groupNum: i + }); + currentPos = groupIndex + groupText.length; + } + } + } + + // Sort by position + groupPositions.sort((a, b) => a.index - b.index); + + // Build the highlighted match with different colors for groups + let matchResult = ''; + let matchPos = 0; + const fullMatchColor = GROUP_COLORS[0]; + + // Process the match text, applying different colors to groups + groupPositions.forEach((group) => { + // Add text before this group (part of full match) + if (group.index > matchPos) { + const beforeText = escapeHtml(fullMatchText.slice(matchPos, group.index)); + matchResult += `${beforeText}`; + } + + // Add the group with its color + const groupColor = GROUP_COLORS[group.groupNum % GROUP_COLORS.length] || GROUP_COLORS[1]; + matchResult += `${escapeHtml(group.text)}`; + + matchPos = group.index + group.text.length; + }); + + // Add remaining text after last group + if (matchPos < fullMatchText.length) { + const afterText = escapeHtml(fullMatchText.slice(matchPos)); + matchResult += `${afterText}`; + } + + result += matchResult; + } else { + // No groups - just highlight the full match + const fullMatchColor = GROUP_COLORS[0]; + result += `${escapeHtml(fullMatchText)}`; + } lastIndex = re.lastIndex; - + // Prevent infinite loop for zero-length matches if (match.index === re.lastIndex) { re.lastIndex++; @@ -55,25 +176,460 @@ const HighlightedText = ({ text, regex, flags }) => { // Add remaining text if (lastIndex < text.length) { - parts.push( - {text.slice(lastIndex)} - ); + result += escapeHtml(text.slice(lastIndex)); } - return <>{parts}; + return result; } catch (e) { - return {text}; + return escapeHtml(text); } }; +// Live Highlighted Editor Component +const LiveHighlightedEditor = ({ text, setText, regex, flags }) => { + const textareaRef = useRef(null); + const backdropRef = useRef(null); + const [highlightedHtml, setHighlightedHtml] = useState(''); + + // Update highlighting when text, regex, or flags change + useEffect(() => { + const html = generateHighlightedHtml(text, regex, flags); + setHighlightedHtml(html); + }, [text, regex, flags]); + + // Sync scroll between textarea and backdrop + const handleScroll = useCallback(() => { + if (textareaRef.current && backdropRef.current) { + backdropRef.current.scrollTop = textareaRef.current.scrollTop; + backdropRef.current.scrollLeft = textareaRef.current.scrollLeft; + } + }, []); + + return ( +
+ {/* Backdrop - shows highlighted text */} +
' }} + /> + + {/* Textarea - for user input */} +