Last active
November 15, 2023 03:55
-
-
Save ten9miq/03ad50188de8b335b899fe7495f1ab1a to your computer and use it in GitHub Desktop.
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
// ==UserScript== | |
// @name LinkBot ver.UserScript | |
// @namespace https://github.com/PaeP3nguin/LinkBot | |
// @version 0.6 | |
// @description Removed linkBot from Chrome Web store. https://chrome.google.com/webstore/detail/chnfcfcbnhloogdohcmjogkklghefofm | |
// @author ten9miq | |
// @match http*://*/* | |
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAiZJREFUOE+VlEtIVGEUx3/nPiYxIiGYikgKimgjBUWUK4OghbQoiGhRLoqYuRW9hB4StigJogKdubiyrRVkghuFWhbVpgcY9KaFLsQgkNS5M/+4k4+RxmH8Vh/nO+fHOf9zzmdUOF6oi8A5RB3iRhTYncXcbVFORps84z1QM+sTiXoC+1kuZlGQ36kdcnm9IEjsjQJ7viQQD+W6Y3wy2DgTOBxNsJNWm1gaKPbuVr2X5ztgFrEtd9beLl2jedA3wHHF5qnAPlcNqgm1IYJ1FDA51Jl4GoMELSa+4CBPjEym7WspdE5sN6ODZrQB2yuNRMnbsIxr+ZQ9iW1FkJ/ReRm3SltdJQwzzuRS1mV+l3bJYQhYAbxCXI6meMlyorKw3yS82mLW1zH2AX8wms3P6oHgOPAuStDECRuvKptu1Xp5XgANQJ95WY0Aa0xcyAV2L5FVg6C1AI/yaesvhbqhDjsFDjjQHnfQz6hFRk+cVQyajmUSNOfTNjCj111gMEqxHzPNwrysHgOHEG1RYDe9jJowngGKS/shqAeuRmnroF0JL8klx+ifTtmHBWXe12rf50gOegls1A91WqITGDU3VI+JFuBX5LKHU/axGo2WZbWlAEOC9SZ6jVBJT8UtTwLjiNs4vKFAXPL/x3AxdiOOAVuBMbdAY3GOvFCNiAFgZUnknDZlcf+MY4KT+bT1zX8jXVrlGVcwjgJrK5Unios86BsdkymL7/wFPJLM3jNrHaYAAAAASUVORK5CYII= | |
// @updateURL https://gist.github.com/53JIlLenWe11/03ad50188de8b335b899fe7495f1ab1a/raw/LinkBot%2520ver.UserScript.user.js | |
// @downloadURL https://gist.github.com/53JIlLenWe11/03ad50188de8b335b899fe7495f1ab1a/raw/LinkBot%2520ver.UserScript.user.js | |
// @grant none | |
// ==/UserScript== | |
const URL_REGEX = /\b(?:(?:h?ttps?|ftp|):\/\/)?((?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d?\d|2[0-4]\d|25[0-5])){3}|(?:[a-z\u00a1-\uffff\d]+-)*[a-z\u00a1-\uffff\d]+(?:\.(?:[a-z\u00a1-\uffff\d]+--?)*[a-z\u00a1-\uffff\d]+)*\.(?:com?|net|org|edu|gov|cc|in(?:fo)?|io|bi(?:z|d)|mobi|tv|bz|fm|am|me|ly|gl|gdn?|do(?:wnload)?|tw|us|tk|cf|cn|de|uk|ru|nl|eu|br|au|fr|it|pl|jp|ws|ca|ws|es|ch|be|im|pr|pw|gs|nu|ie|is|mn|mp|nz|rs|sh|vg|lu|ug|xn--[a-z\u00a1-\uffff\d-]{4,59}|xyz|top?|wang|win|cl(?:ub|ick)|li(?:nk)?|vip|online|science|engineering|si(?:te)?|racing|date|bar|chat|website|social|life|lol|ai|group|space|town|pro|love|host|fyi|zone|estate|moe|world|work|lgbt|church))(?::\d{2,5})?(?:[\/?#]\S*[a-z\u00a1-\uffff\d=])?)\b/gi; | |
const EMAIL_REGEX = /\b[\w\u00a1-\uffff!#$%&'*+/=?^`{|}~-]+(?:\.[\w\u00a1-\uffff!#$%&'*+/=?^`{|}~-]+)*@(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d?\d|2[0-4]\d|25[0-5])){3}|(?:[a-z\u00a1-\uffff\d]+-)*[a-z\u00a1-\uffff\d]+(?:\.(?:[a-z\u00a1-\uffff\d]+-)*[a-z\u00a1-\uffff\d]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))[a-z\u00a1-\uffff\d]?\b/gi; | |
const SUBREDDIT_REGEX = /(?:\b|\/)(r\/[a-z0-9][a-z0-9_]{2,29})\b/gi; | |
// Temporary placeholder for potentially conflicting email substitution | |
const TEMP_CHAR = '\uFFFF'; | |
const TEMP_CHAR_REGEX = /\uFFFF/gi; | |
const range = document.createRange(); | |
const parse = range.createContextualFragment.bind(range); | |
// A collection of tags to not replace text inside of | |
const EXCLUDED_TAGS = { | |
// Already clickable | |
A: true, | |
BUTTON: true, | |
OPTION: true, | |
// May cause issues if we replace things | |
IFRAME: true, | |
NOSCRIPT: true, | |
SCRIPT: true, | |
STYLE: true, | |
META: true, | |
EMBED: true, | |
// Better UX if we don't, tags may be user input or contain HTML | |
CITE: true, | |
TITLE: true, | |
TEXTAREA: true, | |
INPUT: true, | |
FORM: true, | |
PRE: false, | |
// Every example site seems to use this... | |
// CODE: true, | |
H1: true, | |
}; | |
(function() { | |
'use strict'; | |
recursiveLink(document.body); | |
const observerOptions = { | |
subtree: true, | |
characterData: true, | |
childList: true | |
}; | |
// Watch for DOM changes | |
const observer = new MutationObserver(function(mutations) { | |
mutations.forEach(function(m) { | |
if (!shouldLinkParents(m.target) || !shouldLink(m.target)) { | |
return; | |
} | |
var linkCount = 0; | |
if (m.type === "characterData") { | |
// Actual text node itself changed | |
linkCount += linkSingleNode(m.target); | |
} else if (m.type === "childList") { | |
// Added or removed stuff somewhere | |
for (var i = 0, l = m.addedNodes.length; i < l; i++) { | |
var child = m.addedNodes[i]; | |
if (shouldLink(child)) { | |
linkCount += linkSingleNode(child); | |
} | |
} | |
} | |
if (linkCount > 0) { | |
// Stop and restart watching so we don't see mutations that we're causing | |
// and allow webpage JS to run, which prevents infinite loops | |
observer.disconnect(); | |
setTimeout(function() { | |
observer.observe(document.body, observerOptions); | |
}, 0); | |
} | |
}); | |
}); | |
observer.observe(document.body, observerOptions); | |
})(); | |
// Convert all links under a root node, returns number of links found | |
function recursiveLink(root) { | |
// Initialize a TreeWalker to start looking at text from the root node | |
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ALL, { | |
acceptNode: nodeFilter | |
}, false); | |
let prev; | |
let node = walker.nextNode(); | |
let linkCount = 0; | |
while (node !== null) { | |
// Advance the walker past the current node to prevent | |
// seeing the same text nodes twice due to our own DOM changes | |
prev = node; | |
node = walker.nextNode(); | |
linkCount += linkTextNode(prev); | |
} | |
return linkCount; | |
} | |
// Filter for TreeWalker to determine which nodes to examine | |
function nodeFilter(node) { | |
switch (node.nodeType) { | |
case Node.TEXT_NODE: | |
// Skip if text is too short to be a link | |
// Shortest possible link is something like g.co | |
if (node.data.trim().length <= 4) { | |
return NodeFilter.FILTER_SKIP; | |
} | |
return NodeFilter.FILTER_ACCEPT; | |
case Node.ELEMENT_NODE: | |
// Skip node and all descendants of an editable node | |
if (isNodeEditable(node)) { | |
return NodeFilter.FILTER_REJECT; | |
} | |
// Skip node and all descendants of any excluded tags | |
if (EXCLUDED_TAGS[node.tagName]) { | |
return NodeFilter.FILTER_REJECT; | |
} | |
// Pass by boring old non-text nodes | |
return NodeFilter.FILTER_SKIP; | |
default: | |
// What are you???? | |
return NodeFilter.FILTER_SKIP; | |
} | |
} | |
// Tests whether a node is contentEditable | |
function isNodeEditable(node) { | |
return node.isContentEditable || node.contentEditable === "true"; | |
} | |
// Link a single node based on its nodeType, returns number of nodes found | |
function linkSingleNode(node) { | |
if (node.nodeType === Node.TEXT_NODE) { | |
// Skip if text is too short to be a link | |
// Shortest possible link is something like g.co | |
if (node.data.trim().length <= 4) { | |
return 0; | |
} | |
return linkTextNode(node); | |
} else if (node.nodeType === Node.ELEMENT_NODE) { | |
// Skip node and all descendants of an editable node | |
if (isNodeEditable(node)) { | |
return 0; | |
} | |
return recursiveLink(node); | |
} | |
return 0; | |
} | |
// Find links in a text node. Returns number of links found | |
function linkTextNode(node) { | |
// Email saving variables and functions | |
const emails = []; | |
let i = 0; | |
let urlCount = 0; | |
// Save the text to compare with later | |
let oldText = node.data; | |
let newText = oldText; | |
// Save emails and replace with a temporary, noncharacter Unicode character | |
// We'll put the emails back in later | |
// Why? Because otherwise the part after the @ sign will be recognized and replaced as a URL! | |
newText = newText.replace(EMAIL_REGEX, function(email) { | |
emails.push('<a href="mailto:' + email + '">' + email + '</a>'); | |
return TEMP_CHAR; | |
}); | |
// Replace URLs with links | |
newText = newText.replace(URL_REGEX, function(match, part) { | |
urlCount++; | |
return '<a href="//' + part + '">' + match + '</a>'; | |
}); | |
if ( window.location.hostname === "www.reddit.com") { | |
newText = newText.replace(SUBREDDIT_REGEX, function(match, part) { | |
urlCount++; | |
return '<a href="//www.reddit.com/' + part + '">' + match + '</a>'; | |
}); | |
} | |
if (emails.length) { | |
// Put emails back in, if any | |
newText = newText.replace(TEMP_CHAR_REGEX, function() { | |
return emails[i++]; | |
}); | |
} | |
if (newText !== oldText) { | |
// If we successfully added any links, insert into DOM | |
node.replaceWith(parse(newText)); | |
} | |
return i + urlCount; | |
} | |
// Returns false if any of the parents of a node should not be linkified. | |
function shouldLinkParents(node) { | |
let parent = node.parentNode; | |
while (parent !== null) { | |
if (shouldLink(parent)) { | |
parent = parent.parentNode; | |
} else { | |
return false; | |
} | |
} | |
return true; | |
} | |
// Returns true if we should link the node. | |
function shouldLink(node) { | |
if (EXCLUDED_TAGS[node.tagName] || isNodeEditable(node)) { | |
return false; | |
} else { | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment