Подсветка кода

Иногда необходимо разместить код программы в сети Интернет, чтобы его можно было просмотреть с помощью браузера. Подсветку кода программы, разметки или чего-либо еще можно организовать несколькими способами, но для начала необходимо вообще понять, в чем состоит проблема? Казалось бы, нет ничего проще: вставляеш код программы в редактор сайта или в чистый HTML и получаеш результат, но не тут то было. В HTML игнорируются двойные пробелы: 2 или более пробела будут выглядеть как 1, перенос строки не работает, а если еще какая-либо часть кода будет заключена между символами "меньше" и "больше": <текст>, то этот текст вообще не будет отображен

Отображение кода с помощью HTML

Для решения этой проблеммы в HTML существует специальный дескриптор <XMP></XMP>. Например, вот так будет выглядеть текст HTML разметки, заключенный в дескриптор <XMP></XMP>:

Пример использования тега <XMP> </XMP>
<table width="100%"> <tr> <td></td> </tr> </table>

К сожалению, этот тег хоть и поддерживается всеми популярными браузерами, но не входит ни в одну из спецификаций HTML. Код проходит валидацию только при переходном <!DOCTYPE>.

Существует еще 3 дискриптора HTML, которые могут помочь в вопросе вывода текста программы: <PRE></PRE>, <CODE></CODE> и <PLAINTEXT>. <PRE></PRE> хоть и поддерживается всеми спецификациями HTML и практичесски всеми браузерами не поможет в выводе текста HTML разметки, а <PLAINTEXT> - это вообще крайний случай. Более подробную информацию по этим тегам можно посмотреть в справочнике htmlbook.ru: <XMP></XMP>, <PRE></PRE>, <CODE></CODE> и <PLAINTEXT>

Отображение кода с помощью PHP

PHP располагает специальной функцией: highlight_string(), с помощью которой можно вывести на страницу код PHP скрипта. Выглядеть работа функции highlight_string() будет примерно так:

Вывод функции PHP highlight_string()
<?php 
  phpinfo
(); 
  
$b 3;
  
mysql_real_escape_string($text);
  for (
$i 0$i 10$i++)
    {
    
$b++;
    }
  
?>

Ну а как быть, если нужно отобразить на web-странице текст скрипта написаного на Perl, структуру MySQL, разметку CSS или еще что-то? Разукрашивать код вручную - занятие, по меньшей мере, не благодарное, не говоря уже про кол-во времени, необходимое на такую работу. И тут на помощь прийдет класс GeSHi, написанный на PHP. Аббревиатура расшифровуется так: "Generic Syntax Highlighter".

Подсветка кода с помощью PHP класса GeSHi

Для начала работы необходимо скачать сам класс. С документацией (английский вариант) PHP класса GeSHi можно ознакомиться на официальном сайте: http://qbnz.com/highlighter. На этом же сайте можно в online-режиме протестировать работу этого скрипта. Все скрипты, опубликованные ниже, тестировались с версией GeSHi 1.0.8.10. Эта версия поддерживает 112 языков программирования, со писком которых можно ознакомиться тут: открыть список.

Файлы GeSHi нужно разместить на сервере и подключить к скрипту с помощью include или require: include_once ('geshi/geshi.php'). Директории docs и contrib желательно удалить, если класс будет использоваться на общедоступной машине. Далее записать в переменную $code текст скрипта, а в переменной $lang указать, каким образом подсвечивать код (названия языка программирования или др). Текст для переменной $lang - имена файлов (без .php) интерпритаторов, которые расположены в директории geshi/geshi/. Не нужные, на ваш взгляд интерпритаторы, также, стоит удалить из geshi/geshi/ для экономии дискового пространства сервера. Далее подключаем класс $geshi = new GeSHi($source, $language) и выводим результат echo $geshi -> parse_code().

Пример простейшей подсветки кода с помощью GeSHi
  1. <?php
  2.  
  3. include_once ('geshi/geshi.php');
  4. $code = '$main = 45;
  5. for ($i = 1; $i < 10; $i++)
  6.  {
  7.  echo $i;
  8.  $main++;
  9.  }';
  10. $lang = 'php';
  11. $geshi = new GeSHi($code, $lang);
  12. echo $geshi -> parse_code();
  13.  
  14. ?>

Сам класс имеет множество настроек, с которыми можно ознакомится в документации к нему. Ниже приведен пример реального использования GeSHi с кратким обзором по его наладке под свои нужды.

Обработка длинного кода с помощью GeSHi весьма ресурсоемкая процедура, поэтому не стоит парсить код "на лету", при выводе его клиенту. Лучше всего хранить обработанный код в кэше.

Пример функций подсветки кода с помощью GeSHi
  1. function geshi_highlight_code($content)
  2.   {
  3.   $pattern_geshi = '/\[hcode=(.*?)\]\r?\n?(.*?)\r?\n?\[\/hcode\]/is';
  4.   return preg_replace_callback($pattern_geshi, 'hcode_filter_callback', $content);
  5.   }
  6.  
  7. function hcode_filter_callback($data)
  8.   {
  9.   include_once(geshi/geshi.php);
  10.   global $geshi_css_files;
  11.   $numbers = false;
  12.   $start_number = 1;
  13.   $inline = false;
  14.   $tabsize = 4;
  15.   $geshi_header_t = GESHI_HEADER_DIV;
  16.  
  17.   if (isset($data[2]))
  18.     {
  19.     $lang = strtolower($data[1]);
  20.     $code = $data[2];
  21.     if (empty($code))
  22.       return $code;
  23.     }
  24.   if (strstr($lang, '*'))
  25.     {
  26.     $inline = true;
  27.     $lang = str_replace('*', '', $lang);
  28.     $geshi_header_t = GESHI_HEADER_NONE;
  29.     }
  30.   if (preg_match('!\#{1}([0-9]{0,7})!', $lang, $match))  // Выборка номера строки, с которого начнется нумерация
  31.     {
  32.     $lang = preg_replace('!(\#{1}[0-9]{0,7})!', '', $lang);
  33.     $numbers = true;
  34.     $match[1] = intval($match[1]);
  35.     if ($match[1] > 0)    
  36.       $start_number = $match[1];
  37.     }
  38.   if ($lang === 'html')
  39.     {
  40.     $lang = 'html4strict';
  41.     }
  42.   if ($lang === 'js')
  43.     {
  44.     $lang = 'javascript';
  45.     }
  46.    
  47.     // Все файлы-парсеры кода доступные в GeSHi 1.0.8.10
  48.     $geshi_lang_arr = array('4cs', '6502acme', '6502kickass', '6502tasm', '68000devpac', 'abap', 'actionscript', 'actionscript3', 'ada', 'algol68',
  49.                             'apache', 'applescript', 'apt_sources', 'asm', 'asp', 'autoconf', 'autohotkey', 'autoit', 'avisynth', 'awk',
  50.                             'bascomavr', 'bash', 'basic4gl', 'bf', 'bibtex', 'blitzbasic', 'bnf', 'boo', 'c', 'c_loadrunner',
  51.                             'c_mac', 'caddcl', 'cadlisp', 'cfdg', 'cfm', 'chaiscript', 'cil', 'clojure', 'cmake', 'cobol',
  52.                             'coffeescript', 'cpp-qt', 'cpp', 'csharp', 'css', 'cuesheet', 'd', 'dcs', 'delphi', 'diff',
  53.                             'div', 'dos', 'dot', 'e', 'ecmascript', 'eiffel', 'email', 'epc', 'erlang', 'euphoria',
  54.                             'f1', 'falcon', 'fo', 'fortran', 'freebasic', 'fsharp', 'gambas', 'gdb', 'genero', 'genie',
  55.                             'gettext', 'glsl', 'gml', 'gnuplot', 'go', 'groovy', 'gwbasic', 'haskell', 'hicest', 'hq9plus',
  56.                             'html4strict', 'html5', 'icon', 'idl', 'ini', 'inno', 'intercal', 'io', 'j', 'java',
  57.                             'java5', 'javascript', 'jquery', 'kixtart', 'klonec', 'klonecpp', 'latex', 'lb', 'lisp', 'llvm',
  58.                             'locobasic', 'logtalk', 'lolcode', 'lotusformulas', 'lotusscript', 'lscript', 'lsl2', 'lua', 'm68k', 'magiksf',
  59.                             'make', 'mapbasic', 'matlab', 'mirc', 'mmix', 'modula2', 'modula3', 'mpasm', 'mxml', 'mysql',
  60.                             'newlisp', 'nsis', 'oberon2', 'objc', 'objeck', 'ocaml-brief', 'ocaml', 'oobas', 'oracle11', 'oracle8',
  61.                             'oxygene', 'oz', 'pascal', 'pcre', 'per', 'perl', 'perl6', 'pf', 'php-brief', 'php',
  62.                             'pic16', 'pike', 'pixelbender', 'pli', 'plsql', 'postgresql', 'povray', 'powerbuilder', 'powershell', 'proftpd',
  63.                             'progress', 'prolog', 'properties', 'providex', 'purebasic', 'pycon', 'python', 'q', 'qbasic', 'rails',
  64.                             'rebol', 'reg', 'robots', 'rpmspec', 'rsplus', 'ruby', 'sas', 'scala', 'scheme', 'scilab',
  65.                             'sdlbasic', 'smalltalk', 'smarty', 'sql', 'systemverilog', 'tcl', 'teraterm', 'text', 'thinbasic', 'tsql',
  66.                             'typoscript', 'unicon', 'uscript', 'vala', 'vb', 'vbnet', 'verilog', 'vhdl', 'vim', 'visualfoxpro',
  67.                             'visualprolog', 'whitespace', 'whois', 'winbatch', 'xbasic', 'xml', 'xorg_conf', 'xpp', 'yaml', 'z80',
  68.                             'zxbasic');
  69.  
  70.   if (!in_array($lang, $geshi_lang_arr)) //Запрашиваемый парсер GeSHi не найден
  71.     {
  72.     $parsed = '<pre><code>'.$code.'</code></pre>';
  73.     return $parsed;
  74.     }
  75.  
  76.   $geshi = new GeSHi($code, $lang);
  77.   $geshi -> enable_classes();
  78.   $geshi -> set_header_type($geshi_header_t);
  79.   $geshi -> set_overall_style('font-family: monospace;');
  80.   if ($numbers)
  81.     {
  82.     $geshi -> enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
  83.     $geshi -> set_line_style('color: #575757;');
  84.     $geshi -> set_overall_style('font-family: monospace;', true);
  85.     $geshi -> start_line_numbers_at($start_number);
  86.     }
  87.   $geshi -> set_tab_width($tabsize);
  88.   $geshi -> enable_keyword_links(false);
  89.  
  90.   $geshi_css_files['counter_code'] = $geshi_css_files['counter_code'] + 1;
  91.   if (!in_array($lang, $geshi_css_files['hcode']))
  92.     {
  93.     $geshi_css_files['hcode'][] = $lang;
  94.     // Запись CSS стилей в файл
  95.     $css_file = 'style/geshi_'.$lang.'.css';
  96.     if (!is_file($css_file))
  97.       {
  98.       $css_file_content = $geshi -> get_stylesheet();
  99.       $result_file_save = @file_put_contents($css_file, $css_file_content);      
  100.       if ($result_file_save === false)
  101.         $geshi_css_files['err'][] = 'невозможно записать файл '.$css_file;
  102.       else if ($result_file_save == 0)
  103.         $geshi_css_files['err'][] = 'записаный файл '.$css_file.' пуст';
  104.       }
  105.     }  
  106.  
  107.   $parsed = $geshi -> parse_code();
  108.  
  109.   if ($inline)
  110.     {
  111.     $parsed = '<span class="'.$lang.'">'.$parsed.'</span>';
  112.     }
  113.   return $parsed;
  114.   // Описание функции geshi_highlight_code(): http://obovsem.org.ua
  115.   }
  116.  
  117. $content = geshi_highlight_code($content);

Функция geshi_highlight_code() обрабатывает входящие данные, и выбирает все участки текста, заключенные в подобиие BB-кодов [hсode=php]...[/hсode], с последующей их подсветкой и возвратом результата. После знака равенства в [hсode=php] еще можно указать 2 параметра: # - нумерация строк кода и * - режим inline, который позволяет разместить подсвеченный код с окружающим его текстом в 1 строку. Сразу после знака # можно указать номер строки, с которой будет начинаться нумерация кода (например [hcode=#32css]), иначе нумерация пойдет с единицы.

В переменную $geshi_css_files, объявленную в начале функции hcode_filter_callback() глобальной, записываются данные о кол-ве вызовов функции, возможные ошибки записи файлов-стилей, и валидные названия языков программирования, поддерживаемые GeSHi. Поскольку данные из этой переменной будут доступны и за пределами функции, перебрав в цикле массив используемых языков программирования, можно записать их в БД.

Контейнер подcвечиваемого кода в GeSHi

Контейнер подсвечиваемого кода - теги HTML, обертывающие изменяемый GeSHi листинг скрипта. Задается он с помощью метода $geshi -> set_header_type($flag), где $flag - является константой. Константы флагов для метода $geshi -> set_header_type():

  • GESHI_HEADER_DIV - подсвечиваемый код и нумерация строк обертывается HTML-дескриптором <div>...</div>. Все совмещенные пробелы преобразуются в сочетания пробелов и спец. HTML сущности &nbsp;. Например, 9 идущих подряд пробелов будут заменены на такой код: " &nbsp; &nbsp; &nbsp; &nbsp; ". Этот подход позволяет не беспокоиться о проблемах длинных строк, которые могли бы сильно растянуть страницу в ширину, вплоть до появления горизонтальной прокрутки. Преобразование символов табуляции (Tab) происходит в соответствии с методом $geshi -> set_tab_width($tabsize). Номера строк создаются при помощи HTML-тегов нумерованных списков. Отрицательные моменты при использовании этого контейнера: увеличение объема кода за счет HTML-мнемоник &nbsp;, тега перехода на новую строку <br> и трудности, возникающие при копировании пронумерованного кода из окна браузера - в Internet Explorer и некоторых других код копируется вместе с номерами строк.
  • GESHI_HEADER_NONE - как следует из названия, вокруг подсвеченного кода теги не устанавливаются.
  • GESHI_HEADER_PRE - используется по умолчанию. Код заключается в HTML-тег <pre>...</pre>. Таким образом, пробелы хранятся как есть, и код обработанный этим методом занимает меньше места, в сравнении с GESHI_HEADER_DIV. Минусы - номера строк кода отображаются при помощи нумерованного списка, что вызывает ошибку при прохождении проверки на соответствии страницы стандартам HTML.
  • GESHI_HEADER_PRE_TABLE - подсвечиваемый код обертывается в HTML-тег <div>...</div> и используется таблица с двумя колонками в одной строке: одна колонка - для номеров строк, вторая - для текста кода. Содержимое первой и второй колонки, в свою очередь, заключается между дескрипторами <pre>...</pre>, как и при использовании GESHI_HEADER_PRE. Код, подсвеченный с помощью GESHI_HEADER_PRE_TABLE успешно проходит проверку на соответствие стандартам HTML, но имеются некоторые проблемы с браузерами, построенными на движке Gecko: Firefox и другие.
  • GESHI_HEADER_PRE_VALID - с отключенным отображением номеров строк, код обертывается также, как и с использованием GESHI_HEADER_PRE. Но если строки пронумерованы - проверка на соответствие страницы стандартам HTML проходит без ошибок. Минусы - более "тяжелый" код на выходе, за счет использования дополнительных HTML-дескрипторов, в сравнении с GESHI_HEADER_PRE.

Нумерация строк GeSHi

Нумерация строк может производится с выбранного числа. Для этого необходимо указать число отсчета с помощью $geshi -> start_line_numbers_at($number). Нумерование строк по умолчанию отключено, и включается с помощью метода $geshi -> enable_line_numbers($flag). Существуют 3 флага, позволяющих видоизменить отображение номеров строк:

  • GESHI_NORMAL_LINE_NUMBERS - обычная нумерация;
  • GESHI_FANCY_LINE_NUMBERS - нумерация с подсветкой номера каждой 5-ой строки;
  • GESHI_NO_LINE_NUMBERS - выключение нумерации строк кода.

Для того, чтобы выделялся не каждый 5-ый номер, а, например, каждый 3, в методе $geshi -> enable_line_numbers() существует 2-ой, дополнительный параметр, указываемый в виде числа. Запись

$geshi -> enable_line_numbers(GESHI_FANCY_LINE_NUMBERS, 3);

будет указывать классу на необходимость выделения каждого третего номера строки. Дополнительный параметр работает только в случае его использования с флагом GESHI_FANCY_LINE_NUMBERS, при использовании с другими флагами, он игнорируется.

Стилевые параметры номерам строк задаются с помощью метода $geshi -> set_line_style('color: #333;', 'color: #ff00ff;'). Второй параметр метода применяется только вместе с использованием флага GESHI_FANCY_LINE_NUMBERS и задает стилевое оформление подсвеченному номеру строки.

Стилевое оформление кода, подсвеченного с помощью GeSHi

По умолчанию GeSHi не использует классы CSS для оформления подсветки кода. Каждому тегу <pre>, <span> или <div> присваивается свой цвет и другие параметры напрямую в HTML: <div style="color: red;">...</div>. Это сделано для удобства первоночального запуска GeSHi. Но использование такого приема, без загрузки внешних фалов с разметкой CSS в приложениях с хорошей нагрузкой крайне не выгодно, так как расходуется больше трафика и используется больше места на диске для хранения CSS разметки. Да и стоит подумать о пользователях с медленным соединением с сетью! Чтобы включить поддержку CSS классов используется метод $geshi -> enable_classes() (без параметров). При необходимости в дальнейшем снова отключить классы CSS, метод используется так: $geshi -> enable_classes(false).

В документации GeSHi приводится пример, как генерировать CSS "на лету". Но поскольку обработка самого кода с помощью класса требует заметных серверных ресурсов, как говорилось выше, лучшим вариантом будет либо сгенерировать все подключаемые CSS файлы за 1 раз, либо же создавать их по мере необходимости, как это и реализовано в функции. Информация о том, какие CSS файлы необходимы для конкретной статьи, новости или сообщения форума сохраняется в отдельную БД, так как не в каждой же статье будет использоваться класс GeSHi.

Пример БД для хранения ссылок на подключаемые CSS-файлы
CREATE TABLE `prefix_article_geshicss` (
  `id` int(10) unsigned NOT NULL,
  `file_css` text NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM;

-- Дамп данных таблицы `prefix_article_geshicss`

INSERT INTO `prefix_article_geshicss` (`id`, `file_css`) VALUES
(10, 'php,html4strict,javascript,html5'),
(11, 'html4strict,php');

Запись данных в БД лучше всего проводить за пределами функций, сразу после их запуска.

Обнаружение ошибок GeSHi

При использовании подобия BB-кодов обвертывания скриптов и генерации CSS файлов способом, приведенным выше, важно учитывать, что [hсode=...] не всегда может содержать верное значение. Для обнаружения ошибок в функции hcode_filter_callback(), используется метод $geshi_err = $geshi -> error(), и в случае ее нахождения, язык подсветки устанавливается по умолчаию: $lang = 'html4strict'.

Ссылки на документацию из подсвечиваемого кода GeSHi.

Весьма интересными могут быть функциональные возможности GeSHi по добавлению ссылок на документацию. Это позволяет каждый HTML тег, имя функции PHP и т.д. превратить в ссылку, на раздел в соответствующей документации, например: <XMP></XMP>. Более подробные свединия, кассательно применения этого метода можно посомотреть в документации на английском языке: "3.11.3 Adding a Keyword Group" и "3.13 Keyword URLs". По умолчанию, ссылки на документацию включены, и для того, чтобы их отключить необходимо воспользоваться методом $geshi -> enable_keyword_links(false). Отключать ссылки приходится лишь по тому, что они являются "прямыми". К примеру, 50 "прямых" индекcируемых ссылок на 4 различных домена будут весьма плохо влиять на восприятие сайта поисковыми системами, такими как Google или Yandex.

Список файлов поддерживаемых GeSHi языков

Проверку правильности указания пользователем названия необходимого языка программирования лучше всего осуществлять с помощью заранее созданного массива, так как эта проверка будет быстрее, чем с помощью вызова метода $geshi -> error().

Создание массива доступных языков GeSHi
$i = 0;
$c = 0;  
include_once('geshi/geshi.php');
$available_lang_arr = scandir('geshi/geshi/');
foreach ($available_lang_arr AS $val)
  {
  if ($val === '.' OR $val === '..')
    continue;
  $val = str_ireplace('.php', '', $val);
  $c++;
  $i++;
  $rn = ' ';
  if ($i == 10)
    {
    $rn = "\r\n";
    $i = 0;
    }
  $content .= '\''.$val.'\','.$rn;
  }
$content = substr($content, 0, strlen($content) - 2);
$content = '$geshi_lang_arr = array('.$content.');';
echo $content.'<br><br>Общее кол-во файлов: '.$c;
Код работает только в PHP 5 и выше, в более ранних версиях функции scandir() не было.
Опубликовано: 2011/05/17
HTML-код ссылки на эту страницу:
<a href="https://petrenco.com/php.php?txt=11" target="_blank">Подсветка кода</a>
27663
Добавить комментарий
Ваш e-mail: (не виден посетителям сайта)
Ваше имя:
Комментарий:
Символы с картинки:
Только выделенные поля формы добавления комментариев обязательны к заполнению.