Last active
October 11, 2024 00:50
-
-
Save DV8FromTheWorld/e8935e434d22e383fc919dd6df488dc5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(async () => { | |
const HANDLERS = { | |
'github.com': handleGithub, | |
'app.asana.com': handleAsana, | |
'www.google.com': { | |
'/travel/flights/booking': handleGoogleFlights | |
} | |
}; | |
/* ----------- Github ------------- */ | |
function handleGithub() { | |
const pathname = location.pathname; | |
const isIssueOrPR = pathname.includes("pull") || pathname.includes("issues"); | |
assert(isIssueOrPR, "Can only make masked links for Github PRs and Issues pages"); | |
const GITHUB_URL_PATH_REGEX = /\/(?<owner>.+?)\/(?<repo>.+?)\/(?<type>pull|issues)\/(?<refNumber>.+?)(?<extra>$|\/.*$)/; | |
const pathParts = assert(GITHUB_URL_PATH_REGEX.exec(location.pathname), "Failed to parse github url path. Did regex break?"); | |
const { owner, repo, type, refNumber } = pathParts.groups; | |
const newPathname = `/${owner}/${repo}/${type}/${refNumber}`; | |
const prTitleEl = assert(document.querySelector('#partial-discussion-header h1 bdi'), `Could not find ${type} title header`); | |
const title = prTitleEl.innerText; | |
const cleanedUrl = new URL(location); | |
cleanedUrl.pathname = newPathname; | |
cleanedUrl.hash = ''; | |
if (location.href != cleanedUrl.href) { | |
alert(`Cleaned up github url when creating masked link. Will point at the base ${type} page for #${refNumber}`); | |
} | |
return { | |
title, | |
url: cleanedUrl.href | |
}; | |
}; | |
/* ----------- Asana ------------- */ | |
function handleAsana() { | |
const taskPanelInput = assert(document.querySelector('[aria-label="Task Name"]', "Could not find task name. Is the sidepanel open?")); | |
const taskName = taskPanelInput.value; | |
let url = location.href; | |
if (!url.endsWith('/f')) { | |
/* Ensure the url goes to the full-screened task */ | |
url + '/f'; | |
} | |
return { | |
title: taskName, | |
url | |
}; | |
} | |
/* ----------- Google Flights ------------ */ | |
async function handleGoogleFlights() { | |
/* structure | |
heading | |
location-grandparent | |
location-parent(aria-label="to" or "to" + "and back") | |
span(depart, or "multi-city") | |
svg? | |
span(arrival)? | |
price-grandparent | |
price-parent <-- detected | |
price-span(aria=US dollars) | |
price(lowest total price) | |
*/ | |
/* Find elemens containing US dollars, ensure they are the top-level price via "Lowest total price", then get the price string */ | |
const priceParentEl = assert( | |
Array.from(document.querySelectorAll('div[role="text"]:has([aria-label*="US dollars"])')) | |
.filter(el => el.innerText.includes('Lowest total price'))[0], | |
"Cannot find trip booking total price. Did HTML structure change?" | |
); | |
const priceEl = assert(priceParentEl.querySelector('[aria-label*="US dollars"]'), "Could not find 'priceEl'"); | |
const price = priceEl.innerText; | |
const headingEl = priceParentEl.parentNode.parentNode; | |
const locationParentEl = assert(headingEl.querySelector('[role="text"]'), "Could not find 'locationParentEl'"); | |
/* | |
Use the location grandparent node so that we can more easily check the aria-label data on locationParentEl. | |
Also, make that the aria-label we match is the first element, otherwise incorrect nested stuff matches. | |
*/ | |
const locationGrandparentEl = locationParentEl.parentNode; | |
const roundTripEl = locationGrandparentEl.querySelector(':scope > [aria-label*="to"][aria-label*="and back"]'); | |
const oneWayTripEl = locationGrandparentEl.querySelector(':scope > [aria-label*="to"]:not([aria-label*="and back"])'); | |
const flightListItemEls = document.querySelectorAll('[role="listitem"]:has([aria-label*="Flight on" i]:not([aria-label*="Flight details" i]))'); | |
const flights = Array.from(flightListItemEls).map(listEl => { | |
const dateEl = listEl.querySelector('[aria-label*="Flight on" i]:not([aria-label*="Flight details" i]'); | |
const stopsEl = listEl.querySelector('[aria-label*="stop"]'); | |
const durationEl = listEl.querySelector('[aria-label^="Total duration"]'); | |
const durationAndFlightPlanContainerEl = durationEl.parentNode; | |
const airportEls = Array.from(durationAndFlightPlanContainerEl.querySelectorAll('[aria-describedby]')); | |
return { | |
/* contains nbsp | TODO: remove it */ | |
date: dateEl.innerText.replaceAll('\n', ' '), | |
duration: durationEl.innerText, | |
airports: airportEls.map(el => el.innerText), | |
stops: stopsEl.innerText, | |
}; | |
}); | |
let destination; | |
const twoAirportSrc = roundTripEl || oneWayTripEl; | |
if (twoAirportSrc) { | |
const depart = twoAirportSrc.children[0].innerText; | |
const arrival = twoAirportSrc.children[2].innerText; | |
const arrowChar = roundTripEl ? '↔' : '→'; | |
destination = `${depart} ${arrowChar} ${arrival}`; | |
} | |
else { | |
/* Given we are multi-city, build up the destination from visited airports */ | |
const airportHops = []; | |
flights.forEach((flight, idx) => { | |
if (idx === 0) { | |
airportHops.push(flight.airports[0]); | |
} | |
airportHops.push(flight.airports[1]); | |
}); | |
destination = airportHops.join(' → '); | |
} | |
const simpleFlightStr = flights.map(({date, duration, stops}) => `${date}`).join(' | '); | |
const title = `${destination} | ${price} | ${simpleFlightStr}`; | |
/* Open the share modal to generate the share link */ | |
document.querySelector('button[aria-label="Share this flight"]').click(); | |
const shareUrlEl = await awaitEl('input[aria-label="Copy link"]'); | |
/* Then close the modal because we don't need it */ | |
document.querySelector('div[role="dialog"] button[aria-label="Close"]').click(); | |
return { | |
title: title, | |
url: shareUrlEl.value | |
}; | |
}; | |
/* ----------- Utilities ------------- */ | |
function assert(thing, errMessage) { | |
if (!thing) { | |
let err = new Error(errMessage); | |
err.__showPrettyErr = true; | |
throw err; | |
} | |
return thing; | |
} | |
async function awaitEl(selector, timeout = 5000) { | |
const stop = Date.now() + timeout; | |
let el = document.querySelector(selector); | |
while (el == null && Date.now() < stop) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
el = document.querySelector(selector); | |
} | |
return assert(el, `Could not find element via selector: '${selector}' within ${timeout}ms`); | |
} | |
/* ----------- Main script ------------- */ | |
try { | |
const site = location.hostname; | |
const registeredSite = HANDLERS[site]; | |
assert(registeredSite, `No registered handler for '${site}'`); | |
if (typeof registeredSite === 'object') { | |
const pathname = location.pathname; | |
const subpathHandler = registeredSite[pathname]; | |
assert(subpathHandler, `Site '${site}' is known, but could not find subpath handler for path: '${pathname}'`); | |
siteHandler = subpathHandler; | |
} | |
else { | |
siteHandler = registeredSite ; | |
} | |
const details = await siteHandler(); | |
assert(details, `Site handler for '${site}' produced no result. Likely indicates an error`); | |
const { title, url } = details; | |
const markdown = `[${title}](${url})`; | |
console.log(`Copying masked link: ${markdown}`); | |
await navigator.clipboard.writeText(markdown); | |
} | |
catch (err) { | |
const errMessage = err.__showPrettyErr ? err.message : err.toString(); | |
alert(errMessage); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment