Skip to content

Instantly share code, notes, and snippets.

@ten9miq
Last active November 15, 2023 03:55
Show Gist options
  • Save ten9miq/03ad50188de8b335b899fe7495f1ab1a to your computer and use it in GitHub Desktop.
Save ten9miq/03ad50188de8b335b899fe7495f1ab1a to your computer and use it in GitHub Desktop.
// ==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