MareX WeBlog

Zvýrazňování syntaxe PHP zdrojových kódů pomocí PHP

REKLAMA

Tenhle script jsem narychlo „naškrábal“ asi před dvěma roky, když jsem jej nutně na cosi potřeboval. Tak nutně, že jsem ho ve finále ani nepoužil. Nemohl jsem totiž najít script, který jsem napsal několik let zpět a byl to kompletní „multihighlighter“. No kompletní zrovna ne, uměl v jednom textu zvýrazňovat syntaxi (X)HTML, CSS, JavaScriptu a PHP, což není zrovna vše, ale pro mé potřeby plně dostačující. Jenže pokud chcete něco později použít, je potřeba si to nejdřív zálohovat, což jsem já jaksi neudělal.

Proto vznikl tento „PHP only“ syntax highlighter. Teď jsem jej po té době náhodou objevil na disku a rozhodl se o něj podělit (já jsem ale hodný kluk :) ). Samozřejmě vím, že to asi není nic moc, každopádně to funguje. Jak může vypadat text prohnaný tímto scriptem, můžete vidět na následujícím obrázku.

Našel jsem v něm několik chyb, dvě z nich jsem opravil.

  1. Za každým řádkovým komentářem se vkládalo jedno odřádkování navíc. To jsem opravil vložením dalšího reguláru, který odstraní odřádkování ihned za komentářem – to, které tam bylo navíc. Nepřišel jsem totiž na to, jak upravit regulární výraz, který se stará o vytažení komentářů.
  2. Nebyly správně interpretovány proměnné v proměnných – tedy toto $$promenna – to jsem opravil úpravou reguláru '/(\$[a-z_]{1}[a-z0-9_]*)/i' na '/(\${1,2}[a-z_]{1}[a-z0-9_]*)/i'.
  3. Nesprávně jsou interpretovány některé typy proměnných v textových řetězcích uzavřených ve dvojitých uvozovkách – klasické proměnné, proměnné v proměnných a pole jsou správně, zbytek ne – neopraveno.

Tu poslední chybu jsem neopravil, protože když se na ten script podívám, tak nedokážu pochopit, jak jsem tehdy mohl „vyplodit“ některé ty šílené regulární výrazy :) PHP člověk ani po delší době nezapomene, ale v případě regulárních výrazů už je to poněkud složitější – tím spíš po dvou letech nepoužívání.

Každopádně zbytek funguje normálně, na jiné chyby jsem nepřišel. Ještě jedna věc: script předpokládá, že všechno, co se mu pošle ke zpracování je PHP. Nebere tedy v potaz počáteční a ukončovací značky PHP scriptu. Jednoduše vše, co se mu pošle se pokusí obarvit, jako by se jednalo o PHP i když tam budou například části HTML.

Co to tedy umí?

  1. Komentáře – řádkové, blokové, u blokových je odlišována dokumentace – tedy takový blokový komentář, jehož počáteční značka obsahuje více než jednu hvězdičku
  2. Textové řetězce – ve dvojitých uvozovkách jsou, na rozdíl od řetězců v jednoduchých uvozovkách, zvýrazňovány proměnné (výjimka viz chyba uvedená výše) a jsou rozpoznávány i řetězce vkládané pomocí syntaxe HEREDOC
  3. Klíčová/vyhrazená slova – (např. for, echo, break atd.) – seznam klíčových slov je v regulárním výrazu na řádku 32 a lze tím pádem doplňovat vlastní, nebo některá odstranit
  4. Funkce – odlišení funkcí vestavěných v PHP od uživatelských, vestavěné funkce jsou zobrazeny jako odkaz na PHP manuál s popisem příslušné funkce
  5. Symboly, číselné hodnoty
  6. Ostatní – ve většině případů se bude jednat o konstanty, případně chyby ve scriptu

Zde je tedy celý script (možnost stažení včetně CSS je na konci článku):

<?php
$specchars = Array (
  '#'    => '<##>',
  '<'    => '<#b>',
  '>'    => '<#f>',
  '&'    => '<#&>',
  '"'    => '<#dq>',
  "'"    => '<#sq>',
  '\\\\' => '<\\\\>',
  '""'   => '<#dq>SHemptystring<#dq>',
  "''"   => '<#sq>SHemptystring<#sq>'
);
 
$string_array = Array ();
 
function php_highlight ($text, $tab = 6) {
  global $specchars, $string_array;
  $stab = '';
  $text = stripslashes ($text);
 
  for ($i = 0; $i < $tab; $i++) { $stab .= ' '; }
 
  $text = str_replace ("\t", $stab, $text);
  $text = strtr ($text, $specchars);
 
  $replace_arr = Array (
    '/((?m:\/\/.*$)|(?m:<##>.*$)|(?s:\/\*+.*?\*+\/)|(?s:<#sq>.*?[^\\\]<#sq>)|(?s:<#dq>.*?[^\\\]<#dq>)|(?si:(?:<#b>){3}([a-z0-9]+)\s+.*?\n\2(\n|\r|\r\n|;)))/e' => 'replace_uniqid (\'\1\')', // řetězce, komentáře
    '/(\${1,2}[a-z_]{1}[a-z0-9_]*)/i' => '##span#variable#\1##endofspan##', // proměnné
    '/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(\s*\()/e' => 'php_functions (\'\1\', \'\2\')', // funkce
    '/(?<!\w)(0x[\da-f]+|\d+)(?!\w)/ix' => '##span#number#\1##endofspan##', // čísla
    '/([^\d\w>\<\#\$]+|<\#&>|<\#b>|<\#f>)/x' => '<span class="php-symbol">\1</span>', // symboly
    '/(?<!\w|\$)(and|or|xor|for|do|while|foreach|as|return|die|exit|if|then|else|elseif|new|delete|try|throw|catch|finally|class|function|string|array|object|resource|var|bool|boolean|int|integer|float|double|real|string|array|global|const|static|public|private|protected|published|extends|switch|true|false|null|void|this|self|struct|char|signed|unsigned|short|long|break|endif|echo|case|continue|isset|endswitch|unset|continue|default)(?!=|\w)/ix' => '<span class="php-resw">\1</span>'
  );
  $str = preg_replace (array_keys($replace_arr), array_values($replace_arr), $text);
 
  $back_replace = Array (
    '<#&>' => '&amp;',
    '<#b>' => '&lt;',
    '<#f>' => '&gt;',
    '<#dq>' => '&quot;',
    '<#sq>' => '&#39;',
    '##endofspan##' => '</span>',
    '##endofspanwithurl##' => '</span></a>',
    '##span#variable#' => '<span class="php-var">',
    '##span#number#' => '<span class="php-number">',
    '##span#ufunc#' => '<span class="php-ufunc">',
    '<##>' => '#'
  );
  $str = php_comstring ($str);
  $str = str_replace (array_keys($back_replace), array_values($back_replace), $str);
  $str = preg_replace ('/##span#phpfunc#url#([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)##/', '<a class="php-func" href="http://php.net/\1"><span class="php-phpfunc">', $str);
  $str = str_replace ('SHemptystring', '', $str);
  $str = str_replace ('#\\\\#', '\\\\', $str);
  $str = preg_replace ('/(<span class="php-comment">.*)(\n|\r|\r\n)(<\/span>)/u', '\1\3', $str);
 
  return '<span class="php-const">'.$str.'</span>';
}
 
/*******************************************************
 * Nahrazení řetězců unikátním ID (funkce uniqid())    *
 *******************************************************/
function replace_uniqid ($text) {
  global $string_array;
  $b = uniqid();
  $string_array['##cs'.$b.'##'] = $text;
  return '##cs'.$b.'##';
}
 
/***********************************************
 * Odlišení uživatelských funkcí od PHP funkcí *
 ***********************************************/
function php_functions ($text, $other) {
  if (function_exists($text)) {
    return '##span#phpfunc#url#'.$text.'##'.$text.'##endofspanwithurl##'.$other;
  } else {
    return '##span#ufunc#'.$text.'##endofspan##'.$other;
  }
}
 
/***************************************************************
 * Odlišení textových řetězců od komentářů a jejich zvýraznění *
 ***************************************************************/
function php_comstring ($text) {
  global $string_array;
  foreach ($string_array as $from => $to) {
    if (strpos ($to, '/**') === 0) { // dokumentace
      $text = str_replace ($from, '<span class="php-doc">'.$to.'</span>', $text);
    } else
    if (strpos ($to, '/') === 0 || strpos ($to, '<##>') === 0) { // komentář # nebo //
      $text = str_replace ($from, '<span class="php-comment">'.$to.'</span>', $text);
    } else
    if (strpos ($to, '<#dq>') === 0) { // řetězec ve dvojitých uvozovkách
      $to = preg_replace ('/((?:\{{1})(?<!\\\)(?:[\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:\}{1})|(?:\{{1})(?<!\\\)(?:[\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*) *\[.*\](\}{1})|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\[[0-9]+\])|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\-<#f>([\$]{0,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)))/', '<span class="php-var">\1</span>', $to);
      $text = str_replace ($from, '<span class="php-string">'.$to.'</span>', $text);
    } else
    if (strpos ($to, '<#b>') === 0) { // řetězec syntaxe HEREDOC
      $to = preg_replace ('/((?:\{{1})(?<!\\\)(?:[\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:\}{1})|(?:\{{1})(?<!\\\)(?:[\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*) *\[.*\](\}{1})|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\[[0-9]+\])|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\-<#f>([\$]{0,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))|((?<!\\\)([\$]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)))/', '<span class="php-var">\1</span>', $to);
      $to = preg_replace ('/(?si:((?:<#b>){3}([a-z0-9]+))(\s+.*?\n)(\2)(\n|\r|\r\n|;))/', '<span class="php-symbol">\1</span>\3<span class="php-symbol">\4\5</span>', $to);
      $text = str_replace ($from, '<span class="php-string">'.$to.'</span>', $text);
    } else { // ostatní: řetězec v jednoduchých uvozovkách
      $text = str_replace ($from, '<span class="php-string">'.$to.'</span>', $text);
    }
  }
  return $text;
}
?>

a mnou použitý CSS styl:

/* CSS Document */
/*body { font: 14px Consolas, 'Courier New', monospace; }*/
pre, code { font: 12px Consolas, 'Courier New', monospace; }
.php-comment     { color: rgb(83,207,255);  font-style: italic; font-weight: normal; }
.php-doc         { color: rgb(128,128,192); font-style: italic; font-weight: normal; }
.php-phpfunc     { color: rgb(0,128,0);     font-style: normal; font-weight: bold; }
.php-ufunc       { color: rgb(0,128,0);    font-style: normal; font-weight: bold; }
.php-const       { color: rgb(0,80,0);   font-style: normal; font-weight: bold; }
.php-number      { color: rgb(204,0,0);     font-style: normal; font-weight: bold; }
.php-resw        { color: rgb(0,0,0);       font-style: normal; font-weight: bold; }
.php-space       { color: rgb(0,128,128);   font-style: italic; font-weight: normal; }
.php-string      { color: rgb(79,79,79);    font-style: italic; font-weight: normal; }
.php-symbol      { color: rgb(255,0,128);   font-style: normal; font-weight: normal; }
.php-var         { color: rgb(0,0,255);     font-style: normal; font-weight: bold; }
 
a.php-func       { text-decoration: none; background-color: rgb(216,255,216); }
a.php-func:hover { text-decoration: none; background-color: rgb(255,195,195); }
 
.php-string:hover { background-color: rgb(220,220,220); }
.php-comment:hover { color: rgb(83,147,181); }
.php-doc:hover { color: rgb(59,59,116); }

Jak to použít? Jednoduše, do Vašeho scriptu, který se má starat o zpracování předávaného textu vložíte (například pomocí include) náš script:

include ('./syn-highlight.php');

a poté předáte text určený ke zpracování funkci php_highlight(), jejíž návratová hodnota je již upravený text (HTML kód):

$novy_text = php_highlight ($puvodni_text, $velikost_tab);

kde $puvodni_text je text předávaný ke zpracování a $velikost_tab je číselná hodnota, která znamená počet mezer, kterými má být nahrazen tabulátor. $velikost_tab je nepovinný parametr, pokud nebude zadán, bude použita výchozí hodnota, která je 2. Proměnná $novy_text samozřejmě obsahuje již přeformátovaný text.

Pokud by Vám nefungovala změna velikosti tabulátoru, ujistěte se, že Váš editor opravdu vkládá tabulátory a nikoliv mezery. Takový PSPad nemá například v nastavení defaultně zaškrtnutou položku ‘Skutečné tabulátory’ a tedy místo nich vkládá zadaný počet mezer. V takovém případě samozřejmě nebude mít změna parametru $velikost_tab na výstup žádný vliv.

Stažení (syn-highlight.php + styl.css): PHP-Syntax-Highlighter.zip (2.28 kB)

Jen tak pro zajímavost: zpracování 150 kB PHP kódu trvalo u mě na localhostu cca 5 vteřin.

REKLAMA

Zvýrazňování syntaxe PHP zdrojových kódů pomocí PHP