Colorear código con PHP (Parte 4 PHP)
Categorías : PHP, HTML, Programación.

Hoy seguimos con los tutoriales para pintar código utilizando PHP, y el objetivo será colorear código PHP.
Con este tutorial prácticamente se cierra el círculo para poder hacer una función que pinte un archivo HTML/PHP entero incluyendo posibles partes de código JavaScript, CSS y PHP.
A decir verdad ya dispongo de un objeto para esta web el cual creo que ya está completo, y que me permite pintar un archivo HTML/PHP entero del tirón, pero voy a dejar unos días mas para probarlo y probablemente lo liberare en la séptima entrega de los tutoriales "Colorear código con PHP" que supongo sacare a principios de la semana que viene.
Para el caso de hoy vamos a utilizar prácticamente el mismo algoritmo que utilizamos en la parte 3 : Colorear código con PHP (Parte 3 JavaScript). Por lo que no voy a extenderme demasiado explicando cosas que creo que ayer quedaron claras.
Lo primero será ver un ejemplo de código PHP coloreado que aunque no tenga sentido alguno, nos servirá para hacernos una idea de que partes necesitamos identificar y colorear.
<?php/* Comentario multilinea con alguna trampa "if" '\\' /* //*/function MiFuncion($Parametro = "NADA", $Valor = 0) {// Comentario hasta el final de la linea con alguna trampa /* "" { }$Texto = "Cadena de caracteres con comillas dobles y algunas trampas /* if \\";switch ($Texto) {case 333 :return false;case "Texto" :return true;default :break;}echo substr($Texto, 0, 5);}?>
Como podemos observar en el ejemplo anterior en esencia se utilizan 5 colores a parte del negro, y prácticamente queda todo el código de colorines.
Por una parte tenemos los comentarios en amarillo, variables en azul claro, cadenas de caracteres y valores en rojo, palabras reservadas para condiciones en verde, y palabras reservadas para funciones en azul.
Viendo esto ya sabemos que colores vamos a necesitar, por lo que ya podemos escribir el css pertinente :
.Codigo_AzulClaro { font-family:Courier New; font-size:12px; color:rgb(102, 129, 255); }.Codigo_AzulOscuro { font-family:Courier New; font-size:12px; color:#009; }.Codigo_Rojo { font-family:Courier New; font-size:12px; color:#CC0000; }.Codigo_Amarillo { font-family:Courier New; font-size:12px; color:rgb(255, 153, 0); }.Codigo_Verde { font-family:Courier New; font-size:12px; color:#006600; }
Una vez tenemos los colores escritos dentro del CSS podemos empezar con el array de delimitadores, y el array que contiene el diccionario de palabras php a colorear :
// Array de los caracteres delimitadores para PHP$_DelimitadoresPHP = array(" ", "(", ")", "[", "]", "+", "-", "/", "*", "=", "!", ",", ".", ";", "@", " ", "\n", "\r");// Array con el diccionario de palabras y su color pertinente para PHP$_DiccionarioPHP = array( array("Palabra" => "if" , "Color" => "Codigo_Verde"),array("Palabra" => "else" , "Color" => "Codigo_Verde"),array("Palabra" => "for" , "Color" => "Codigo_Verde"),array("Palabra" => "foreach" , "Color" => "Codigo_Verde"),array("Palabra" => "as" , "Color" => "Codigo_Verde"),array("Palabra" => "while" , "Color" => "Codigo_Verde"),array("Palabra" => "array" , "Color" => "Codigo_Verde"),array("Palabra" => "break" , "Color" => "Codigo_Verde"),array("Palabra" => "class" , "Color" => "Codigo_Verde"),array("Palabra" => "true" , "Color" => "Codigo_Verde"),array("Palabra" => "false" , "Color" => "Codigo_Verde"),array("Palabra" => "TRUE" , "Color" => "Codigo_Verde"),array("Palabra" => "FALSE" , "Color" => "Codigo_Verde"),array("Palabra" => "echo" , "Color" => "Codigo_Verde"),array("Palabra" => "return" , "Color" => "Codigo_Verde"),array("Palabra" => "include" , "Color" => "Codigo_Verde"),array("Palabra" => "public" , "Color" => "Codigo_Verde"),array("Palabra" => "protected" , "Color" => "Codigo_Verde"),array("Palabra" => "private" , "Color" => "Codigo_Verde"),array("Palabra" => "new" , "Color" => "Codigo_Verde"),array("Palabra" => "case" , "Color" => "Codigo_Verde"),array("Palabra" => "switch" , "Color" => "Codigo_Verde"),array("Palabra" => "default" , "Color" => "Codigo_Verde"),array("Palabra" => "global" , "Color" => "Codigo_Verde"),array("Palabra" => "+" , "Color" => "Codigo_Azul"),array("Palabra" => "-" , "Color" => "Codigo_Azul"),array("Palabra" => "*" , "Color" => "Codigo_Azul"),array("Palabra" => "/" , "Color" => "Codigo_Azul"),array("Palabra" => "=" , "Color" => "Codigo_Azul"),array("Palabra" => "{" , "Color" => "Codigo_Azul"),array("Palabra" => "}" , "Color" => "Codigo_Azul"),array("Palabra" => "fread" , "Color" => "Codigo_Azul"),array("Palabra" => "fopen" , "Color" => "Codigo_Azul"),array("Palabra" => "fclose" , "Color" => "Codigo_Azul"),array("Palabra" => "strlen" , "Color" => "Codigo_Azul"),array("Palabra" => "substr" , "Color" => "Codigo_Azul"),array("Palabra" => "strpos" , "Color" => "Codigo_Azul"),array("Palabra" => "print_r" , "Color" => "Codigo_Azul"),array("Palabra" => "filesize" , "Color" => "Codigo_Azul"),array("Palabra" => "function" , "Color" => "Codigo_Azul"),array("Palabra" => "file_exists" , "Color" => "Codigo_Azul"),array("Palabra" => "mysql_error" , "Color" => "Codigo_Azul"),array("Palabra" => "str_replace" , "Color" => "Codigo_Azul"),array("Palabra" => "xml_parser_free" , "Color" => "Codigo_Azul"),array("Palabra" => "xml_parser_create" , "Color" => "Codigo_Azul"),array("Palabra" => "xml_parse_into_struct" , "Color" => "Codigo_Azul"),array("Palabra" => "?>" , "Color" => "Codigo_Rojo"));
Cabe recordar que el primer array es el que se usara para saber cómo separar las palabras, y el segundo array se utilizara para saber que palabras hay que colorear y con qué color.
Al igual que con JavaScript el código tiene varias complicaciones para ser coloreado, así que vamos a hacer un resumen de las normas que deberíamos seguir :
- El primer problema que tenemos es que no queremos buscar palabras en el diccionario y re-emplazarlas si están dentro de un comentario o de una cadena de caracteres. Para evitar esto, lo mejor que se me ocurre es escanear todo el texto introducido una primera vez y separar el código en partes.
- Los comentarios deben ir en una parte del array sin separar nada de ellos.
- Los strings deben ir en una parte del array sin separar nada de ellos, además hay que diferenciar si el string empieza con una comilla o con comillas dobles.
- Las demás palabras deben ir separadas cuando se encuentre un delimitador.
- Los valores numéricos deben ir separados en una parte del array. Hay que identificar los valores numéricos de forma que tengan un delimitador al principio y un delimitador al final.
- Las variables deben ir separadas en una parte del array. Estas se pueden diferenciar porque llevan el símbolo del dólar delante : $
- Tanto los strings como los comentarios llevaran puesto el "<span>" al principio para determinar que esa posición del array no debe re-emplazarse nada más.
Con esto claro ya podemos ver la primera parte de la función PintarPHP :
// Función que colorea el texto PHP especificado según el esquema de colores de Dreamweaver// - $Texto : Texto PHP que queremos colorearfunction PintarPHP($Texto) {// $_Debug = true;global $_DiccionarioPHP;$Texto = str_replace(' ', ' ', $Texto); // Cambio tabulaciones por 4 espacios$Texto = str_replace('<', '<', $Texto); // Cambio el caracter '<' por '<'$Texto = str_replace('>', '>', $Texto); // Cambio el caracter '>' por '>'$TotalCaracteres = strlen($Texto);$Palabras = array();$TotalPalabras = 0;$Palabras[$TotalPalabras ++] = "<span class='Codigo_Negro'>";$PalabraActual = "";$Estado = ""; // Puede ser : Comentario, ComentarioML, String1, String2, Variable, y Numerofor ($i = 0; $i < $TotalCaracteres; $i++) {switch ($Estado) {case "" : // Sin estadoif ($Texto[$i] == "/" && $Texto[$i + 1] == "*") { // Principio ComentarioML$Estado = "ComentarioML";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "<span class='Codigo_Amarillo'>/*";$i++;}else if ($Texto[$i] == "/" && $Texto[$i + 1] == "/") { // Principio Comentario$Estado = "Comentario";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "<span class='Codigo_Amarillo'>//";$i++;}else if ($Texto[$i] == '"' && $Texto[$i - 1] != "\\") { // Principio String2$Estado = "String2";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = '<span class="Codigo_Rojo">"';}else if ($Texto[$i] == "'" && $Texto[$i - 1] != "\\") { // Principio String1$Estado = "String1";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "<span class='Codigo_Rojo'>'";}else if ($Texto[$i] == "$") { // Principio Variable$Estado = "Variable";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "<span class='Codigo_AzulClaro'>$";}else if (_EsNumero($Texto[$i]) == true && _BuscarDelimitadorPHP($Texto[$i - 1])) {$Estado = "Numero";$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "<span class='Codigo_Rojo'>".$Texto[$i];}else { // Cualquier letra$PalabraActual .= $Texto[$i];if (_BuscarDelimitadorPHP($Texto[$i]) == true) {// Para no gripar el array de palabras miramos que el siguiente caracter no sea un delimitador// Si no lo hacemos, creara un espacio en el array para cada delimitador, incluidos los caracteres ' '// De todas formas no estoy seguro si esto puede traer problemasif (_BuscarDelimitadorPHP($Texto[$i + 1]) != true) {$Palabras[$TotalPalabras ++] = $PalabraActual;$PalabraActual = "";}}}break;// Estado : comentario multi lineacase "ComentarioML" :if ($Texto[$i] == "*" && $Texto[$i + 1] == "/") {$Palabras[$TotalPalabras ++] = $PalabraActual."*/</span>";$PalabraActual = "";$Estado = "";$i++;}else $PalabraActual .= $Texto[$i];break;// Estado : comentario hasta el final de la lineacase "Comentario" :if ($Texto[$i] == chr(10) || $Texto[$i] == chr(13)) {$Palabras[$TotalPalabras ++] = $PalabraActual."</span>".$Texto[$i];$PalabraActual = "";$Estado = "";}else $PalabraActual .= $Texto[$i];break;// Estado : string de comillas doblescase "String2" :if (_FinString2($Texto, $i) == true) {$Palabras[$TotalPalabras ++] = $PalabraActual.'"</span>';$PalabraActual = "";$Estado = "";}else $PalabraActual .= $Texto[$i];break;// Estado : string de comillas simplescase "String1" :if (_FinString1($Texto, $i) == true) {$Palabras[$TotalPalabras ++] = $PalabraActual."'</span>";$PalabraActual = "";$Estado = "";}else $PalabraActual .= $Texto[$i];break;// Estado : en una variablecase "Variable" :if (_BuscarDelimitadorPHP($Texto[$i]) == true) {$Palabras[$TotalPalabras ++] = $PalabraActual."</span>";$PalabraActual = "";$Estado = "";}$PalabraActual .= $Texto[$i];break;// Estado : en un valor numéricocase "Numero" :if (_BuscarDelimitadorPHP($Texto[$i]) == true) {$Palabras[$TotalPalabras ++] = $PalabraActual."</span>";$PalabraActual = $Texto[$i];$Estado = "";}else $PalabraActual .= $Texto[$i];break;}}// Si se ha quedado algun estado abierto, cerramos su span// (los comentarios pueden quedar abiertos si se encuentran en la ultima linea)if ($Estado != "") $PalabraActual.= "</span>";// Pasamos la ultima palabra al array de palabras$Palabras[$TotalPalabras ++] = $PalabraActual;
En esta primera parte de la función recorremos todo el array $Texto mirando el $Estado en el que nos encontramos.
Hay varios estados, y dependiendo del estado se buscaran unos caracteres en concreto para salir de dicho estado. Podemos encontrar los siguientes estados : "" (sin estado), "ComentarioML" (comentario multilinea /* */), "Comentario" (comentario hasta el final de la línea //), "String1" (string con comillas simples 'str'), "String2" (string con comillas dobles "str"), "Variable" (una variable del código php, empieza por $) y "Numero" (un valor numérico).
Si el estado esta vacio / sin estado, lo que se hace es ir buscando hasta que un carácter nos indique un nuevo estado, y cuando hay un estado asignado, según el estado solo se podrá salir de él con cierta combinación de caracteres.
Al final de este bucle nos queda el array $Palabras lleno de palabras indefinidas, comentarios, strings, variables, y valores numéricos. Exceptuando las palabras indefinidas todas las demás ya estarán pintadas por lo que llevaran al principio un "<span>".
Veamos la segunda parte de la función PintarPHP :
// Post-análisis del array de palabrasfor ($i = 0; $i < $TotalPalabras; $i++) {if ($Palabras[$i][0] != '<') { // Si tiene '<' es el principio de un span por lo que no se debe tocarforeach ($_DiccionarioPHP as $Palabra) {$PosPalabra = strpos($Palabras[$i], $Palabra['Palabra']);// El operador !== también puede ser usado.// Puesto que != no funcionará como se espera porque si la posición de la palabra es 0.// La declaración (0 != false) se evalúa a false.if ($PosPalabra !== false) {$DelimitadorInicio = false;$DelimitadorFin = false;$TamPalabra = strlen($Palabra['Palabra']);// Miramos si el caracter anterior es un delimitadorif ($PosPalabra == 0)$DelimitadorInicio = true;else$DelimitadorInicio = _BuscarDelimitadorPHP($Palabras[$i][$PosPalabra - 1]);// Miramos si el caracter inmediatamente siguiente a la palabra es un delimitadorif ($PosPalabra + $TamPalabra == strlen($Palabras[$i]))$DelimitadorFin = true;else$DelimitadorFin = _BuscarDelimitadorPHP($Palabras[$i][$PosPalabra + $TamPalabra]);// Si la palabra esta bien delimitada la coloreamosif ($DelimitadorInicio == true && $DelimitadorFin == true) {$Palabras[$i] = str_replace( $Palabra['Palabra'],"<span class='".$Palabra['Color']."'>".$Palabra['Palabra']."</span>",$Palabras[$i] );break; // Salimos del foreach para no colorear 2 veces la misma palabra}}}}$TextoColoreado .= $Palabras[$i];}// Imprime el array para depurarif ($_Debug == true) { echo "PintarPHP<pre>"; print_r($Palabras); echo "</pre>"; }// Buscamos el inicio del código PHP que al ser con <?php no es detectable por el algoritmo (<?php)$TextoColoreado = str_replace("<?php", "<span class='Codigo_Rojo'><?php</span>", $TextoColoreado);return $TextoColoreado."</span>";}
En esencia en el post-análisis recorremos el array de palabras que creamos anteriormente donde deberíamos tener el código separado por comentarios, strings o palabras. Lo primero que miramos en cada palabra, es que el primer carácter no sea '<' porque si lo fuera estaríamos tocando un comentario o un string.
Luego recorremos el array $_DiccionarioPHP y miramos con la función strpos si encontramos la palabra del diccionario dentro de nuestro array de palabras del primer análisis. Si la encontramos, nos aseguramos que dicha palabra venga delimitada tanto al principio como al final con uno de los caracteres delimitadores. Y finalmente en el caso de que dicha palabra venga bien delimitada utilizamos str_replace para añadir un span con su color correspondiente.
Vista la función PintarPHP nos queda echar un vistazo varias funciones de apoyo. Empezaremos por _FinString1 y _FinString2 :
// Función que retorna si relamente esta al final de un string empezado con dobles comillas// - $Texto : Cadena de caracteres que contiene el string a analizar// - $Posicion : Posicion actual dentro de la cadena de caracteres// NOTA la razón de ser de esta función es que podemos encontrar una cadena con un terminador \", este terminador representa// una doble comilla, pero si la cadena es \\" lo que representa es una antibarra seguida de una doble comilla.function _FinString2(&$Texto, $Posicion) {if ($Texto[$Posicion] == '"') {// No es una cadena de escape que define una doble comillaif ($Texto[$Posicion - 1] == "\\" && $Texto[$Posicion - 2] != "\\") return false;else return true;}else return false;}// Función que retorna si relamente esta al final de un string empezado con comilla// - $Texto : Cadena de caracteres que contiene el string a analizar// - $Posicion : Posicion actual dentro de la cadena de caracteres// NOTA la razón de ser de esta función es que podemos encontrar una cadena con un terminador \', este terminador representa// una comilla, pero si la cadena es \\' lo que representa es una antibarra seguida de una comilla.function _FinString1(&$Texto, $Posicion) {if ($Texto[$Posicion] == "'") {// No es una cadena de escape que define una doble comillaif ($Texto[$Posicion - 1] == "\\" && $Texto[$Posicion - 2] != "\\") return false;else return true;}else return false;}
En la parte 3 de estos tutoriales se vio muy bien el porque de estas funciones, por lo que no me voy a extender mas. En esencia os dire que sirven para detectar el final correcto de un string.
Por ultimo queda echar un vistazo a las funciones _EsNumero y _BuscarDelimitadorPHP :
// Función que mira si el caracter pasado como parámetro es un numero// - $Caracter : Caracter del que queremos saber si es un numerofunction _EsNumero($Caracter) {switch ($Caracter) {case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' :return true;default :return false;}}// Función que busca si el caracter es un delimitador de palabra PHP// - $Caracter : Caracter que queremos compribar con la lista de delimitadoresfunction _BuscarDelimitadorPHP($Caracter) {global $_DelimitadoresPHP;foreach($_DelimitadoresPHP as $Delimitador) {if ($Caracter == $Delimitador) return true;}return false;}
No hay mucho que contar sobre estas dos funciones, la primera básicamente mira si el carácter es un numero o no, y la segunda mira si el carácter es el mismo que uno de los caracteres del array $_DelimitadoresPHP.
Y con esto terminamos por hoy, espero que este tutorial os sirva si algún día tenéis la intención de pintar un código PHP en vuestra web, o almenos para haceros una idea de como parsear códigos complicados con PHP.