Nalezení URL v textu a vytvoření odkazu

(publikováno 17.10.2017) PHP, JavaScript, HTML, Tipy & triky

Nalezení URL odkazů v textu a jejich obalení a zaktivnění tagem <a> hned dvěma metodami. V PHP, pokud je zdrojový text typu plain-text a v JavaScriptu pro detekci v HTML kódu.

Nalezení URL v textu a vytvoření odkazu

Nalezení a vytvoření odkazu se může na první pohled zdát jako jednoduchá záležitost. První problém nastane hned při zamyšlení nad formátem URL. Druhý problém je hledání URL v HTML. Zde totiž může být nalezená URL hodnota atributu, nebo potomek již nějakého tagu <a>, a tyto URL se určitě obalit dalším tagem <a> nesmí.

<!-- Zde URL nahradit chceme -->
Lorem ipsum http://www.google.com dolor sit amet

<!-- Zde určitě ne, je v atributu -->
<img src="https://www.kutac.cz/favicon.ico">
<a href="http://www.google.com">
    <span>
        <!-- Zde také ne, nějaký předek již je odkaz -->
        Go to http://www.google.com
    </span>
</a>

Otestovat JavaScriptovou verzi je možné na stránce testdata.kutac.cz

Regulární výraz pro nalezení URL

Formát URL je komplikovaný a napsat pro něj regulární výraz není jednoduché. Mnou použitý rozhodně není bulletproof řešení, ale měl by na většinu dostačovat. Určitě nepodporuje HTTP Basic auth přímo v URL adrese a jiné protokoly než HTTP a HTTPS, které musejí být přítomny.

V případě potřeby lze regulární výraz nahradit. Například z regexr.com.

var urlReg = /((?:https?|ftps?):\/\/|www.)([\w-]+(?:(?:\.[\w-]+)+))(:[0-9]+)?(\/(?:[\w-\.%]+\/?)*\/?)?(\?[\w-%\.]+[^#\s]+)?(#[^\s]+)?/ig;

PHP a plain text

Pokud je výstup čistý text bez HTML, stačí každou nalezenou URL obalit do tagu <a> a je vyhráno. Zde nehrozí žádná kolize a pro nahrazení lze využít funkce preg_replace

$text = preg_replace("/((?:https?|ftps?):\/\/|www.)([\w-]+(?:(?:\.[\w-]+)+))(:[0-9]+)?(\/(?:[\w-\.%]+\/?)*\/?)?(\?[\w-%\.]+[^#\s]+)?(#[^\s]+)?/i", '<a href="$0">$0</a>', $text);

PHP a HTML text

Pokud je nutné provést nahrazení v HTML, ale nelze použít JavaScript níže, je nutné podobnou logiku převést do PHP. Pro procházení DOMu lze využít knihoven DOM přímo v PHP + pár postřehů na PHPFashion. Nebo dalších knihoven jako Simple HTML DOM parser. Nic z toho jsem ale osobně netestoval.

JavaScript a jQuery

Pokud je text například výstupem nějakého WYSIWYG editoru, kde uživatel zapomněl URL označit jako odkaz, detekce je složitější. Nyní je lepší využít JavaScriptu, protože procházením DOMu lze jednoduše zjistit, jedná-li se o TextNode. Poté pouze v něm provést nahrazení, pokud žádný jeho rodič není tag <a>. Implementace je pomocí jQuery, nic ale nebrání přepsání do čistého JavaScriptu.

var activeLinks = {
    // Wrap inactive URL to <a>
    linkMatchRegex: /((?:https?|ftps?):\/\/|www.)([\w-]+(?:(?:\.[\w-]+)+))(:[0-9]+)?(\/(?:[\w-\.%]+\/?)*\/?)?(\?[\w-%\.]+[^#\s]+)?(#[^\s]+)?/ig,
    init: function (jQEls) {
        // Nelezení všech přímých potomků, včetně textových uzlů
        this.processChildren(jQEls.contents());
    },
    processChildren: function (wraps) {
        if (wraps.length < 1) {
            return;
        }
        // nodeType 3 = text node
        this.processTexts(wraps.filter(function () { return this.nodeType === 3; }));
        // nodeType 1 = element node
        this.processChildren(wraps.filter(function () { return this.nodeType === 1; }).contents());
    },
    processTexts: function (textEls) {
        if (textEls.length < 1) {
            return;
        }
        var t = this;
        textEls.each(function () {
            var jqEl = $(this);
            // Textový uzel nemá žádného předka <a>
            if (jqEl.parents("a").length === 0) {
                var splitMatches = jqEl.text().split(t.linkMatchRegex);
                // Rozdělení na obyčejné textové uzly a textové uzly obsahující URL
                t.wrapLink(jqEl, splitMatches);
            }
        });
    },
    wrapLink: function (jqEl, splitMatches) {
        var tmpEl = $("<div>");
        for(var i = 0; i < splitMatches.length; i++){
            // Zpětné spojení všech textových uzlů, pokud je URL
            // vloží se jako "element node" <a>
            if (splitMatches[i].match(this.linkMatchRegex)) {
                tmpEl.append( $("<a>").attr("href", splitMatches[i]).text(splitMatches[i]) );
            }else{
                tmpEl.append(document.createTextNode(splitMatches[i]));
            }
        }
        // Původní textový uzel může být 1 z mnoha ve stejném rodiči,
        // pro správné nahrazení se prvně obalí a až poté se nahradí
        var lineWrap = jqEl.wrap("<span>").parent();
        lineWrap.replaceWith(tmpEl.html());
    }
};
$(function () {
    activeLinks.init($(".js-activeLinks"));
});

S osobními zkušenostmi či postřehy se můžete podělit v komentářích

K tomuto článku již není možné přidávat další komentáře