Doccano - Convert URLs to Clickable Hyperlinks

For the Doccano text annotator application: replace non-clickable urls with hyperlinks.

// ==UserScript==
// @name         Doccano - Convert URLs to Clickable Hyperlinks
// @version      1.1.1
// @namespace    http://tampermonkey.net/
// @description  For the Doccano text annotator application: replace non-clickable urls with hyperlinks.
// @author       Kyle Nakamura
// @license      MIT
// @match        */projects/1/text-classification*
// @icon         https://doccano.herokuapp.com/static/_nuxt/img/icon.c360b38.png
// ==/UserScript==


// Global constants
const urlRegStr = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$',
    urlRegExp = new RegExp(urlRegStr, 'i'),
    urlMaxLength = 2083,
    loadtimeDelay = 200;


/**
 *  Check if a string is a valid URL/URI.
 *
 *  @param  {string}    str
 *  @return {boolean}
 */
function isURL(str) {
    return str.length < urlMaxLength && urlRegExp.test(str);
}


/**
 *  Wrap a string in an HTML <a> tag to create a clickable hyperlink.
 *
 *  @param  {string}    url
 *  @return {string}
 */
function makeHyperlink(url) {
    return `<a href="${url}" target="_blank">${url}</a>`;
}


function findAndReplaceUrl() {
    let hyperlinkCreated = false;

    // Iterate over all elements with the relevant classnames
    // (in case multiple such elements exist).
    document.querySelectorAll('.v-card__text.title.highlight').forEach(el => {
        // Extract first word from the element and trim extra whitespace.
        const url = el.innerHTML.split(',')[0].trim();

        // Insert the clickable hyperlink in place within `el`,
        // replacing only the non-clickable url.
        if (isURL(url)) {
            el.innerHTML = el.innerHTML.replace(url, makeHyperlink(url));
            hyperlinkCreated = true;
        }
    });

    return hyperlinkCreated;
}


// Repeat every 500 ms because the page never refreshes and
// therefore the script never executes twice within the same session.
const interval = setInterval(() => {
    // Cancel interval after a url was replaced on this page load.
    if (findAndReplaceUrl()) {
        clearInterval(interval);

        // Additionally, add event listeners to repeat
        // on every page navigation done via button presses.
        document.querySelectorAll('button').forEach(el => {
            el.addEventListener('click', () => {
                let eventInterval = setInterval(() => {
                    if (findAndReplaceUrl()) {
                        clearInterval(eventInterval);
                    }
                }, parseInt(loadtimeDelay / 5));
            });
        });
    };
}, loadtimeDelay);