Вероятно, в настоящее время разработка движка блога на чистом PHP с нуля не является самым разумным выбором, но я хотел закрепить свои навыки и знания веб-программирования, поэтому решил сделать это. Одним из побочных результатов стал несложный язык разметки текста, который я использую при написании заметок на этом сайте. Парсер этого языка вышел расширяемым, а сам синтаксис оказался удобным в использовании. Посмотрим на его реализацию поподробнее.
В первую очередь я составил список тех элементов, без которых никак не обойтись:
Поскольку все эти элементы имеют свои теги в HTML, то чтобы не усложнять задачу, построим наш синтаксис также на базе тегов. Определим общий вид элемента синтаксиса:
<тег>@ [параметры] @<тег>.
Например, если нам нужно определить заголовок, то можно записать: h2@ Текст заголовка @h2, что будет преобразовано парсером при отображении заметки в <h2>Текст заголовка</h2>.
Но почему именно символ @? На самом деле, выбор достаточно произвольный. Он редко встречается в обычном тексте, поэтому я на нем и остановился.
А если нужно записать символ @ в тексте? Да и как я до этого записывал примеры разметки без их преобразования в теги HTML? Все просто. Достаточно считать тег h2@ действительным лишь тогда, когда после h2 идет ровно один символ @. Если же я записываю две "собаки" подряд, то парсер пропускает такой тег, а в конце делает замену @@ --> @ (в исходном тексте заметки мне пришлось писать 4 "собаки" подряд, чтобы вывести 2).
С общей идеей разобрались, а теперь попробуем определить теги для каждого элемента из приведенного выше списка:
h2@ Заголовок @h2. Этот тег мы уже рассмотрели;img@ src, (=|-|+) @img. С изображениями немного сложнее. Мне бы хотелось иметь возможность выравнивать изображения по краям или по центру. Поэтому тег поддерживает два параметра: src - url изображения и одно из значений после запятой: =, - и + для выравнивания по центру, левому или правому краям соответственно;(ul|ol)@ Элементы списка @(ul|ol). Списки представлены блочным тегом. Элементы списка определяются символом перехода на новую строку. То есть каждая строка будет обернута HTML-тегом <li>;c@ inline-код @c и code@ block-код @code. Первый тег предназначен для отображения коротких фрагментов кода внутри параграфов и будет преобразован в <code>, а второй используется для вынесения более длинных блоков кода в отдельный абзац и оборачивается с помощью HTML-тега <pre>;a@ href, text @a. По аналогии с изображениями для ссылок используется два параметра: href - адрес ссылки и text - текст, который будет отображаться.Осталось написать функцию, которая бы на основе этой разметки выдавала корректный HTML-код. Практически ничего, кроме PHP-функции preg_replace_callback() нам для этого не понадобится. Главный цикл обработки тегов можно записать следующим образом:
foreach( $tags as $t ) {
$res = preg_replace_callback(
'/' . $t[ 'tag' ] . '@([^@].+[^@])@' . $t[ 'tag' ] . '/suU',
$t[ 'func' ],
$res
);
}
Каждый тег, который нам нужен, помещен в массив $tags, по которому проходит цикл. При этом сами теги определяются именем 'tag' и функцией обратного вызова 'func'.
Как видно, функции preg_replace_callback() мы передаем три аргумента:
Рассмотрим пример реализации обработчика одного из тегов, чтобы проанализировать выполняемые действия. Вот так можно определить элемент H2:
function parseH2( $m ) {
return '<h2>' . mb_trim( $m[ 1 ] ) . '</h2>';
}
const TAG_H2 = [ 'tag' => 'h2', 'func' => 'parseH2' ];
Здесь все достаточно просто. После подстановки 'tag' в приведенное выше регулярное выражение получаем:
'/h2@(^[@].*[^@])@h2/suU'.
Обратим внимание на ключи поиска в конце регулярного выражения:
s - заставляет рассматривать весь обрабатываемый текст в виде одной строки. То есть символ точка ( . ) в регулярном выражении начинает включать и символ переноса на новую строку;u - включает режим совместимости с символами юникода;U - "ungreedy"-режим. Поиск сокращается до минимального соответствия шаблону, чтобы предотвратить объединение двух и более одинаковых тегов в один.Таким образом, каждый набор символов в тексте, который находится между h2@ и @h2, а также начинается и заканчивается любым символом, отличным от @, будет передан в функцию обратного вызова parseH2().
Функция parseH2() достаточно проста. В качестве входного аргумента preg_replace_callback() передает ей массив из двух элементов:
$m[ 0 ] - содержит весь найденный текст, включая теги h2@ и @h2;$m[ 1 ] - содержит лишь то, что в регулярном выражении было обернуто в скобки, то есть текст нашего заголовка, который мы и хотим обернуть в HTML-тег <h2>.Здесь стоит отметить, что на момент написания заметки в PHP еще не было функции mb_trim(). Она нам нужна, чтобы обрезать пробельные символы в начале и конце строки. Можно было бы воспользоваться стандартной функцией trim(), но она не работает с символами юникода. Код mb_trim() честно взят из комментариев с официальной страницы документации PHP и имеет следующий вид:
function mb_trim( $str, $trimChars = '\s' ) {
return preg_replace( '/^[' . $trimChars . ']*(?U)(.*)[' . $trimChars . ']*$/su', '\1', $str );
}
Теперь мы можем размечать заголовки. Попробуем добавить поддержку списков:
function parseListItems( $str ) {
return preg_replace_callback(
'/^(.*)$/um',
function( $m ) {
$inLI = mb_trim( $m[ 1 ] );
if( empty( $inLI ) ) return '';
return '<li>' . $inLI . '</li>';
},
mb_trim( $str )
);
}
function parseOL_UL( $m ) {
$tag = $m[ 1 ];
return '<' . $tag . '>' . parseListItems( $m[ 2 ] ) . '</' . $tag . '>';
}
const TAG_UL_OL = [ 'tag' => '(ul|ol)', 'func' => 'parseOL_UL' ];
И вновь все достаточно просто. Всего несколько отличий от H2. Поскольку нет смысла делать разные обработчики для тегов UL и OL, то мы определили параметр 'tag' => '(ul|ol'), то есть ul или ol. Причем, в функции-обработчике parseOL_UL() мы узнаем о том, какой же тег был выбран через параметр $m[ 1 ]. А элементы списка, которые находятся в $m[ 2 ], построчно обернем в HTML-тег <li> с помощью функции parseListItems(). Принцип ее работы также построен на вызове preg_replace_callback(). Но есть и несколько отличий:
s в регулярном выражении задействован флаг m, который позволяет рассматривать каждую строку отдельно, выделяя начало символом ^ и конец символом $. Кроме того, флаг U в этом случае нам не нужен вовсе, поскольку мы работаем со всей строкой целиком;Есть свои особенности и у реализации тегов A и IMG. Рассмотрим, например, первую из них:
function parseA( $m ) {
return preg_replace_callback(
'/([^,]+)\s*,\s*(.+)/um',
function( $m ) {
$href = mb_trim( $m[ 1 ] );
$text = mb_trim( $m[ 2 ] );
return '<a href="' . $href . '">' . $text . '</a>';
},
mb_trim( $m[ 1 ] )
);
}
const TAG_A = [ 'tag' => 'a', 'func' => 'parseA' ];
Как и при обработке элементов списков мы воспользовались еще одним вызовом preg_replace_callback(). Регулярное выражение построено таким образом, что сначала стоит href ссылки, то есть набор символов до запятой, а после запятой следует текст, который будет отображаться на странице. После чего все это преобразуется в HTML и возвращается для вывода.
Остальные теги строятся по тем же принципам, поэтому на них мы останавливаться не будем. Но как же абзацы? Сейчас все, что не будет обернуто в наши теги, отобразится браузером в виде сплошного текста без переносов строк, что не так уж смертельно, но, очевидно, не оптимально. Поэтому весь оставшийся текст, который находится между блочных HTML-тегов, обернем с помощью <p> по аналогии с тем, как мы делали это для элементов списков:
function parsePlainText( $str ) {
return preg_replace_callback(
'/^(.*)$/um',
function( $m ) {
$inP = mb_trim( $m[ 1 ] );
if( empty( $inP ) ) return '';
return '<p>' . $inP . '</p>';
},
mb_trim( $str )
);
}
define( 'BLOCK_TAGS', 'h2|ul|ol|pre' );
$res = preg_replace_callback(
'#(^|</(' . BLOCK_TAGS .').*>)(.*)($|<(' . BLOCK_TAGS . ')>)#suU',
function( $m ) {
return $m[ 1 ] . parsePlainText( $m[ 3 ] ) . $m[ 4 ];
},
mb_trim( $res )
);
Представленный выше код выделяет каждый фрагмент текста, который находится между блочными HTML-тегами, определенными с помощью define, или началом/концом содержимого заметки. А потом оборачивает каждую непустую строку получившихся фрагментов с помощью <p>, как мы это делали с <li>. В результате все, что осталось, будет разбито на абзацы с учетом всех переходов на новые строки.
Таким образом, код преобразования содержимого заметки принимает вид:
function parseH2( $m ) {
return '<h2>' . mb_trim( $m[ 1 ] ) . '</h2>';
}
const TAG_H2 = [ 'tag' => 'h2', 'func' => 'parseH2' ];
function parseListItems( $str ) {
return preg_replace_callback(
'/^(.*)$/um',
function( $m ) {
$inLI = mb_trim( $m[ 1 ] );
if( empty( $inLI ) ) return '';
return '<li>' . $inLI . '</li>';
},
mb_trim( $str )
);
}
function parseOL_UL( $m ) {
$tag = $m[ 1 ];
return '<' . $tag .'>' . parseListItems( $m[ 2 ] ) . '</' . $tag . '>';
}
const TAG_UL_OL = [ 'tag' => '(ul|ol)', 'func' => 'parseOL_UL' ];
function parseCode( $m ) {
return '<pre><code>' . mb_trim( $m[ 1 ] ) . '</code></pre>';
}
const TAG_CODE = [ 'tag' => 'code', 'func' => 'parseCode' ];
function parseC( $m ) {
return '<code>' . mb_trim( $m[ 1 ] ) . '</code>';
}
const TAG_C = [ 'tag' => 'c', 'func' => 'parseC' ];
function parseA( $m ) {
return preg_replace_callback(
'/([^,]+)\s*,\s*(.+)/um',
function( $m ) {
$href = mb_trim( $m[ 1 ] );
$text = mb_trim( $m[ 2 ] );
return '<a href="' . $href . '">' . $text . '</a>';
},
mb_trim( $m[ 1 ] )
);
}
const TAG_A = [ 'tag' => 'a', 'func' => 'parseA' ];
function parseIMG( $m ) {
return preg_replace_callback(
'/([^,]+)\s*,\s*([^\s]+)/us',
function( $m ) {
$src = mb_trim( $m[ 1 ] );
$align = mb_trim( $m[ 2 ] );
$class = 'center_img';
switch( $align ) {
case '-':
$class = 'left_img';
break;
case '+':
$class = 'right_img';
break;
}
return '<img src="' . $src . '" class="' . $class . '">';
},
mb_trim( $m[ 1 ] )
);
}
const TAG_IMG = [ 'tag' => 'img', 'func' => 'parseIMG' ];
function parsePlainText( $str ) {
return preg_replace_callback(
'/^(.*)$/um',
function( $m ) {
$inP = mb_trim( $m[ 1 ] );
if( empty( $inP ) ) return '';
return '<p>' . $inP . '</p>';
},
mb_trim( $str )
);
}
define( 'BLOCK_TAGS', 'h2|ul|ol|pre' );
function parseContent( $str, $tags = [ TAG_H2, TAG_UL_OL, TAG_CODE, TAG_C, TAG_A, TAG_IMG ] ) {
$res = $str;
foreach( $tags as $t ) {
$res = preg_replace_callback(
'/' . $t[ 'tag' ] . '@([^@].+[^@])@' . $t[ 'tag' ] . '/suU',
$t[ 'func' ],
$res
);
}
$res = preg_replace_callback(
'#(^|</(' . BLOCK_TAGS .').*>)(.*)($|<(' . BLOCK_TAGS . ')>)#suU',
function( $m ) {
return $m[ 1 ] . parsePlainText( $m[ 3 ] ) . $m[ 4 ];
},
mb_trim( $res )
);
$res = preg_replace( '/@@/u', '@', $res );
return $res;
}
Здесь уже нет ничего принципиально нового, но обратим внимание на следующую строку:
$res = preg_replace( '/@@/u', '@', $res ).
Как и планировалось. Каждая пара стоящих рядом символов @@ превращается в @.
Еще есть смысл обратить внимание на входной параметр $tags главной функции parseContent(). У него уже есть значение по умолчанию, которое включает все подготовленные нами теги. Но при желании можно убрать или добавить нужные обработчики, чтобы ограничить или расширить возможности разметки текста. В первом же параметре ожидается передача текста для обработки.
Таким образом, мы получили простой и расширяемый синтаксис для форматирования текста. И рассмотрели возможную реализацию его парсера средствами PHP на основе регулярных выражений и функции preg_replace_callback().
А при чем здесь тогда Hello, world? Исправляю эту оплошность:
#include <iostream>
int main( int argc, char** argv ) {
std::cout << "Hello, world!" << std::endl;
return 0;
}