(linkStrings)
| 404 | // favor 'next page' over 'the next big thing', and 'more' over 'nextcompany', even if 'next' occurs |
| 405 | // before 'more' in `linkStrings`. |
| 406 | function 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; |
no test coverage detected