feat(terminal): add automatic URL detection in output

- Detect URLs including localhost addresses in terminal output
- Support for various URL formats and localhost patterns
- Provide utilities to make links clickable

Automatically detects when development servers start and offers
to open them in the preview pane.
This commit is contained in:
Mufeed VH
2025-06-23 00:29:47 +05:30
parent 444d480bad
commit a44e9a35c3

144
src/lib/linkDetector.tsx Normal file
View File

@@ -0,0 +1,144 @@
/**
* URL Detection utility for terminal output
* Detects various URL formats including localhost addresses
*/
import React from 'react';
// URL regex pattern that matches:
// - http:// and https:// URLs
// - localhost URLs with ports
// - IP addresses with ports
// - URLs with paths and query parameters
const URL_REGEX = /(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[[0-9a-fA-F:]+\]|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?::[0-9]+)?(?:\/[^\s]*)?/gi;
// More specific localhost pattern for better accuracy
const LOCALHOST_REGEX = /(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::[0-9]+)?(?:\/[^\s]*)?/gi;
export interface DetectedLink {
url: string;
fullUrl: string; // URL with protocol
isLocalhost: boolean;
startIndex: number;
endIndex: number;
}
/**
* Detects URLs in the given text
* @param text - The text to search for URLs
* @returns Array of detected links
*/
export function detectLinks(text: string): DetectedLink[] {
const links: DetectedLink[] = [];
const seenUrls = new Set<string>();
// Reset regex lastIndex
URL_REGEX.lastIndex = 0;
let match;
while ((match = URL_REGEX.exec(text)) !== null) {
const url = match[0];
// Skip if we've already seen this URL
if (seenUrls.has(url)) continue;
seenUrls.add(url);
// Ensure the URL has a protocol
let fullUrl = url;
if (!url.match(/^https?:\/\//)) {
// Default to http for localhost, https for others
const isLocalhost = LOCALHOST_REGEX.test(url);
fullUrl = `${isLocalhost ? 'http' : 'https'}://${url}`;
}
// Validate the URL
try {
new URL(fullUrl);
} catch {
// Invalid URL, skip
continue;
}
links.push({
url,
fullUrl,
isLocalhost: LOCALHOST_REGEX.test(url),
startIndex: match.index,
endIndex: match.index + url.length
});
}
return links;
}
/**
* Checks if a text contains any URLs
* @param text - The text to check
* @returns True if URLs are found
*/
export function hasLinks(text: string): boolean {
URL_REGEX.lastIndex = 0;
return URL_REGEX.test(text);
}
/**
* Extracts the first URL from text
* @param text - The text to search
* @returns The first detected link or null
*/
export function getFirstLink(text: string): DetectedLink | null {
const links = detectLinks(text);
return links.length > 0 ? links[0] : null;
}
/**
* Makes URLs in text clickable by wrapping them in a callback
* @param text - The text containing URLs
* @param onLinkClick - Callback when a link is clicked
* @returns React elements with clickable links
*/
export function makeLinksClickable(
text: string,
onLinkClick: (url: string) => void
): React.ReactNode[] {
const links = detectLinks(text);
if (links.length === 0) {
return [text];
}
const elements: React.ReactNode[] = [];
let lastIndex = 0;
links.forEach((link, index) => {
// Add text before the link
if (link.startIndex > lastIndex) {
elements.push(text.substring(lastIndex, link.startIndex));
}
// Add the clickable link
elements.push(
<a
key={`link-${index}`}
href={link.fullUrl}
onClick={(e) => {
e.preventDefault();
onLinkClick(link.fullUrl);
}}
className="text-primary underline hover:text-primary/80 cursor-pointer"
title={link.fullUrl}
>
{link.url}
</a>
);
lastIndex = link.endIndex;
});
// Add remaining text
if (lastIndex < text.length) {
elements.push(text.substring(lastIndex));
}
return elements;
}