|
// ==UserScript== |
|
// @name HN Article Search |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.0.0 |
|
// @match *://*/* |
|
// @grant GM_registerMenuCommand |
|
// @grant GM_unregisterMenuCommand |
|
// @grant GM_openInTab |
|
// @grant GM_xmlhttpRequest |
|
// @connect hn.algolia.com |
|
// ==/UserScript== |
|
|
|
(function () { |
|
"use strict"; |
|
|
|
// Exit if running in an iframe |
|
if (window.self !== window.top) { |
|
return; |
|
} |
|
|
|
function canonicalize(href) { |
|
try { |
|
const u = new URL(href); |
|
u.search = ""; |
|
u.hash = ""; |
|
u.hostname = u.hostname.replace(/^www\./, ""); |
|
if (u.pathname.length > 1 && u.pathname.endsWith("/")) { |
|
u.pathname = u.pathname.slice(0, -1); |
|
} |
|
return u.toString(); |
|
} catch { |
|
return href.split("?")[0].split("#")[0]; |
|
} |
|
} |
|
|
|
// If we're on an HN item page, grab the submitted URL from the post itself |
|
function resolvePageUrl() { |
|
const loc = window.location; |
|
const isHNItem = |
|
(loc.hostname === "news.ycombinator.com" || |
|
loc.hostname === "hn.algolia.com") && |
|
loc.pathname === "/item"; |
|
|
|
if (isHNItem) { |
|
// The submitted link lives in .titleline > a on news.ycombinator.com |
|
const link = document.querySelector( |
|
".titleline > a, .title > a.storylink", |
|
); |
|
if (link && link.href && !link.href.startsWith("item?")) { |
|
return link.href; |
|
} |
|
// Self-post (Ask HN, Show HN text post) — nothing to search for |
|
return null; |
|
} |
|
|
|
return loc.href; |
|
} |
|
|
|
const pageUrl = resolvePageUrl(); |
|
if (!pageUrl) { |
|
// On an HN self-post; nothing meaningful to search |
|
return; |
|
} |
|
|
|
const canonical = canonicalize(pageUrl); |
|
const bareUrl = canonical.replace(/^https?:\/\//, ""); |
|
|
|
const searchUrl = `https://hn.algolia.com/?q=${encodeURIComponent(bareUrl)}`; |
|
const apiUrl = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(bareUrl)}&restrictSearchableAttributes=url`; |
|
|
|
let menuId = null; |
|
|
|
function openSearch() { |
|
GM_openInTab(searchUrl, { active: true, setParent: true }); |
|
} |
|
|
|
function registerMenu(label) { |
|
if (menuId !== null && typeof GM_unregisterMenuCommand === "function") { |
|
try { |
|
GM_unregisterMenuCommand(menuId); |
|
} catch (e) { |
|
/* some managers don't support it reliably */ |
|
} |
|
} |
|
menuId = GM_registerMenuCommand(label, openSearch); |
|
} |
|
|
|
registerMenu("Searching…"); |
|
|
|
GM_xmlhttpRequest({ |
|
method: "GET", |
|
url: apiUrl, |
|
timeout: 8000, |
|
onload: (res) => { |
|
try { |
|
const data = JSON.parse(res.responseText); |
|
const n = data.nbHits ?? 0; |
|
const label = |
|
n === 0 ? "No results" : `${n} result${n === 1 ? "" : "s"}`; |
|
registerMenu(label); |
|
} catch (e) { |
|
registerMenu("Search this page"); |
|
} |
|
}, |
|
onerror: () => registerMenu("Search this page"), |
|
ontimeout: () => registerMenu("Search this page"), |
|
}); |
|
})(); |