Errores PHP
X
Usuario
Password
0 FPS

Colorear código con PHP (Parte 4 PHP)

19 de Octubre del 2011 por Josep Antoni Bover, 2564 visitas, 2 comentarios, 1 voto con una media de 5 sobre 5.
Categorías : PHP, HTML, Programación.
La web está en segundo plano, animación en pausa.
Cargando animación...
Colorear código con PHP (Parte 4 PHP)

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.

Ejemplo de una función PHP absurda
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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 :

CSS con los colores necesarios
1
2
3
4
5
.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 :

Arrays con los delimitadores y las palabras reservadas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 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 :

Con esto claro ya podemos ver la primera parte de la función PintarPHP :

Función PintarPHP parte 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Función que colorea el texto PHP especificado según el esquema de colores de Dreamweaver
// - $Texto : Texto PHP que queremos colorear
function 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 Numero
for ($i = 0; $i < $TotalCaracteres; $i++) {
switch ($Estado) {
case "" : // Sin estado
if ($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 problemas
if (_BuscarDelimitadorPHP($Texto[$i + 1]) != true) {
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "";
}
}
}
break;
// Estado : comentario multi linea
case "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 linea
case "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 dobles
case "String2" :
if (_FinString2($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual.'"</span>';
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
// Estado : string de comillas simples
case "String1" :
if (_FinString1($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual."'</span>";
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
// Estado : en una variable
case "Variable" :
if (_BuscarDelimitadorPHP($Texto[$i]) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual."</span>";
$PalabraActual = "";
$Estado = "";
}
$PalabraActual .= $Texto[$i];
break;
// Estado : en un valor numérico
case "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 :

Función PintarPHP parte 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Post-análisis del array de palabras
for ($i = 0; $i < $TotalPalabras; $i++) {
if ($Palabras[$i][0] != '<') { // Si tiene '<' es el principio de un span por lo que no se debe tocar
foreach ($_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 delimitador
if ($PosPalabra == 0)
$DelimitadorInicio = true;
else
$DelimitadorInicio = _BuscarDelimitadorPHP($Palabras[$i][$PosPalabra - 1]);
// Miramos si el caracter inmediatamente siguiente a la palabra es un delimitador
if ($PosPalabra + $TamPalabra == strlen($Palabras[$i]))
$DelimitadorFin = true;
else
$DelimitadorFin = _BuscarDelimitadorPHP($Palabras[$i][$PosPalabra + $TamPalabra]);
// Si la palabra esta bien delimitada la coloreamos
if ($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 depurar
if ($_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 :

Funciones FinString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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 comilla
if ($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 comilla
if ($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 :

Funciones _EsNumero y _BuscarDelimitadorPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Función que mira si el caracter pasado como parámetro es un numero
// - $Caracter : Caracter del que queremos saber si es un numero
function _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 delimitadores
function _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.

Este código ha quedado obsoleto, por favor echa un vistazo a la versión 2 : Resaltar sintaxis de un código fuente.


[#2] devildrey33 19 Octubre del 2011 a las 18:01, votos 6 de 21.
A ti por seguir mi blog.
[#1] Alejandro 19 Octubre del 2011 a las 5:32, votos 2 de 2.
Justo lo que necesitaba! Gracias de nuevo :)