Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions components/RegexHighlightText.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";

interface RegexHighlightTextProps {
text: string;
matches: string[];
Expand All @@ -16,6 +22,7 @@ export default function RegexHighlightText(props: RegexHighlightTextProps) {
);

let lastIndex = 0;
let matchNumber = 0;

props.matches.forEach((match, index) => {
const offset = props.text.indexOf(match, lastIndex);
Expand All @@ -28,10 +35,45 @@ export default function RegexHighlightText(props: RegexHighlightTextProps) {
);
}

matchNumber++;
const currentMatchNumber = matchNumber;
const matchLength = match.length;
const startPos = offset;
const endPos = offset + matchLength;

parts.push(
<span key={`match-${offset}-${index}`} className="bg-blue-200/80">
{match === "\n" ? newLine : match}
</span>
<HoverCard
key={`match-${offset}-${index}`}
openDelay={100}
closeDelay={100}
>
<HoverCardTrigger asChild>
<span className="bg-blue-200/80 dark:bg-blue-700/60 hover:bg-blue-300 dark:hover:bg-blue-600 cursor-help transition-colors rounded-sm">
{match === "\n" ? newLine : match}
</span>
</HoverCardTrigger>
<HoverCardContent side="top" className="w-auto max-w-xs p-3">
<div className="space-y-1">
<p className="text-sm font-semibold">Match #{currentMatchNumber}</p>
<p className="text-xs text-muted-foreground">
Position:{" "}
<span className="bg-muted px-1 rounded">{startPos}</span> -{" "}
<span className="bg-muted px-1 rounded">{endPos}</span>
</p>
<p className="text-xs text-muted-foreground">
Length:{" "}
<span className="bg-muted px-1 rounded">{matchLength}</span>{" "}
character
{matchLength !== 1 ? "s" : ""}
</p>
{match.length <= 50 && (
<p className="text-xs bg-muted px-1 py-0.5 rounded mt-1">
&quot;{match === "\n" ? "\\n" : match}&quot;
</p>
)}
</div>
</HoverCardContent>
</HoverCard>
);

lastIndex = offset + match.length;
Expand Down
206 changes: 206 additions & 0 deletions components/regex/RegexCaptureGroupVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { useMemo } from "react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import {
getDetailedMatches,
RegexMatch,
} from "@/components/utils/regex-tester.utils";
import { cn } from "@/lib/utils";

interface RegexCaptureGroupVisualizerProps {
pattern: string;
testString: string;
}

const GROUP_COLORS = [
"border-blue-400 bg-blue-50 dark:bg-blue-950/30",
"border-green-400 bg-green-50 dark:bg-green-950/30",
"border-purple-400 bg-purple-50 dark:bg-purple-950/30",
"border-orange-400 bg-orange-50 dark:bg-orange-950/30",
"border-pink-400 bg-pink-50 dark:bg-pink-950/30",
"border-cyan-400 bg-cyan-50 dark:bg-cyan-950/30",
];

export default function RegexCaptureGroupVisualizer({
pattern,
testString,
}: RegexCaptureGroupVisualizerProps) {
const matches: RegexMatch[] = useMemo(() => {
if (!pattern || !testString) return [];
try {
return getDetailedMatches(pattern, testString);
} catch {
return [];
}
}, [pattern, testString]);

if (matches.length === 0) {
return (
<div className="text-sm text-muted-foreground italic">
No capture groups found. Add groups with parentheses () to see them
here.
</div>
);
}

const hasGroups = matches.some(
(m) => m.captureGroups.length > 0 || Object.keys(m.groups).length > 0
);

if (!hasGroups) {
return (
<div className="text-sm text-muted-foreground italic">
No capture groups in pattern. Use parentheses () to create capture
groups.
</div>
);
}

return (
<div className="space-y-4">
{matches.map((match, matchIndex) => (
<div key={matchIndex} className="space-y-2">
<div className="text-xs font-medium text-muted-foreground">
Match {matchIndex + 1} at position {match.index}
</div>
<div
className={cn(
"p-3 rounded-lg border-2 border-dashed",
"border-foreground/30 bg-muted/30"
)}
>
<div className="text-xs text-muted-foreground mb-2">Full Match</div>
<div className="font-mono text-sm bg-background px-2 py-1 rounded border">
{match.fullMatch || (
<span className="text-muted-foreground italic">empty</span>
)}
</div>

{match.captureGroups.length > 0 && (
<div className="mt-3 space-y-2">
<div className="text-xs text-muted-foreground">
Capture Groups (CSS Box Model)
</div>
<div className="relative">
{match.captureGroups.map((group, groupIndex) => (
<HoverCard
key={groupIndex}
openDelay={100}
closeDelay={100}
>
<HoverCardTrigger asChild>
<div
className={cn(
"p-2 rounded border-2 transition-all cursor-help",
"hover:shadow-md hover:ring-2 hover:ring-ring",
GROUP_COLORS[groupIndex % GROUP_COLORS.length]
)}
style={{
marginLeft: `${groupIndex * 8}px`,
marginTop: groupIndex > 0 ? "8px" : "0",
}}
>
<div className="flex items-center gap-2">
<span className="text-xs font-medium px-1.5 py-0.5 rounded bg-background/80">
Group {groupIndex + 1}
</span>
<span className="font-mono text-sm truncate">
{group ?? (
<span className="text-muted-foreground italic">
undefined
</span>
)}
</span>
</div>
</div>
</HoverCardTrigger>
<HoverCardContent
side="right"
className="w-auto max-w-xs p-3"
>
<p className="text-sm font-semibold">
Capture Group {groupIndex + 1}
</p>
<p className="text-xs text-muted-foreground mt-1">
Captured value:{" "}
<span className="bg-muted px-1 rounded">
{group ?? "undefined"}
</span>
</p>
<p className="text-xs text-muted-foreground">
Access via:{" "}
<span className="bg-muted px-1 rounded">
match[{groupIndex + 1}]
</span>{" "}
or{" "}
<span className="bg-muted px-1 rounded">
${groupIndex + 1}
</span>
</p>
</HoverCardContent>
</HoverCard>
))}
</div>
</div>
)}

{Object.keys(match.groups).length > 0 && (
<div className="mt-3 space-y-2">
<div className="text-xs text-muted-foreground">
Named Groups
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(match.groups).map(([name, value], index) => (
<HoverCard key={name} openDelay={100} closeDelay={100}>
<HoverCardTrigger asChild>
<div
className={cn(
"px-3 py-2 rounded border-2 transition-all cursor-help",
"hover:shadow-md hover:ring-2 hover:ring-ring",
GROUP_COLORS[index % GROUP_COLORS.length]
)}
>
<div className="text-xs font-medium mb-1">{name}</div>
<div className="font-mono text-sm">
{value ?? (
<span className="text-muted-foreground italic">
undefined
</span>
)}
</div>
</div>
</HoverCardTrigger>
<HoverCardContent
side="bottom"
className="w-auto max-w-xs p-3"
>
<p className="text-sm font-semibold">
Named Group: {name}
</p>
<p className="text-xs text-muted-foreground mt-1">
Captured value:{" "}
<span className="bg-muted px-1 rounded">
{value ?? "undefined"}
</span>
</p>
<p className="text-xs text-muted-foreground">
Access via:{" "}
<span className="bg-muted px-1 rounded">
match.groups.{name}
</span>
</p>
</HoverCardContent>
</HoverCard>
))}
</div>
</div>
)}
</div>
</div>
))}
</div>
);
}
97 changes: 97 additions & 0 deletions components/regex/RegexCheatSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { CHEAT_SHEET } from "@/components/utils/regex-tester.utils";
import { cn } from "@/lib/utils";

interface RegexCheatSheetProps {
onInsert?: (syntax: string) => void;
}

export default function RegexCheatSheet({ onInsert }: RegexCheatSheetProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(["Character Classes"])
);

const toggleSection = (section: string) => {
setExpandedSections((prev) => {
const next = new Set(prev);
if (next.has(section)) {
next.delete(section);
} else {
next.add(section);
}
return next;
});
};

return (
<div className="border border-border rounded-lg overflow-hidden">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-3 flex items-center justify-between bg-muted hover:bg-accent transition-colors"
>
<span className="font-medium text-sm">Regex Cheat Sheet</span>
{isExpanded ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
</button>

{isExpanded && (
<div className="p-4 space-y-3 bg-background">
{Object.entries(CHEAT_SHEET).map(([category, items]) => (
<div
key={category}
className="border border-border rounded-md overflow-hidden"
>
<button
type="button"
onClick={() => toggleSection(category)}
className="w-full px-3 py-2 flex items-center justify-between bg-muted/50 hover:bg-muted transition-colors text-left"
>
<span className="text-sm font-medium">{category}</span>
{expandedSections.has(category) ? (
<ChevronUp className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
</button>

{expandedSections.has(category) && (
<div className="p-2 grid grid-cols-1 sm:grid-cols-2 gap-1">
{items.map((item, index) => (
<button
key={index}
type="button"
onClick={() => onInsert?.(item.syntax)}
className={cn(
"flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors",
"hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring",
onInsert ? "cursor-pointer" : "cursor-default"
)}
>
<code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono min-w-[60px]">
{item.syntax}
</code>
<span className="text-xs text-muted-foreground truncate">
{item.description}
</span>
</button>
))}
</div>
)}
</div>
))}
{onInsert && (
<p className="text-xs text-muted-foreground text-center">
Click any syntax to insert it into your pattern
</p>
)}
</div>
)}
</div>
);
}
Loading