Compare commits
2 Commits
v0.1.51-wi
...
v0.1.53-wi
| Author | SHA1 | Date | |
|---|---|---|---|
| d947824436 | |||
| c2b21b794c |
@@ -2,8 +2,13 @@
|
|||||||
* Detects long URLs that span multiple hard-wrapped lines in PTY output.
|
* Detects long URLs that span multiple hard-wrapped lines in PTY output.
|
||||||
*
|
*
|
||||||
* The Linux PTY hard-wraps long lines with \r\n at the terminal column width,
|
* The Linux PTY hard-wraps long lines with \r\n at the terminal column width,
|
||||||
* which breaks xterm.js WebLinksAddon URL detection. This class reassembles
|
* which breaks xterm.js WebLinksAddon URL detection. This class flattens
|
||||||
* those wrapped URLs and fires a callback for ones >= 100 chars.
|
* the buffer (stripping PTY wraps, converting blank lines to spaces) and
|
||||||
|
* matches URLs with a single regex, firing a callback for ones >= 100 chars.
|
||||||
|
*
|
||||||
|
* When a URL match extends to the end of the flattened buffer, emission is
|
||||||
|
* deferred (more chunks may still be arriving). A confirmation timer emits
|
||||||
|
* the pending URL if no further data arrives within 500 ms.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ANSI_RE =
|
const ANSI_RE =
|
||||||
@@ -11,6 +16,7 @@ const ANSI_RE =
|
|||||||
|
|
||||||
const MAX_BUFFER = 8 * 1024; // 8 KB rolling buffer cap
|
const MAX_BUFFER = 8 * 1024; // 8 KB rolling buffer cap
|
||||||
const DEBOUNCE_MS = 300;
|
const DEBOUNCE_MS = 300;
|
||||||
|
const CONFIRM_MS = 500; // extra wait when URL reaches end of buffer
|
||||||
const MIN_URL_LENGTH = 100;
|
const MIN_URL_LENGTH = 100;
|
||||||
|
|
||||||
export type UrlCallback = (url: string) => void;
|
export type UrlCallback = (url: string) => void;
|
||||||
@@ -19,7 +25,9 @@ export class UrlDetector {
|
|||||||
private decoder = new TextDecoder();
|
private decoder = new TextDecoder();
|
||||||
private buffer = "";
|
private buffer = "";
|
||||||
private timer: ReturnType<typeof setTimeout> | null = null;
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private confirmTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private lastEmitted = "";
|
private lastEmitted = "";
|
||||||
|
private pendingUrl: string | null = null;
|
||||||
private callback: UrlCallback;
|
private callback: UrlCallback;
|
||||||
|
|
||||||
constructor(callback: UrlCallback) {
|
constructor(callback: UrlCallback) {
|
||||||
@@ -35,8 +43,14 @@ export class UrlDetector {
|
|||||||
this.buffer = this.buffer.slice(-MAX_BUFFER);
|
this.buffer = this.buffer.slice(-MAX_BUFFER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounce — scan after 300 ms of silence
|
// Cancel pending timers — new data arrived, rescan from scratch
|
||||||
if (this.timer !== null) clearTimeout(this.timer);
|
if (this.timer !== null) clearTimeout(this.timer);
|
||||||
|
if (this.confirmTimer !== null) {
|
||||||
|
clearTimeout(this.confirmTimer);
|
||||||
|
this.confirmTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce — scan after 300 ms of silence
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.scan();
|
this.scan();
|
||||||
@@ -44,29 +58,60 @@ export class UrlDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scan(): void {
|
private scan(): void {
|
||||||
|
// 1. Strip ANSI escape sequences
|
||||||
const clean = this.buffer.replace(ANSI_RE, "");
|
const clean = this.buffer.replace(ANSI_RE, "");
|
||||||
const lines = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
// 2. Flatten the buffer:
|
||||||
const match = lines[i].match(/https?:\/\/[^\s'"]+/);
|
// - Blank lines (2+ consecutive line breaks) → space (real paragraph break / URL terminator)
|
||||||
if (!match) continue;
|
// - Remaining \r and \n → removed (PTY hard-wrap artifacts)
|
||||||
|
const flat = clean
|
||||||
|
.replace(/(\r?\n){2,}/g, " ")
|
||||||
|
.replace(/[\r\n]/g, "");
|
||||||
|
|
||||||
// Start with the URL fragment found on this line
|
if (!flat) return;
|
||||||
let url = match[0];
|
|
||||||
|
|
||||||
// Concatenate subsequent continuation lines (non-empty, no spaces, no leading whitespace)
|
// 3. Match URLs on the flattened string — spans across wrapped lines naturally
|
||||||
for (let j = i + 1; j < lines.length; j++) {
|
const urlRe = /https?:\/\/[^\s'"<>\x07]+/g;
|
||||||
const next = lines[j];
|
let m: RegExpExecArray | null;
|
||||||
if (!next || next.startsWith(" ") || next.includes(" ")) break;
|
|
||||||
url += next;
|
while ((m = urlRe.exec(flat)) !== null) {
|
||||||
i = j; // skip this line in the outer loop
|
const url = m[0];
|
||||||
|
|
||||||
|
// 4. Filter by length
|
||||||
|
if (url.length < MIN_URL_LENGTH) continue;
|
||||||
|
|
||||||
|
// 5. If the match extends to the very end of the flattened string,
|
||||||
|
// more chunks may still be arriving — defer emission.
|
||||||
|
if (m.index + url.length >= flat.length) {
|
||||||
|
this.pendingUrl = url;
|
||||||
|
this.confirmTimer = setTimeout(() => {
|
||||||
|
this.confirmTimer = null;
|
||||||
|
this.emitPending();
|
||||||
|
}, CONFIRM_MS);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.length >= MIN_URL_LENGTH && url !== this.lastEmitted) {
|
// 6. URL is clearly complete (more content follows) — dedup + emit
|
||||||
|
this.pendingUrl = null;
|
||||||
|
if (url !== this.lastEmitted) {
|
||||||
this.lastEmitted = url;
|
this.lastEmitted = url;
|
||||||
this.callback(url);
|
this.callback(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan finished without a URL at the buffer end.
|
||||||
|
// If we had a pending URL from a previous scan, it's now confirmed complete.
|
||||||
|
if (this.pendingUrl) {
|
||||||
|
this.emitPending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitPending(): void {
|
||||||
|
if (this.pendingUrl && this.pendingUrl !== this.lastEmitted) {
|
||||||
|
this.lastEmitted = this.pendingUrl;
|
||||||
|
this.callback(this.pendingUrl);
|
||||||
|
}
|
||||||
|
this.pendingUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
@@ -74,5 +119,9 @@ export class UrlDetector {
|
|||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
}
|
}
|
||||||
|
if (this.confirmTimer !== null) {
|
||||||
|
clearTimeout(this.confirmTimer);
|
||||||
|
this.confirmTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user