MCPcopy Index your code
hub / github.com/philc/vimium / findLink

Function findLink

content_scripts/mode_normal.js:406–485  ·  view source on GitHub ↗
(linkStrings)

Source from the content-addressed store, hash-verified

404// favor 'next page' over 'the next big thing', and 'more' over 'nextcompany', even if 'next' occurs
405// before 'more' in `linkStrings`.
406function findLink(linkStrings) {
407 const linksXPath = DomUtils.makeXPath([
408 "a",
409 "*[@onclick or @role='link' or contains(@class, 'button')]",
410 ]);
411 const links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
412 let candidateLinks = [];
413
414 // At the end of this loop, candidateLinks will contain all visible links that match our patterns
415 // links lower in the page are more likely to be the ones we want, so we loop through the snapshot
416 // backwards.
417 for (let i = links.snapshotLength - 1; i >= 0; i--) {
418 const link = links.snapshotItem(i);
419
420 // NOTE(philc): We used to enforce the bounding client rect on the link had nonzero width and
421 // height. However, that's not a valid requirement. If an anchor tag has a single floated span
422 // as a child, the anchor is still clickable even though it appears to have zero height. This is
423 // the case with Google Search's "next" links as of 2025-06. See #4650.
424
425 const computedStyle = globalThis.getComputedStyle(link, null);
426 const isHidden = computedStyle.getPropertyValue("visibility") != "visible" ||
427 computedStyle.getPropertyValue("display") == "none";
428 if (isHidden) continue;
429
430 let linkMatches = false;
431 for (const linkString of linkStrings) {
432 // SVG elements can have a null innerText.
433 const matches = link.innerText?.toLowerCase().includes(linkString) ||
434 link.value?.includes?.(linkString) ||
435 link.getAttribute("title")?.toLowerCase().includes(linkString) ||
436 link.getAttribute("aria-label")?.toLowerCase().includes(linkString);
437 if (matches) {
438 linkMatches = true;
439 break;
440 }
441 }
442
443 if (!linkMatches) continue;
444
445 candidateLinks.push(link);
446 }
447
448 if (candidateLinks.length == 0) return;
449
450 for (const link of candidateLinks) {
451 link.wordCount = link.innerText.trim().split(/\s+/).length;
452 }
453
454 // We can use this trick to ensure that Array.sort is stable. We need this property to retain the
455 // reverse in-page order of the links.
456
457 candidateLinks.forEach((a, i) => a.originalIndex = i);
458
459 // favor shorter links, and ignore those that are more than one word longer than the shortest link
460 candidateLinks = candidateLinks
461 .sort(function (a, b) {
462 if (a.wordCount === b.wordCount) {
463 return a.originalIndex - b.originalIndex;

Callers 2

goPreviousFunction · 0.85
goNextFunction · 0.85

Calls 3

matchMethod · 0.80
pushMethod · 0.45
filterMethod · 0.45

Tested by

no test coverage detected