У меня периодически появляется потребность в использовании «интерпретатора» 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.
Если вы тоже встречали подобные библиотеки (корректно работающие с абзацам! 🙂 ) , интересно было бы узнать о них.