mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Simplify multi-line URL detection with whitespace-based approach
- Detect http(s):// in current line with regex - Scan subsequent lines until first whitespace character - Much simpler and more reliable than URL validation - Use actual <a> tags instead of spans for proper linking - Remove complex string highlighting (focus on URLs only) - Cleaner, more maintainable code 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
98bb30f89f
commit
97143abc20
2 changed files with 100 additions and 166 deletions
|
|
@ -227,16 +227,6 @@
|
|||
content += 'Another long URL in middle of text:\r\n';
|
||||
content += 'Check out this amazing resource at https://very-long-domain-name.example.com/path/to/\r\n';
|
||||
content += 'some/deeply/nested/resource/with/query?param1=value1¶m2=value2 for more details.\r\n\r\n';
|
||||
|
||||
content += 'Multi-line strings:\r\n';
|
||||
content += 'config = {\r\n';
|
||||
content += ' "database_url": "postgresql://user:password@localhost:5432/mydb?sslmode=require&\r\n';
|
||||
content += 'connection_timeout=30&pool_size=10",\r\n';
|
||||
content += ' "api_key": `very-long-api-key-that-spans-multiple-lines-\r\n';
|
||||
content += 'because-it-is-really-really-long-and-complex`,\r\n';
|
||||
content += ' "message": \'This is a single quoted string that\r\n';
|
||||
content += 'spans multiple lines for demonstration\'\r\n';
|
||||
content += '}\r\n\r\n';
|
||||
|
||||
// Separator
|
||||
content += '\x1b[90m' + '═'.repeat(100) + '\x1b[0m\r\n\r\n';
|
||||
|
|
|
|||
|
|
@ -550,77 +550,94 @@ export class Terminal extends LitElement {
|
|||
const lines = this.container.querySelectorAll('.terminal-line');
|
||||
if (lines.length === 0) return;
|
||||
|
||||
// Extract text content from all lines for multi-line URL detection
|
||||
const fullText = Array.from(lines)
|
||||
.map((line) => this.getLineText(line))
|
||||
.join('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineText = this.getLineText(lines[i]);
|
||||
|
||||
// Find URLs and strings
|
||||
const patterns: Array<{ text: string; start: number; end: number; type: 'url' | 'string' }> =
|
||||
[];
|
||||
// Look for http(s):// in this line
|
||||
const httpMatch = lineText.match(/(https?:\/\/)/);
|
||||
if (httpMatch) {
|
||||
const urlStart = httpMatch.index!;
|
||||
let fullUrl = '';
|
||||
let endLine = i;
|
||||
|
||||
// URL regex that matches common URL patterns
|
||||
const urlRegex = /(https?:\/\/[^\s\n<>"']+)/gi;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(fullText)) !== null) {
|
||||
patterns.push({
|
||||
text: match[1],
|
||||
start: match.index,
|
||||
end: match.index + match[1].length,
|
||||
type: 'url',
|
||||
});
|
||||
}
|
||||
// Build the URL by scanning from the http part until we hit whitespace
|
||||
for (let j = i; j < lines.length; j++) {
|
||||
let remainingText = '';
|
||||
|
||||
// String regex that matches quoted strings (single, double, backtick)
|
||||
const stringRegex = /(['"`])((?:\\.|(?!\1)[^\\])*?)\1/gs;
|
||||
urlRegex.lastIndex = 0; // Reset regex
|
||||
while ((match = stringRegex.exec(fullText)) !== null) {
|
||||
// Only highlight strings that span multiple lines or are reasonably long
|
||||
const stringContent = match[0];
|
||||
const hasNewline = stringContent.includes('\n');
|
||||
const isLong = stringContent.length > 20;
|
||||
if (j === i) {
|
||||
// Current line: start from http position
|
||||
remainingText = lineText.substring(urlStart);
|
||||
} else {
|
||||
// Subsequent lines: take the whole trimmed line
|
||||
remainingText = this.getLineText(lines[j]).trim();
|
||||
}
|
||||
|
||||
if (hasNewline || isLong) {
|
||||
patterns.push({
|
||||
text: stringContent,
|
||||
start: match.index,
|
||||
end: match.index + stringContent.length,
|
||||
type: 'string',
|
||||
});
|
||||
// Find first whitespace character in this line's text
|
||||
const whitespaceMatch = remainingText.match(/\s/);
|
||||
if (whitespaceMatch) {
|
||||
// Found whitespace, URL ends here
|
||||
fullUrl += remainingText.substring(0, whitespaceMatch.index);
|
||||
endLine = j;
|
||||
break;
|
||||
} else {
|
||||
// No whitespace, take the whole line
|
||||
fullUrl += remainingText;
|
||||
endLine = j;
|
||||
|
||||
// If this is the last line, we're done
|
||||
if (j === lines.length - 1) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now create links for this URL across the lines it spans
|
||||
if (fullUrl.length > 7) {
|
||||
// More than just "http://"
|
||||
this.createUrlLinks(lines, fullUrl, i, endLine, urlStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (patterns.length === 0) return;
|
||||
private createUrlLinks(
|
||||
lines: NodeListOf<Element>,
|
||||
fullUrl: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
startCol: number
|
||||
) {
|
||||
let remainingUrl = fullUrl;
|
||||
|
||||
// Sort by start position to handle overlaps
|
||||
patterns.sort((a, b) => a.start - b.start);
|
||||
for (let lineIdx = startLine; lineIdx <= endLine; lineIdx++) {
|
||||
const line = lines[lineIdx];
|
||||
const lineText = this.getLineText(line);
|
||||
|
||||
// Remove overlapping patterns (URLs take precedence over strings)
|
||||
const filteredPatterns = [];
|
||||
for (const pattern of patterns) {
|
||||
const hasOverlap = filteredPatterns.some(
|
||||
(existing) => pattern.start < existing.end && pattern.end > existing.start
|
||||
);
|
||||
if (!hasOverlap) {
|
||||
filteredPatterns.push(pattern);
|
||||
if (lineIdx === startLine) {
|
||||
// First line: URL starts at startCol
|
||||
const lineUrlPart = lineText.substring(startCol);
|
||||
const urlPartLength = Math.min(lineUrlPart.length, remainingUrl.length);
|
||||
|
||||
this.createClickableInLine(line, fullUrl, 'url', startCol, startCol + urlPartLength);
|
||||
remainingUrl = remainingUrl.substring(urlPartLength);
|
||||
} else {
|
||||
// Subsequent lines: take from start of trimmed content
|
||||
const trimmedLine = lineText.trim();
|
||||
const urlPartLength = Math.min(trimmedLine.length, remainingUrl.length);
|
||||
|
||||
if (urlPartLength > 0) {
|
||||
const startColForLine = lineText.indexOf(trimmedLine);
|
||||
this.createClickableInLine(
|
||||
line,
|
||||
fullUrl,
|
||||
'url',
|
||||
startColForLine,
|
||||
startColForLine + urlPartLength
|
||||
);
|
||||
remainingUrl = remainingUrl.substring(urlPartLength);
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingUrl.length === 0) break;
|
||||
}
|
||||
|
||||
// Convert character positions to line/column positions
|
||||
const patternPositions = filteredPatterns.map((patternInfo) => {
|
||||
const { text, start, end, type } = patternInfo;
|
||||
return {
|
||||
text,
|
||||
type,
|
||||
startPos: this.charPosToLineCol(fullText, start),
|
||||
endPos: this.charPosToLineCol(fullText, end),
|
||||
};
|
||||
});
|
||||
|
||||
// Apply styling to each pattern
|
||||
patternPositions.forEach(({ text, type, startPos, endPos }) => {
|
||||
this.createClickableSpans(lines, text, type, startPos, endPos);
|
||||
});
|
||||
}
|
||||
|
||||
private getLineText(lineElement: Element): string {
|
||||
|
|
@ -629,62 +646,10 @@ export class Terminal extends LitElement {
|
|||
return textContent;
|
||||
}
|
||||
|
||||
private charPosToLineCol(fullText: string, charPos: number): { line: number; col: number } {
|
||||
const lines = fullText.split('\n');
|
||||
let currentPos = 0;
|
||||
|
||||
for (let line = 0; line < lines.length; line++) {
|
||||
const lineLength = lines[line].length;
|
||||
if (charPos <= currentPos + lineLength) {
|
||||
return { line, col: charPos - currentPos };
|
||||
}
|
||||
currentPos += lineLength + 1; // +1 for the newline character
|
||||
}
|
||||
|
||||
return { line: lines.length - 1, col: lines[lines.length - 1].length };
|
||||
}
|
||||
|
||||
private createClickableSpans(
|
||||
lines: NodeListOf<Element>,
|
||||
text: string,
|
||||
type: 'url' | 'string',
|
||||
startPos: { line: number; col: number },
|
||||
endPos: { line: number; col: number }
|
||||
) {
|
||||
// Handle single-line and multi-line patterns
|
||||
if (startPos.line === endPos.line) {
|
||||
// Single line pattern
|
||||
this.createClickableInLine(lines[startPos.line], text, type, startPos.col, endPos.col);
|
||||
} else {
|
||||
// Multi-line pattern
|
||||
for (let lineIdx = startPos.line; lineIdx <= endPos.line; lineIdx++) {
|
||||
const line = lines[lineIdx];
|
||||
if (!line) continue;
|
||||
|
||||
let startCol, endCol;
|
||||
if (lineIdx === startPos.line) {
|
||||
// First line: from startPos.col to end of line
|
||||
startCol = startPos.col;
|
||||
endCol = this.getLineText(line).length;
|
||||
} else if (lineIdx === endPos.line) {
|
||||
// Last line: from start of line to endPos.col
|
||||
startCol = 0;
|
||||
endCol = endPos.col;
|
||||
} else {
|
||||
// Middle lines: entire line
|
||||
startCol = 0;
|
||||
endCol = this.getLineText(line).length;
|
||||
}
|
||||
|
||||
this.createClickableInLine(line, text, type, startCol, endCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createClickableInLine(
|
||||
lineElement: Element,
|
||||
text: string,
|
||||
type: 'url' | 'string',
|
||||
url: string,
|
||||
type: 'url',
|
||||
startCol: number,
|
||||
endCol: number
|
||||
) {
|
||||
|
|
@ -718,8 +683,7 @@ export class Terminal extends LitElement {
|
|||
textNode,
|
||||
linkStart,
|
||||
linkEnd,
|
||||
text,
|
||||
type,
|
||||
url,
|
||||
!foundStart,
|
||||
nodeEnd >= endCol
|
||||
);
|
||||
|
|
@ -739,8 +703,7 @@ export class Terminal extends LitElement {
|
|||
textNode: Text,
|
||||
start: number,
|
||||
end: number,
|
||||
text: string,
|
||||
type: 'url' | 'string',
|
||||
url: string,
|
||||
_isFirst: boolean,
|
||||
_isLast: boolean
|
||||
) {
|
||||
|
|
@ -749,42 +712,28 @@ export class Terminal extends LitElement {
|
|||
|
||||
const nodeText = textNode.textContent || '';
|
||||
const beforeText = nodeText.substring(0, start);
|
||||
const clickableText = nodeText.substring(start, end);
|
||||
const linkText = nodeText.substring(start, end);
|
||||
const afterText = nodeText.substring(end);
|
||||
|
||||
// Create the clickable element
|
||||
const clickableElement = document.createElement('span');
|
||||
// Create the link element
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.className = 'terminal-link';
|
||||
linkElement.href = url;
|
||||
linkElement.target = '_blank';
|
||||
linkElement.rel = 'noopener noreferrer';
|
||||
linkElement.style.color = '#4fc3f7';
|
||||
linkElement.style.textDecoration = 'underline';
|
||||
linkElement.style.cursor = 'pointer';
|
||||
linkElement.textContent = linkText;
|
||||
|
||||
if (type === 'url') {
|
||||
clickableElement.className = 'terminal-link';
|
||||
clickableElement.style.color = '#4fc3f7';
|
||||
clickableElement.style.textDecoration = 'underline';
|
||||
clickableElement.style.cursor = 'pointer';
|
||||
clickableElement.setAttribute('data-url', text);
|
||||
// Add hover effects
|
||||
linkElement.addEventListener('mouseenter', () => {
|
||||
linkElement.style.backgroundColor = 'rgba(79, 195, 247, 0.2)';
|
||||
});
|
||||
|
||||
// Add click handler for URLs
|
||||
clickableElement.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.open(text, '_blank');
|
||||
});
|
||||
|
||||
// Add hover effects for URLs
|
||||
clickableElement.addEventListener('mouseenter', () => {
|
||||
clickableElement.style.backgroundColor = 'rgba(79, 195, 247, 0.2)';
|
||||
});
|
||||
|
||||
clickableElement.addEventListener('mouseleave', () => {
|
||||
clickableElement.style.backgroundColor = '';
|
||||
});
|
||||
} else {
|
||||
// String styling
|
||||
clickableElement.className = 'terminal-string';
|
||||
clickableElement.style.color = '#ce9178';
|
||||
clickableElement.style.backgroundColor = 'rgba(206, 145, 120, 0.1)';
|
||||
clickableElement.setAttribute('data-string', text);
|
||||
}
|
||||
|
||||
clickableElement.textContent = clickableText;
|
||||
linkElement.addEventListener('mouseleave', () => {
|
||||
linkElement.style.backgroundColor = '';
|
||||
});
|
||||
|
||||
// Replace the text node with the new structure
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
|
@ -793,7 +742,7 @@ export class Terminal extends LitElement {
|
|||
fragment.appendChild(document.createTextNode(beforeText));
|
||||
}
|
||||
|
||||
fragment.appendChild(clickableElement);
|
||||
fragment.appendChild(linkElement);
|
||||
|
||||
if (afterText) {
|
||||
fragment.appendChild(document.createTextNode(afterText));
|
||||
|
|
@ -883,11 +832,6 @@ export class Terminal extends LitElement {
|
|||
.terminal-link:hover {
|
||||
background-color: rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.terminal-string {
|
||||
color: #ce9178;
|
||||
background-color: rgba(206, 145, 120, 0.1);
|
||||
}
|
||||
</style>
|
||||
<div id="terminal-container" class="terminal-container w-full h-full"></div>
|
||||
`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue