Задача поиска и замены каких либо терминов в тексте кажется на первый взгляд довольно простой в PHP. Есть очень мощные инструменты вроде str_replace, а также поиск и замена с помощью регулярных выражений, например, preg_replace и preg_replace_callback.
На практике возникает множество нюансов, которое не позволяют воспользоваться преимуществами «пакетной» замены.
Была бы очень кстати функция вроде str_replace_callback — гипотетическая функция, позволяющая анализировать каждый случай подстановки. Но пока такой функции в PHP нет, придется все делать вручную.
О каких нюансах идет речь?
Пример из практики : требуется подстановка термина в тексте статьи на сайте и создание связанных с термином описаний, которые открываются при нажатии на термин. При этом хотелось бы, чтобы:
- описания не дублировались — т.е. при многократной замене одного и того же термина и его словоформ в тексте — его описание добавлялось лишь однократно,
- присутствовали описания только тех терминов, которые были найдены в тексте.
При пакетной замене str_replace, мы не сможем контролировать какие термины мы заменили. А регулярные выражения не позволят работать с любыми подстановками, т.к. нельзя использовать символы, участвующие в синтаксисе регулярных выражений.
Постановка задачи
Задача поставлена была для реализации в Drupal 7. Но может быть сформулирована в терминах любой CMS.
Термин — публикация, описывающая какой-либо специальный термин. Содержит — заголовок, текст описания и набор словоформ. Словоформы — это виды написания термина в публикациях, где будет произведена подстановка.
Необходимо создать функцию, которая находит словоформы термина в предложенном тексте и применяет к ним специальное HTML оформление. К тексту также добавляются описания найденных терминов (наподобие сносок).
Словоформа термина, обнаруженная в тексте, должна быть замена на следующий HTML код:
1 |
<term>словоформа</term> |
Реализация
Для реализации в Drupal, я добавил тип материала — termin, с произвольным полем field_wordforms для перечисления словоформ.
Помимо тега <term>, я буду добавлять ссылку на описание термина, добавляемого после текста.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
function _sc_termin_replace($txt) { /* словарь терминов структура следующая: [subs] = array( словоформа => код публикации-термина ) [descr] = array( код публикации-термина => описание ) */ static $termins = array(); /* при первом запуске функции, мы формируем словарь терминов, т.е. описанную выше структуру */ if (empty($termins)) { //выбираем публикации типа termin $res = db_query("SELECT nid FROM node WHERE type = 'termin'"); if ($res->rowCount()) { while ($row = $res->fetchObject()) { //грузим данные термина $nd = node_load($row->nid); //заполняем список словоформ foreach ($nd->field_wordforms['und'] as $v) $termins['subs'][trim($v['value'])] = $nd->nid; //формируем описания $descr = $nd->body['und'][0]['safe_value']; $termins['desc'][$nd->nid] = ' <a name="term-' . $nd->nid . '"></a> <div class="termin" id="term-' . $nd->nid . '"> <h3 class="title">' . $nd -> title . '</h3> <div class="descr">' . $descr . '</div> </div>'; } } } /* массив сносок для добавления в конец текста */ $repl = array(); //здесь мы ищем вхождения термина в тексте и //принимаем решение о его подстановке foreach ($termins['subs'] as $key => $nid) { $pos = 0; while (($L = mb_stripos($txt, $key, $pos)) !== FALSE) { //для начала нужно убедиться, что мы не находимся //внутри тега term. Это может случится, если словоформы //одного и того же термина или разных терминов входят //друг в друга $txt_sub = mb_substr($txt, 0, $L); $A = strrpos($txt_sub, '<term'); if ($A === FALSE) $A = -1; $B = strrpos($txt_sub, '</term'); if ($B === FALSE) $B = -1; if ($A > $B) { //мы внутри тега term $pos = mb_strpos($txt, '</term>', $L); if ($pos === FALSE) break; } else { //выполним замену термина //точное вхождение термина (с учетом регистра) $term = mb_substr($txt, $L, mb_strlen($key)); $term = '<term><a href="#term-' . $nid . '">' . $term . '</a></term>'; //добавляем сноску $repl[$nid] = $termins['desc'][$nid]; //замена $txt = $txt_sub . $term . mb_substr($txt, $L + mb_strlen($key)); $pos = $L + mb_strlen($term); } } } //вернем текст и список сносок return $txt . implode('', $repl); } |
Не всегда удобны описания терминов после текста. Лично я их показываю в отдельном «окне», создаваемом с помощью jquery плагина fancybox.
1 2 3 4 5 6 7 8 9 10 11 |
jQuery(document).ready(function($){ /* всплывающее окно для терминов */ $('term a').fancybox({ type: 'inline', maxWidth: 800, width: '70%', helpers: { overlay: { locked: false } } }); }); |
Завершающий штрих — добавим немного CSS :
1 2 3 4 5 6 7 8 |
.termin { display: none; } term > a { border-bottom: 1px dotted #000; text-decoration: none; color: inherit; } |