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. На этом пока все. Спасибо за внимание!