Created
August 11, 2026 18:17
-
-
Save tschoffelen/7fe7f1df7d4584455d32be1ec3331ec3 to your computer and use it in GitHub Desktop.
AppleScript - Get a Markdown link for the current active tab in Safari or Chrome
This file contains hidden or 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
| #!/usr/bin/osascript -l JavaScript | |
| ObjC.import("AppKit"); | |
| // Some special mappings for various websites I often paste | |
| const mappers = { | |
| "docs.google.com/document": { | |
| emoji: () => "π", | |
| title: ({ title }) => title.replace(" - Google Docs", ""), | |
| }, | |
| "docs.google.com/presentation": { | |
| emoji: () => "π―", | |
| title: ({ title }) => title.replace(" - Google Slides", ""), | |
| }, | |
| "docs.google.com/spreadsheets": { | |
| emoji: () => "π", | |
| title: ({ title }) => title.replace(" - Google Sheets", ""), | |
| }, | |
| "www.strava.com/activities": { | |
| emoji: () => "π", | |
| title: ({ title }) => | |
| title.replace(" | Strava", "").replace(/ \| (Run|Walk)/, ""), | |
| }, | |
| "notion.so": { | |
| emoji: ({ title }) => getEmoji(title) || "π", | |
| title: ({ title }) => { | |
| if (getEmoji(title)) title = title.replace(getEmoji(title), ""); | |
| return title.replace(/^\(\d+[\+]?\) /, ""); | |
| }, | |
| }, | |
| "youtube.com": { | |
| emoji: () => "πΊ", | |
| title: ({ title }) => title.replace(' - YouTube', '').replace(/^\(\d+\) /, ""), | |
| }, | |
| "app.excalidraw.com": { | |
| emoji: () => "π¨", | |
| title: ({ title }) => title.replace(" β Excalidraw Plus", ""), | |
| }, | |
| "linear.app": { | |
| emoji: () => "ποΈ", | |
| title: ({ title }) => title.replace(" - Linear", ""), | |
| }, | |
| "streetart.community": { | |
| emoji: () => "π¬", | |
| title: ({ title }) => title.replace(" - Street Art Community", ""), | |
| }, | |
| "hubspot.com": { | |
| emoji: ({url}) => { | |
| if(url.includes('/record/0-3/')){ // deal | |
| return "π°"; | |
| } else if(url.includes('/record/0-4/')){ // ticket | |
| return "π«"; | |
| } else if(url.includes('/record/0-5/')){ // task | |
| return "β "; | |
| } else if(url.includes('/record/0-1/')){ // contact | |
| return "π€"; | |
| } else if(url.includes('/record/0-2/')){ // company | |
| return "π’"; | |
| } else { | |
| return "π"; | |
| } | |
| }, | |
| title: ({ title }) => title.replace(" | HubSpot", ""), | |
| }, | |
| "github.com": { | |
| emoji: () => "π", | |
| title: ({ title, url }) => { | |
| title = title.split(" Β· ")[0].replace(/ by \w+/, ""); | |
| // Add short hash of commit to title | |
| const match = url.match(/\/commit\/([a-f0-9]{7})/); | |
| if (match) { | |
| title = "`" + match[1] + "` " + title; | |
| } | |
| return title; | |
| }, | |
| }, | |
| "gitlab.com": { | |
| emoji: () => "π¦", | |
| title: ({ title, url }) => { | |
| title = title.split("(!")[0] | |
| // Add short hash of commit to title | |
| const match = url.match(/\/-\/commit\/([a-f0-9]{7})/); | |
| if (match) { | |
| title = "`" + match[1] + "` " + title; | |
| } | |
| return title; | |
| }, | |
| }, | |
| }; | |
| function mapOutput(res) { | |
| res.emoji = ""; | |
| const matchingMapper = Object.keys(mappers).find((key) => | |
| res.url.includes(key), | |
| ); | |
| if (matchingMapper) { | |
| const mapper = mappers[matchingMapper]; | |
| if (mapper.emoji) { | |
| res.emoji = mapper.emoji(res) + " "; | |
| } | |
| if (mapper.title) { | |
| res.title = mapper.title(res); | |
| } | |
| if (mapper.url) { | |
| res.url = mapper.url(res); | |
| } | |
| } | |
| // General cleanup | |
| if (res.title) { | |
| // Remove trailing period if it exists | |
| res.title = res.title.trim().replace(/\.$/, ""); | |
| } | |
| } | |
| function copyToClipboard(res) { | |
| const pb = $.NSPasteboard.generalPasteboard; | |
| const str1 = $.NSString.alloc.initWithUTF8String( | |
| res.emoji + '<a href="' + res.url + '">' + res.title.trim() + "</a>", | |
| ); | |
| const str2 = $.NSString.alloc.initWithUTF8String( | |
| res.emoji + "[" + res.title.trim() + "](" + res.url + ")", | |
| ); | |
| const str3 = $.NSString.alloc.initWithUTF8String( | |
| '{\\rtf1\\ansideff0{\\field{\\*\\fldinst{HYPERLINK "' + | |
| res.url + | |
| '"}}{\\fldrslt ' + | |
| res.emoji + | |
| res.title.trim() + | |
| "}}}", | |
| ); | |
| pb.clearContents; | |
| pb.setStringForType(str1, $.NSPasteboardTypeHTML); | |
| pb.setStringForType(str2, $.NSPasteboardTypeString); | |
| pb.setStringForType(str3, $.NSPasteboardTypeRTF); | |
| } | |
| function getWebLink(res) { | |
| var browsers = ["Google Chrome", "Orion", "Arc", "Dia", "Safari"]; | |
| var strBrowser = ""; | |
| for (var i = 0; i < browsers.length; i++) { | |
| try { | |
| if (Application(browsers[i]).running()) { | |
| strBrowser = browsers[i]; | |
| break; | |
| } | |
| } catch (e) { | |
| // Application not found, skip | |
| console.log("Browser not found: " + browsers[i]); | |
| } | |
| } | |
| if (strBrowser === "") { | |
| return false; | |
| } | |
| console.log("Found browser: " + strBrowser); | |
| if (strBrowser === "Safari" || strBrowser === "Orion" || strBrowser === "Notion") { | |
| var tab = Application(strBrowser).windows[0].currentTab; | |
| res.url = tab.url(); | |
| res.title = tab.name(); | |
| } else if (strBrowser === "Google Chrome" || strBrowser === "Arc") { | |
| res.title = Application("Google Chrome").windows[0].activeTab.name(); | |
| res.url = Application("Google Chrome").windows[0].activeTab.url(); | |
| } else { | |
| return false; | |
| } | |
| return true; | |
| } | |
| function applyJsCode(fn, strBrowser) { | |
| var browser = Application(strBrowser); | |
| var jsCode = "(" + fn.toString() + ").apply(null);"; | |
| var res = ""; | |
| if (strBrowser === "Safari" || strBrowser === "Orion") { | |
| res = browser.doJavaScript(jsCode, { | |
| in: browser.windows[0].tabs[0], | |
| }); | |
| } else { | |
| res = browser.windows[0].activeTab.execute({ | |
| javascript: jsCode, | |
| }); | |
| } | |
| return res; | |
| } | |
| function getEmoji(str) { | |
| const matches = str.match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g); | |
| return matches && matches[0]; | |
| } | |
| function pasteFromClipboard() { | |
| const se = Application("System Events"); | |
| se.keystroke("v", { using: "command down" }); | |
| } | |
| function run(argv) { | |
| var res = {}; | |
| getWebLink(res); | |
| mapOutput(res); | |
| res.title = res.title.replace(/(^"|"$)/g, "").replace(/\[/g, '(').replace(/\]/g, ')').replace('::', '-').trim(); | |
| res.url = res.url.replace(/(^"|"$)/g, "").trim(); | |
| if (res.hasOwnProperty("title") && res.hasOwnProperty("url")) { | |
| const output = res.emoji + "[" + res.title + "](" + res.url + ")"; | |
| copyToClipboard(output); | |
| pasteFromClipboard(); | |
| } | |
| } | |
| return "Failed!"; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment