Skip to content

Commit 109f616

Browse files
committed
Harden transcript link resolution at markup edges
1 parent 85bd457 commit 109f616

1 file changed

Lines changed: 43 additions & 12 deletions

File tree

‎src/tui/url-links.ts‎

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -635,15 +635,45 @@ function findMarkdownLinks(line: string): LinkHit[] {
635635
return spans;
636636
}
637637

638+
/**
639+
* Whole `![label](target)` ranges: bare-URL scanning cannot tell image markup
640+
* from links, so the resolver discards bare matches touching these ranges and
641+
* the spans above already skip them. Images stay non-openable by policy.
642+
*/
643+
function findImageRanges(line: string): { start: number; end: number }[] {
644+
const ranges: { start: number; end: number }[] = [];
645+
for (const match of line.matchAll(/!\[[^\]]*\]\(([^)\s]+)\)/g)) {
646+
const start = match.index ?? 0;
647+
ranges.push({ start, end: start + match[0].length });
648+
}
649+
return ranges;
650+
}
651+
638652
/**
639653
* The link target under one source offset: bare URLs first (fidelity for
640-
* URL-shaped link labels), then inline `[label](target)` spans.
654+
* URL-shaped link labels), then inline `[label](target)` spans. A bare match
655+
* fused across a link span's boundary (`[a](u1)[b](u2)` scans as one run) or
656+
* inside image markup is the matcher's artifact, not a link the line holds,
657+
* so only a bare match one span fully contains — or no span touches — counts.
641658
*/
642659
function markdownUrlAt(line: string, offset: number): string | null {
660+
const spans = findMarkdownLinks(line);
661+
const images = findImageRanges(line);
643662
for (const hit of findLinks(line)) {
644-
if (offset >= hit.start && offset < hit.end) return hit.url;
663+
if (offset >= hit.start && offset < hit.end) {
664+
const fused = spans.some(
665+
(span) =>
666+
hit.start < span.end &&
667+
hit.end > span.start &&
668+
(hit.start < span.start || hit.end > span.end),
669+
);
670+
const imaged = images.some(
671+
(image) => hit.start < image.end && hit.end > image.start,
672+
);
673+
if (!fused && !imaged) return hit.url;
674+
}
645675
}
646-
for (const span of findMarkdownLinks(line)) {
676+
for (const span of spans) {
647677
if (offset >= span.start && offset < span.end) return span.url;
648678
}
649679
return null;
@@ -734,13 +764,15 @@ function codeBlockLinkAt(
734764

735765
/**
736766
* The markdown click target: the raw link target under terminal-absolute
737-
* (x, y), or null when the cell paints no link. Walks from the hit leaf up
738-
* to the nearest painted code block (assistant markdown paints through
739-
* library CodeRenderables, one per block); clicks landing between blocks
740-
* still resolve through the parent markdown node, which pairs the same full
741-
* source with its own line info. TextRenderable rows never resolve here —
742-
* their own armed node handlers own those clicks. Never throws: anything
743-
* unexpected resolves to null so a missed click stays a missed click.
767+
* (x, y), or null when the cell paints no link. Walks from the hit leaf up to
768+
* the nearest painted code block (assistant markdown paints through library
769+
* CodeRenderables, one per block), and that first block decides: its answer
770+
* stands, with no retry at an ancestor, so a miss inside one block never
771+
* falls through to a wider ancestor that pairs the same column with a link
772+
* the narrower block already rejected. Clicks landing outside any block miss.
773+
* TextRenderable rows never resolve here — their own armed node handlers own
774+
* those clicks. Never throws: anything unexpected resolves to null so a
775+
* missed click stays a missed click.
744776
*/
745777
export function markdownLinkAt(
746778
renderer: CliRenderer,
@@ -756,8 +788,7 @@ export function markdownLinkAt(
756788
}
757789
while (current) {
758790
if (current instanceof CodeRenderable) {
759-
const url = codeBlockLinkAt(current, x, y);
760-
if (url !== null) return url;
791+
return codeBlockLinkAt(current, x, y);
761792
}
762793
current = current.parent;
763794
}

0 commit comments

Comments
 (0)