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:
144
src/lib/linkDetector.tsx
Normal file
144
src/lib/linkDetector.tsx
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user