17 сент. 2018 г.
Всем привет! Сегодня расскажу про подсветку кода в markdown файлах. То, что описано в этой статье, Я использую непосредственно на этом сайте. Для отображения этой самой статьи!
Вероятно, проще всего сделать блог на wordpress. Но я же все-таки - программист!. Поэтому я предпочел сделать его на Symfony. Оба подхода имеют свои преимущества и недостатки. Так, например, мне удобнее писать статьи не в админке, а в IDE. И хранить все тексты не в базе данных, а в файлах под git.
Можно было бы просто под каждую статью сделать отдельный html файл. Но это не очень удобно, т.к. часть времени уходит на верстку и прописывание html тегов. Не сложно, но утомительно и рутинно.
Гораздо удобнее писать тексты в markdown. Также стоит учесть, что этот блог - про разработку и программирование. И поэтому важно, чтобы была удобная подсветка кода. Для наглядности я подготовил несложный пример. Итак, приступим!
Посты будем хранить в папке posts в формате .md
.
Пост может содержать markdown синтаксис а также чистый html.
Отдельно стоит обратить внимание на блоки кода.
Чтобы код красиво подсвечивался, нужно указать язык/технелогию. Например php
или bash
.
Пример поста в markdown формате: https://raw.githubusercontent.com/antonshell/markdown_geshi_example/master/posts/demo-markdown.md
Но, в конечном итоге, нам все равно понадобится html для отображения на сайте. Будем генерировать его автоматически с помощью парсера markdown. Я решил использовать парсер Parsedown.
Устанавливаем через composer:
composer require erusev/parsedown
Дальше парсим markdown и сохраняем в html файл.
Функция replaceContentBlocks отвечает за подсветку кода.
Реализовано в классе /src/PostBuilder.php
.
/**
* @param $file
*/
public function buildFromMarkdown($file)
{
$content = file_get_contents(__DIR__ . '/../posts');
$parsedown = new \Parsedown();
$content = $parsedown->text($content);
$content = $this->replaceContentBlocks($content);
$file = str_replace('.md','.html', $file);
file_put_contents(__DIR__ . '/../build/posts/' . $file, $content);
}
Для подсветки кода используем GeSHi Раньше импользовал этот сервис highlight.hohli. Который генерирует код с помощью этой библиотеки.
Устанавливаем через composer:
composer require geshi/geshi
Подсвечиваем блок кода:
$lang = 'php';
$code = '$content = $parsedown->text($content);
$content = $this->replaceContentBlocks($content);';
$geshi = new GeSHi($code, $lang);
$block = $geshi->parse_code();
Будет сгенерирован html такого вида:
<pre class="php" style="font-family:monospace;"><span style="color: #000088;">$content</span> <span style="color: #339933;">=</span> <span style="color: #000088;">$parsedown</span><span style="color: #339933;">-></span><span style="color: #004000;">text</span><span style="color: #009900;">(</span><span style="color: #000088;">$content</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span>
<span style="color: #000088;">$content</span> <span style="color: #339933;">=</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">replaceContentBlocks</span><span style="color: #009900;">(</span><span style="color: #000088;">$content</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span></pre>
Теперь нужно подсветить код непосредственно в markdown посте.
Нужно найти все блоки кода, вырезать, и вставить на их место сгенерированный подсвеченый html.
Реализовано в классе /src/PostBuilder.php
.
Сначала определяем все позиции, где начинаются блоки кода.
Ищем по ключевому слову <pre><code class="language
.
При обработке markdown блоки кода по-умолчанию оборачиваются в тег .
private $startBLockDelimiter = '<pre><code class="language';
/**
* @param $content
* @return array
*/
private function getCodeBlocksPositions($content){
$lastPos = 0;
$positions = [];
while (($lastPos = strpos($content, $this->startBLockDelimiter, $lastPos))!== false) {
$positions[] = $lastPos;
$lastPos = $lastPos + strlen($this->startBLockDelimiter);
}
return $positions;
}
Дальше обходим все блоки, для кадого определяем последнюю позицию.
/**
* highlight code blocks with geshi
*
* @param $content
* @return mixed
*/
private function replaceContentBlocks($content){
$newContent = $content;
// get start positions of code blocks
$positions = $this->getCodeBlocksPositions($content);
// iterate code blocks
foreach ($positions as $startPos) {
// get end position, get code block form text
$stripedContent = substr($content, $startPos);
$endPos = strpos($stripedContent, $this->endBlockDelimiter);
$replaceContent = substr($content, $startPos, $endPos + strlen($this->endBlockDelimiter));
$block = substr($content, $startPos, $endPos);
$startCodePos = strpos($stripedContent,'">') + 2;
$block = substr($block, $startCodePos);
$block = htmlspecialchars_decode($block);
// detect code block language
$lang = $this->getCodeBlockLanguage($replaceContent);
// highlight code block with geshi
$geshi = new GeSHi($block, $lang);
$block = $geshi->parse_code();
// replace source code with highlighted
$newContent = str_replace($replaceContent, $block, $newContent);
}
return $newContent;
}
Определяем язык:
/**
* detect code block language
*
* @param $replaceContent
* @return bool|string
*/
private function getCodeBlockLanguage($replaceContent){
$lang = self::DEFAULT_LANGUAGE;
if(strpos($replaceContent, $this->startBlockDelimiter) === 0){
$replaceContent = str_replace($this->startBlockDelimiter, '', $replaceContent);
$endPos = strpos($replaceContent, '">');
$lang = substr($replaceContent,0, $endPos);
$lang = trim($lang,'-');
}
return $lang;
}
Получаем блок, передаем его geshi. Потом заменяем исхдный код, вставляем на его место подсвеченый код.
Посты генерируются скриптом build_posts.php
<?php
use src\PostBuilder;
require '_bootstrap.php';
echo "Build posts ... \n";
$postBuilder = new PostBuilder();
$files = $postBuilder->getFiles();
foreach($files as $file) {
if(in_array($file, ['.', '..'])){ continue; }
echo "generate - $file ... \n";
$ext = pathinfo($file, PATHINFO_EXTENSION);
$postBuilder->buildFromMarkdown($file);
}
echo "Job is done \n";
Выполняем консольную команду. Нужно выполнять ее при каждом изменении постов.
php build_posts.php
Дальше отображаем на странице поста полученный .html файл. Вот и все. При необходимости можно поменять geshi на что-нибудь другое. Или добавить дополнительную обработку markdown.
Код примера доступен на github. На этом пока все. Спасибо за внимание!