@cplieger/web-terminal-engine 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "exports": {
5
5
  ".": "./src/index.ts"
6
6
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsgo -p tsconfig.json",
package/src/render.ts CHANGED
@@ -346,7 +346,14 @@ function linkifySpans(
346
346
  a.href = match[0];
347
347
  a.target = "_blank";
348
348
  a.rel = "noopener noreferrer";
349
- a.className = "term-link";
349
+ // Auto-detected bare URLs are tightly scoped to the matched URL text
350
+ // (never a padded/bordered region), so `.term-autolink` keeps a persistent
351
+ // underline for discoverability. OSC 8 hyperlinks (buildRowSpans below) get
352
+ // only `.term-link`, which the UI underlines on hover — an app can attach a
353
+ // single OSC 8 link to a whole region (e.g. a URL wrapping inside a table
354
+ // cell, where the link stays open across the cell padding/borders), and a
355
+ // persistent underline would then bleed across the cell/row.
356
+ a.className = "term-link term-autolink";
350
357
  a.textContent = match[0];
351
358
  // Copy inline styles from the source span
352
359
  a.style.cssText = span.style.cssText;
@@ -364,6 +371,29 @@ function linkifySpans(
364
371
  return out;
365
372
  }
366
373
 
374
+ // A hyperlink run is "link text" only if it has at least one glyph that is not
375
+ // whitespace and not a box-drawing (U+2500–U+257F) or block-element
376
+ // (U+2580–U+259F) character. Terminal table structure (borders `│`, padding,
377
+ // empty columns, margins) is made of exactly those, and an app may keep an OSC 8
378
+ // hyperlink open across it while a URL wraps; such decorative runs are not
379
+ // anchored, so the link decoration hugs the actual text instead of bleeding
380
+ // across the cell/row.
381
+ function runHasLinkText(spans: (HTMLSpanElement | HTMLAnchorElement)[]): boolean {
382
+ for (const s of spans) {
383
+ for (const ch of s.textContent) {
384
+ if (/\s/.test(ch)) {
385
+ continue;
386
+ }
387
+ const cp = ch.codePointAt(0) ?? 0;
388
+ if (cp >= 0x2500 && cp <= 0x259f) {
389
+ continue;
390
+ }
391
+ return true;
392
+ }
393
+ }
394
+ return false;
395
+ }
396
+
367
397
  // --- Build row DOM ---
368
398
  function buildRowSpans(runs: WireRun[], cursorAt: number): (HTMLSpanElement | HTMLAnchorElement)[] {
369
399
  const out: (HTMLSpanElement | HTMLAnchorElement)[] = [];
@@ -509,20 +539,33 @@ function buildRowSpans(runs: WireRun[], cursorAt: number): (HTMLSpanElement | HT
509
539
  col++;
510
540
  }
511
541
  flush();
512
- // Wrap spans from this run in an <a> if it has a hyperlink URL.
542
+ // Wrap this run's spans in an <a> when it has a hyperlink URL — but only if
543
+ // the run actually contains link *text*. An OSC 8 hyperlink can stay open
544
+ // across a whole region the app never meant as the clickable "link": a URL
545
+ // that wraps inside a table cell keeps the link open across the cell padding,
546
+ // the borders `│` and the empty adjacent column. Anchoring those decorative
547
+ // cells makes the link's decoration (the underline) bleed across the cell and,
548
+ // on wrap, the full row. Skip them so the anchor — and its underline, at rest
549
+ // or on hover — hugs the visible link text (they stay as plain, non-clickable
550
+ // spans; the wrapped URL's text runs each still carry the full href).
513
551
  const href = run.u && /^https?:\/\//i.test(run.u) ? run.u : null;
514
- if (href) {
552
+ const runSpans = out.splice(runStartIdx);
553
+ if (href && runHasLinkText(runSpans)) {
515
554
  const a = document.createElement("a");
516
555
  a.href = href;
517
556
  a.target = "_blank";
518
557
  a.rel = "noopener";
558
+ // OSC 8 app hyperlink: base `.term-link`. Auto-detected URLs additionally
559
+ // get `.term-autolink` (see linkifySpans).
519
560
  a.className = "term-link";
520
- // Move spans from this run into the anchor.
521
- const runSpans = out.splice(runStartIdx);
522
561
  for (const s of runSpans) {
523
562
  a.appendChild(s);
524
563
  }
525
564
  out.push(a);
565
+ } else {
566
+ for (const s of runSpans) {
567
+ out.push(s);
568
+ }
526
569
  }
527
570
  }
528
571
  if (cursorAt >= 0 && col <= cursorAt) {