У меня периодически появляется потребность в использовании «интерпретатора» BBCode в своих проектах (написанного на PHP), и постоянно нет времени искать какое-то более-менее удобоваримое решение, что в итоге выливается в использование или создание «костылей» для каждого конкретного случая.
Но вот, похоже, получилось найти то, что хотелось.
Моя претензия к подобным готовым решениям обычно в первую очередь заключается в неспособности этих библиотек правильно обрабатывать абзацы. Фактически они обычно вообще не используют абзацы (тэг P), вместо этого в результате своей работы они просто вставляют тег <br /> , заменяя обычные символы переноса строки. Я считаю такой метод эмулирования абзацев в 98 процентах, мягко говоря, не уместным. Но так как перенос строк по средствам <br /> намного легче реализовать вместо «человеческих» <p>, так большинство и делает 🙁 Некоторые даже придумывают оправдания, что мол с br даже правильнее, отчасти, из-за подобной лени разработчиков различных готовых библиотек, другая часть людей думает, что тэг P является устаревшим (ведь даже во многих готовых продуктах и сайтах абзацы формируются путем использования <br />) 🙂
Приступим
Но, кажется, есть свет в конце туннеля. Это готовый класс для работы с BBCode, который, судя по всему, отлично справляется со своей задачей (ничего лучше пока не видел). Единственный минус в том, что документация, представленная на сайте, не на русском языке. Этот минус я и хочу побороть в этой статье, приведя пример использования класса с русскими комментариями.
Для начала нужно скачать библиотеку (на момент написания статьи версия библиотеки была 0.3.3). В скачанном архиве в папке src вы обнаружите два нужных нам файла: stringparser.class.php и stringparser_bbcode.class.php.
Для примера предположим, что у нас есть пустой файл «index.php» и рядом с ним мы создадим папку «/bbcode/», содержащую в себе два упомянутых выше файла.
Для примера минимальное содержимое файла «index.php» должно быть таким (запустив этот пример можно будет сразу увидеть, работает ли библиотека):
< ?php
//Вставляем файл библиотеки
require_once 'bbcode/stringparser_bbcode.class.php';
//Создаем объект класса StringParser_BBCode
$bbcode = new StringParser_BBCode ();
//Добавляем объекту класса понятие о тэге [b]
//(в итоге только этот тэг и будет
//обрабатываться этим классом)
$bbcode->addCode ('b', 'simple_replace', null, array ('start_tag' => '<b>', 'end_tag' => '</b>'),
'inline', array ('block', 'inline'), array ());
//Обрабатываем тестовую строку и выводим ее в браузер
echo $bbcode->parse ('Тестовый текст, это слово должно быть [b]жирным[/b]');
?>
Функция addCode
Наибольший интерес в этом коде может вызвать, пожалуй, функция addCode у объекта класса StringParser_BBCode, вот ее прототип и список описание параметров:
void addCode (string $code, string $type, string $callback, string $params, string $content_type,
array $allowed_in, array $not_allowed_in);
Эта функция добавляет понятие о тех или иных кодах (bb-кодах) для объекта класса, чтоб он мог потом обнаружить эти коды в тексте и соответствующим образом обработать их. Т.е. можно сказать, что изначально объект класса StringParser_BBCode вообще ничего не знает о стандартных bb-кодах и не способен обработать как-либо их. Поэтому этот объект нужно будет после каждой инициализации «обучать» всем разновидностям bb-кодов.
- $code (в примере имеет значение ‘b’)
- Код, который нужно искать в тексте для обработки. Т.е. если указать код test , то потом в обрабатываемом тексте будет искаться тэг [test] и обрабатываться в соответствии с указаниями в других параметрах рассматриваемой функции.
- $type (в примере имеет значение ‘simple_replace’)
- Указание того, как тэг должен обрабатываться (какого он типа). Есть различные предопределенные типы тэгов, которые будут описаны ниже. В нашем же примере указан тип ‘simple_replace’ который указывает на то, что тэг будет парным (открывающийся тэг [b] и закрывающийся [/b]) и что эти тэги будут заменены на указанные ниже html тэги.
- $callback (в примере имеет значение null)
- Позволяет указать имя функции, которая должна будет вызваться при обработке найденного тэга в тексте. В случае с типом тэга ‘simple_replace’ такая функция не вызывается, и, соответственно, в этом параметре можно указать null.
- $params (в примере имеет значение array(‘start_tag’ => ‘‘, ‘end_tag’ => ‘‘))
- В этом параметре в основном указывается, какой нужно вставлять html тэг взамен bb тэга. Наименование параметров напрямую зависит от того, какой тип тэга мы указали в параметре $type.
- $content_type (в примере имеет значение ‘inline’)
- Тип внутреннего содержимого тега. Может принимать значения: ‘inline’, ‘block’, ‘link’, ‘image’. Если я не ошибаюсь, можно прописывать и свои типы чтобы потом можно было указывать для этого содержимого свои индивидуальные фильтры (пример использования фильтров смотрите ниже).
- $allowed_in (в примере имеет значение array (‘block’, ‘inline’))
- В этом параметре можно указать, внутри каких типов объектов может находиться создаваемый bb-код (его обработка будет просто игнорироваться в ином случае). В нашем примере мы указали, что элемент может находиться как внутри блочных элементов, так и внутри линейных.
- $not_allowed_in (в примере имеет значение array ())
- Имеет назначение, противоположное по смыслу предыдущему параметру.
Виды обработки тэгов
Описание вариантов значения параметра $type в функции addCode.
- ‘simple_replace’
- Описывает простой парный тэг. При использовании этого типа обработки тэга в параметре функции ‘params’ должны обязательно присутствовать две ячейки: $params[‘start_tag’] и $params[‘end_tag’]. ‘start_tag’ должен в себе содержать аналог открывающегося тэга в хтмл, а ‘end_tag’ – закрывающегося тэга соответственно.
- ‘simple_replace_single’
- То же самое, что ‘simple_replace’, но используется только лишь для одинарных тэгов, которые, собственно говоря, не имеют содержимого (типа br, hr и т.п.). Требует наличия только параметра $params[‘start_tag’].
- ‘callback_replace’
- При этом типе вы перекладываете на себя обработку по найденным совпадениям (с помощью своей callback функции) для парного тэга.
- ‘callback_replace_single’
- То же самое, что и ‘callback_replace’, но только для одинарных тэгов.
- ‘usecontent’
- То же самое что и ‘callback_replace’, только в содержимом такого тэга другие тэги не будут обрабатываться, например, это удобно для тэга code.
- ‘usecontent?’
- Этот тип может себя вести как ‘usecontent’ или же как ‘callback_replace’ в зависимости от ситуации. Актуальность того или иного варианта определяется за счет присутствия заранее предполагаемого атрибута в bb тэге. Если атрибут найден, то будет использоваться обработка ‘callback_replace’, в другом случае тэг будет обрабатываться как ‘usecontent’. Имя атрибута для поиска указывается через параметр $params[‘usecontent_param’]. Если указано имя default, то подразумевается значение атрибута, присвоенное непосредственно тэгу, например, [url=http://link], значением атрибута default будет текст «http://link». Этот прием часто используется, например, для тэга [URL]. Этот тэг может использоваться в двух формах: [url]http://www.example.com/[/url] и [url=http://www.example.com/]Текст ссылки, а так же [b]жирный[/b] текст[/url]. В первом случае будет использоваться тип ‘usecontent’, т.к. текст ссылки должен выводиться без какого-либо форматирования (и, собственно, сама ссылка будет некорректна, если в ней будут посторонние символы). В другом случае должен быть использован тип ‘callback_replace’, т.к. сама ссылка передается отдельным параметром, а текст, обрамленный в ссылку, вполне может содержать в себе какое-то форматирование.
Примечание: Можно указать несколько параметров для их поиска, для чего в $params[‘usecontent_param’] нужно передать не строку, а массив, содержащий строки. Например: $bbcode->addCode (…, array(‘usecontent_param’ => array (‘parameter1’, ‘parameter2’)), …);. - ‘callback_replace?’
- Является противоположным вариантом типа ‘usecontent?’. Если один из атрибутов, указанных в usecontent_param, встречается в тэге, он будет обработан как ‘usecontent’, в противном случае как ‘callback_replace’.
Пример кода из «боевых» условий
Вот пример файла index.php с более расширенной конфигурацией класса для обрабатывания большего числа тэгов, в нем же и можно понять, как работают callback функции и т.п.:
< ?php
//Вставляем файл библиотеки
require_once 'bbcode/stringparser_bbcode.class.php';
//Приводит разнообразные переводы строк
//разных операционных систем в единый формат (\n)
function convertlinebreaks ($text) {
return preg_replace ("/\015\012|\015|\012/", "\n", $text);
}
//Удалить все символы, кроме переводов строк
function bbcode_stripcontents ($text) {
return preg_replace ("/[^\n]/", '', $text);
}
//Функция для обработки ссылок
function do_bbcode_url ($action, $attributes, $content, $params, $node_object) {
if (!isset ($attributes['default'])) {
$url = $content;
$text = htmlspecialchars ($content);
} else {
$url = $attributes['default'];
$text = $content;
}
//Часть функции, которая занимается
//только валидацией данных тэга
if ($action == 'validate') {
if (substr ($url, 0, 5) == 'data:' || substr ($url, 0, 5) == 'file:'
|| substr ($url, 0, 11) == 'javascript:' || substr ($url, 0, 4) == 'jar:') {
return false;
}
return true;
}
//Непосредственное преобразование тэга в
//html вариант с возвращением результата
return '<a href="'.htmlspecialchars ($url).'">'.$text.'';
}
// Функция для вставки изображений
function do_bbcode_img ($action, $attributes, $content, $params, $node_object) {
//Часть функции, которая занимается
//только валидацией данных тэга
if ($action == 'validate') {
if (substr ($content, 0, 5) == 'data:' || substr ($content, 0, 5) == 'file:'
|| substr ($content, 0, 11) == 'javascript:' || substr ($content, 0, 4) == 'jar:') {
return false;
}
return true;
}
//Непосредственное преобразование тэга в
//html вариант с возвращением результата
return '<img src="'.htmlspecialchars($content).'" alt=""/>';
}
//Создаем объект класса StringParser_BBCode
$bbcode = new StringParser_BBCode();
//Добавляем фильтр (подробнее см. офф. документацию),
//задействуя нашу функцию convertlinebreaks, которая будет
//преобразовывать переводы строки в тексте к единому
$bbcode->addFilter (STRINGPARSER_FILTER_PRE, 'convertlinebreaks');
//Добавляем свои парсеры для разных типов объектов
//(подробнее см. офф. документацию)
//Мы указываем, через какую функцию должно пройти
//содержимое этих тэгов, например, через функцию
//htmlspecialchars для предотвращения XSS и т.д.
$bbcode->addParser (array ('block', 'inline', 'link', 'listitem'), 'htmlspecialchars');
$bbcode->addParser (array ('block', 'inline', 'link', 'listitem'), 'nl2br');
$bbcode->addParser ('list', 'bbcode_stripcontents');
//Добавляем bb-код [h1], используемый в виде:
//[h1]Текст заголовка первого уровня[/h1]
$bbcode->addCode ('h1', 'simple_replace', null, array ('start_tag' => '<h1>', 'end_tag' => '</h1>'),
'block', array ('listitem', 'block', 'link'), array ());
//Добавляем bb-код [h2], используемый в виде:
//[h2]Текст заголовка второго уровня[/h2]
$bbcode->addCode ('h2', 'simple_replace', null, array ('start_tag' => '<h2>', 'end_tag' => '</h2>'),
'block', array ('listitem', 'block', 'link'), array ());
//Добавляем bb-код [h3], используемый в виде:
//[h3]Текст заголовка третьего уровня[/h3]
$bbcode->addCode ('h3', 'simple_replace', null, array ('start_tag' => '<h3>', 'end_tag' => '</h3>'),
'block', array ('listitem', 'block', 'link'), array ());
//Добавляем bb-код [h4], используемый в виде:
//[h4]Текст заголовка четвертого уровня[/h4]
$bbcode->addCode ('h4', 'simple_replace', null, array ('start_tag' => '<h4>', 'end_tag' => '</h4>'),
'block', array ('listitem', 'block', 'link'), array ());
//Добавляем bb-код [h5], используемый в виде:
//[h5]Текст заголовка пятого уровня[/h5]
$bbcode->addCode ('h5', 'simple_replace', null, array ('start_tag' => '<h5>', 'end_tag' => '</h5>'),
'block', array ('listitem', 'block', 'link'), array ());
//Добавляем bb-код [h6], используемый в виде:
//[h6]Текст заголовка шестого уровня[/h6]
$bbcode->addCode ('h6', 'simple_replace', null, array ('start_tag' => '<h6>', 'end_tag' => '</h6>'),
'block', array ('listitem', 'block', 'link'), array ());
//Устанавливаем флаги для bb-кодов с h1 до h6,
//указывая, что они являются блочными элементами,
//что будет в дальнейшем благотворно влиять на умную
//генерацию html кода. Такой элемент, к примеру, не сможет
//находиться внутри других блочных элементов
$bbcode->setCodeFlag('h1', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
$bbcode->setCodeFlag('h2', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
$bbcode->setCodeFlag('h3', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
$bbcode->setCodeFlag('h4', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
$bbcode->setCodeFlag('h5', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
$bbcode->setCodeFlag('h6', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
//Добавляем bb-код [b], используемый в виде:
//[b]выделенный текст[/b]
$bbcode->addCode ('b', 'simple_replace', null, array ('start_tag' => '<b>', 'end_tag' => '</b>'),
'inline', array ('listitem', 'block', 'inline', 'link'), array ());
//Добавляем bb-код [i], используемый в виде:
//[i]наклонный текст[/i]
$bbcode->addCode ('i', 'simple_replace', null, array ('start_tag' => '<i>', 'end_tag' => '</i>'),
'inline', array ('listitem', 'block', 'inline', 'link'), array ());
//Добавляем bb-код [url], используемый в виде:
//[url]http://www.needsite.domain[/url] и
//[url=http://www.needsite.domain]Текст ссылки[/url]
$bbcode->addCode ('url', 'usecontent?', 'do_bbcode_url', array ('usecontent_param' => 'default'),
'link', array ('listitem', 'block', 'inline'), array ('link'));
//Добавляем bb-код [link], используемый в виде:
//[link]http://www.needsite.domain[/link]
$bbcode->addCode ('link', 'callback_replace_single', 'do_bbcode_url', array (),
'link', array ('listitem', 'block', 'inline'), array ('link'));
//Добавляем bb-код [img], используемый в виде:
//[img]http://www.needsite.domain/img.jpg[/img]
$bbcode->addCode ('img', 'usecontent', 'do_bbcode_img', array (),
'image', array ('listitem', 'block', 'inline', 'link'), array ());
//Добавляем bb-код [bild] (по смыслу то же самое,
//что и [img]), используемый в виде:
//[bild]http://www.needsite.domain/img.jpg[/bild]
$bbcode->addCode ('bild', 'usecontent', 'do_bbcode_img', array (),
'image', array ('listitem', 'block', 'inline', 'link'), array ());
//Создаем группу image из bb-кодов img и bild
//для последующей возможности задания
//неких правил для этих групп
$bbcode->setOccurrenceType ('img', 'image');
$bbcode->setOccurrenceType ('bild', 'image');
//Указываем, что тэги из группы image
//могут встречаться (обрабатываться) в тексте не более
//двух раз. В нашем случае это нужно для того,
//чтобы пользователь не мог вставить более двух
//картинок в текст сообщения
$bbcode->setMaxOccurrences ('image', 2);
//Добавляем bb-код [list]
$bbcode->addCode ('list', 'simple_replace', null, array ('start_tag' => '<ul>', 'end_tag' => '</ul>'),
'list', array ('block', 'listitem'), array ());
//Добавляем bb-код [*], указывая, что этот тэг
//может использоваться только внутри тэга
//с типом list (этот тип мы присвоили выше тэгу [list])
$bbcode->addCode ('*', 'simple_replace', null, array ('start_tag' => '<li>', 'end_tag' => '</li>'),
'listitem', array ('list'), array ());
//Устанавливаем флаги для тэгов [list] и [*]
//Указываем, что для кода [*] закрывающийся тэг
//не обязателен, таким образом, возможна будет
//следующая конструкция:
//[list]
//[*] Item
//[*] Item
//[/list]
//Закрывающий тэг будет добавляться автоматически
//в процессе формирования html кода
$bbcode->setCodeFlag ('*', 'closetag', BBCODE_CLOSETAG_OPTIONAL);
//Как я понял, этот флаг обозначает, что тэг [*]
//всегда может быть использован только
//в начале новой строки
$bbcode->setCodeFlag ('*', 'paragraphs', true);
//[list] является блочным элементом
$bbcode->setCodeFlag ('list', 'paragraph_type', BBCODE_PARAGRAPH_BLOCK_ELEMENT);
//Перед открывающимся тэгом [list]
//символ строки будет устранен
$bbcode->setCodeFlag ('list', 'opentag.before.newline', BBCODE_NEWLINE_DROP);
//Перед закрывающимся тэгом [list]
//символ строки будет устранен
$bbcode->setCodeFlag ('list', 'closetag.before.newline', BBCODE_NEWLINE_DROP);
//В итоге мы можем использовать списки в bb-коде,
//используя вместе теги list и *:
//[list]
//[*] Элемент списка
//[*] Элемент списка
//[*] и т.д.
//[/list]
//Активируем обработку параграфов
$bbcode->setRootParagraphHandling (true);
//Как я понял, таким образом указывается,
//какими символами нужно заменять встреченный
//перенос строки внутри абзаца
//(по сути, как обрабатывать пустые абзацы).
$bbcode->setParagraphHandlingParameters ("\n", '<p>', '</p>');
$res_text = "Тестовый текст [b]для проверки[/b] работы класса";
//На всякий случай удаляем все оставшиеся
//символы переноса строки в виде "\r",
//если такие остались в тексте
$res_text = str_replace("\r", '', $res_text);
//Вуаля!
echo $bbcode->parse($res_text);
Послесловие
Я, конечно же, не сделал полный перевод документации, а только самый необходимый минимум, за более подробной документацией вы можете обратиться на официальный сайт (вообще, там описано куда больше различных возможностей).
Эту библиотеку также не составляет труда внедрить в какой-либо php фреймворк, я, к примеру, с успехом проделывал это для cackePHP.
Если вы тоже встречали подобные библиотеки (корректно работающие с абзацам! 🙂 ) , интересно было бы узнать о них.