Jak podmienić kilka wystąpień słów linkiem w tekście?

0

Dzień dobry,

siedzę nad problemem, w którym piszę mały skrypt do tworzenia słowniczka z definicjami.

Przykładowa tablica:

$tab = array();

$tab[0]['frazy'][0]='ala';
$tab[0]['frazy'][1]='Alicja';
$tab[0]['definicja']='Taka tam dziewczynka...';

$tab[1]['frazy'][0]='Alabama';
$tab[1]['frazy'][1]='Alabamie';
$tab[1]['definicja']='Stan w USA';

Przykładowe zdanie:

Alabama to piękny stan, w którym mieszka Ala i Alicja, ale Paladyni to już nie.

No i teraz, chciałbym, aby konkretne słowa zostały zamienione na np.

<a href="#" title="Stan w USA">Alabama</a>

z tym, że tylko pełne słowa, bo np. w słowie "Paladyni" istnieje fraza "ala" $tab[0]['frazy'][0] i tego nie chcę ruszać.

Chciałem zrobić to za pomocą preg_replace_callback() i zastanawiam się czy powyższą tablicę trzeba przelecieć 4 razy (mamy 4 frazy) żeby przy pomocy preg_replace_callback() wprowadzić zmiany w tekście czy jest na to jakaś prostsza metoda - np. 2 razy (bo mamy dwie grupy fraz).

Dodatkowe pytanie - w jaki sposób odnieść się tylko do pełnych słów i pominąć tutaj wielkość liter?

1

Krok pierwszy to byłoby napisanie testu pod to, np:

<?php
namespace Test\Unit;

use PHPUnit\Framework\TestCase;

class ReplaceTest extends TestCase
{
    /**
     * @test
     */
    public function test(): void
    {
        $words = [
            'ala' => 'Taka tam dziewczynka...',
            'Alicja' => 'Taka tam dziewczynka...',
            'Alabama' => 'Stan w USA',
            'Alabamie' => 'Stan w USA',
        ];

        $this->assertSame(
            '<a href="#" title="Stan w USA">Alabama</a> to piękny stan, w którym mieszka Ala i <a href="#" title="Taka tam dziewczynka...">Alicja</a>, ale Paladyni to już nie.',
            x('Alabama to piękny stan, w którym mieszka Ala i Alicja, ale Paladyni to już nie.', $words)
        );
    }
}

Przykładowa implementacja mogłaby wyglądać tak:

function x(string $subject, array $words): string
{
    foreach ($words as $search => $title) {
        $regexp = '\b' . \preg_quote($search) . '\b';
        $subject = \preg_replace_callback(
            "/$regexp/",
            fn(array $match): string => '<a href="#" title="' . \htmlSpecialChars($title) . '">' . $match[0] . '</a>',
            $subject
        );
    }
    return $subject;
}

albo używając preg_replace_callback_array(), ale to w sumie na to samo wychodzi:

function x(string $subject, array $words): string
{
    $regexps = [];
    foreach ($words as $search => $title) {
        $regexp = '\b' . \preg_quote($search) . '\b';
        $regexps["/$regexp/"] = fn(array $match): string =>
         '<a href="#" title="' . \htmlSpecialChars($title) . '">' . $match[0] . '</a>';
    }
    return \preg_replace_callback_array($regexps, $subject);
}
0

@Riddle kod działa super, bardzo dziękuję. Zastanawia mnie tylko kwestia zagnieżdżenia linku w linku, typu jeżeli będzie w subiekcie fragment typu: <a href="#">ala</a> - wtedy zagnieździ link w linku. Niestety bardzo słabo ogarniam wyrażenia regularne, mogę prosić o pomoc w tej kwestii?

0

No to znowu, należałoby napisać test pod to, żeby nie umieszczało linka w linku.

<?php
namespace Test\Unit;

use PHPUnit\Framework\TestCase;

class ReplaceTest extends TestCase
{
    /**
     * @test
     */
    public function test(): void
    {
        $words = [
            'ala' => 'Taka tam dziewczynka...',
            'Alicja' => 'Taka tam dziewczynka...',
            'Alabama' => 'Stan w USA',
            'Alabamie' => 'Stan w USA',
        ];

        $this->assertSame(
            '<a href="#" title="Stan w USA">Alabama</a> to piękny stan, w którym mieszka Ala i <a href="#" title="Taka tam dziewczynka...">Alicja</a>, ale Paladyni to już nie.',
            x('Alabama to piękny stan, w którym mieszka Ala i Alicja, ale Paladyni to już nie.', $words)
        );
    }

    /**
     * @test
     */
    public function noNestedLinks(): void
    {
        $words = ['foo' => 'bar'];

        $this->assertSame(
            '<a>foo</a>',
            x('<a>foo</a>', $words)
        );
    }
}

Ale teraz już same regexpy nie wystarczą, trzeba do tego użyć DOMDocument, sparsować html, i podmienić same tylko węzły tekstowe. Ale to już nie będzie pojedyncza funkcja.

Wyglądałoby to mniej więcej tak:

function recursively_find_text_nodes(string $text) {
  $doc = new DOMDocument();
  $doc->loadHTML($text);

  foreach ($dom_element->childNodes as $dom_child) {
    if ($dom_child->nodeType === XML_TEXT_NODE) {
      // tutaj musiałbyś jakoś sprawdzić czy text ma zakłądany element
      // i jak ma, to go podmienić innym
      if (true) {
        $dom_child->parentNode->replaceNode($dom_child, $doc->createTextNode($newString));
      }
    }
  }
  return $doc->saveHTML();
}
0

Bo niepotrzebnie bawisz się jakimiś dziwnymi regexami, w których wynik wypluty przez jeden może zostać zepsuty przez drugi. Sam zauważyłeś, że ala zawiera się w innych słowach i to może być problem. Więc może coś w stylu (pseudokod):

część_zdania = podziel_zdanie_na_części_zdania_między_innymi_słowa_i_znaki_interpunkcyjne(zdanie)
wynik = ""
dla każdej części zdania:
  jeśli jest_słowem_kluczowym_w_słowniku(część_zdania) -> wynik += stwórz_link(część zdania)
  jeśli nie jest -> wynik += część_zdania

Tam wiadomo będzie wyzwanie ze spacjami i ze znakami interpunkcyjnymi, ale jeśli kontrolujesz zdania wejściowe, to to nie będzie problem.

0

Pomyślałem, żeby najpierw podmienić "niewygodne" treści jak linki na jakieś frazy, a później na koniec je przywrócić:

$tab = array();
$i=-1;

$txt = preg_replace_callback('/<a [^>]+>([^<]+)<\/a>/i', function (array $match) use ($txt): string
{
  global $i;
  global $tab;

  $i++;

  $tab[$i]=$match[0];

  return '{a_'.$i.'}';
}, $txt);

Ale takie wyciągnięcie danych do zewnętrznej tablicy nie działa w ten sposób. Da się jakoś inaczej te dane wyciągnąć na zewnątrz funkcji preg_replace_callback ?

Ps. Pamiętam Riddle jak z pół roku temu pisałeś mi żebym zapomniał, że istnieje coś takiego jak global :P

0
catshy napisał(a):

Pomyślałem, żeby najpierw podmienić "niewygodne" treści jak linki na jakieś frazy, a później na koniec je przywrócić:

Bardzo słaby pomysł. nie da się sparsować HTML'a regexpem.

Skorzystaj z DOMDocument. Tu masz przykład: https://phpenthusiast.com/blog/parse-html-with-php-domdocument

catshy napisał(a):

Ps. Pamiętam Riddle jak z pół roku temu pisałeś mi żebym zapomniał, że istnieje coś takiego jak global :P

Nadal Ci to proponuję.

0

Ok, zrobię to zatem za pomocą:

$dom = new DOMDocument;
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));

$nodes = $dom->getElementsByTagName('a');

foreach ($nodes as $node) {
  $node->nodeValue='{coś_tam}';
}

A tak z ciekawości odnośnie mojego wcześniejszego postu. Da się jakoś w preg_replace_callback wyciągnąć dane na zewnątrz i np. uzupełnić tablicę tak jak w podanym przeze mnie przykładzie?

0
catshy napisał(a):

Ok, zrobię to zatem za pomocą:

$dom = new DOMDocument;
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));

$nodes = $dom->getElementsByTagName('a');

foreach ($nodes as $node) {
  $node->nodeValue='{coś_tam}';
}

Lepiej byłoby textContent, żeby przypadkiem nie pozwolić na XSS.

catshy napisał(a):

A tak z ciekawości odnośnie mojego wcześniejszego postu. Da się jakoś w preg_replace_callback wyciągnąć dane na zewnątrz i np. uzupełnić tablicę tak jak w podanym przeze mnie przykładzie?

Jest na to kilka sposobów, pytanie co konkretnie chcesz zrobić.

Ogólnie to preg_replace_callback() służy do tego żeby podmienić tekst w innym tekście. Jeśli chcesz coś "wyciągać" na zewnątrz, to dużo lepiej byłoby użyć preg_match_all().

1 użytkowników online, w tym zalogowanych: 0, gości: 1