NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Colorear código con PHP (Parte 5 C++)

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

Esta es la quinta entrega de los tutoriales para colorear código con PHP, que en este caso está dedicada a códigos C/C++ con el estilo grafico del VisualC.

Para ello utilizaremos un sistema muy parecido al que se utilizo con Colorear código con PHP (Parte 3 JavaScript) y Colorear código con PHP (Parte 4 PHP) adaptado para este caso concreto. Por ello vamos a disponer de un array que utilizaremos como Diccionario y que podremos modificar fácilmente en caso de encontrar más palabras reservadas que se me puedan haber pasado.

Lo primero que necesitamos es hacernos una idea de que partes del código queremos colorear y como, para ello vamos a ver el siguiente ejemplo de código C++ :

Ejemplo de un codigo en C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef EJEMPLO_H // Si no esta definido EJEMPLO_H
#define EJEMPLO_H /* definimos EJEMPLO_H */
#include <Cabcerea1.h>
#include "Cabecera2.h" // Comentario hasta el final de la linea
/* Comentario multilinea antes de la plantilla
algunas trampas "String\" if else endif */
template <typename T> class ClaseHija : public ClasePadre {
public:
ClaseHija(void);
const T Funcion(int ValorEntero, wchar_t Caracter);
}
#endif

En esencia podemos ver involucrados cuatro colores : verde para comentarios, rojo para cadenas de texto, azul para palabras reservadas, y negro para lo demás. Actualmente se usan mas colores en VisualStudio en fragmentos de código dentro de definiciones que no están definidas (En visual Studio 2008 salen en gris, en el 2010 ya salen con colores normales pero más claros), pero como no es viable parsear todos los documentos y hacerme una lista de definiciones para saber si el código encapsulado dentro de un "#ifdef" se va a compilar, omitiremos esta regla.

En definitiva vamos a necesitar cuatro colores, así que ya podemos ir definiéndolos en un archivo CSS :

Colores necesarios para el código C/C++
1
2
3
4
.Codigo_Rojo { font-family:Courier New; font-size:12px; color:#CC0000; }
.Codigo_Verde { font-family:Courier New; font-size:12px; color:#006600; }
.Codigo_Negro { font-family:Courier New; font-size:12px; color:#000000; }
.Codigo_Azul { font-family:Courier New; font-size:12px; color:#0000FF; }

Una vez tenemos los colores definidos podemos pasar a definir el diccionario de palabras reservadas, y el array para los delimitadores :

Diccionario de palabras reservadas, y delimitadores
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
// Array de los caracteres delimitadores para C/C++
$_DelimitadoresC = array(" ", "(", ")", "[", "]", "+", "-", "/", "*", "=", "!", ",", ".", ";", ":", "\n", "\r", "&");
// Array con el diccionario de palabras y su color pertinente para C/C++
$_DiccionarioC = array( array("Palabra" => "reinterpret_cast" , "Color" => "Codigo_Azul"),
array("Palabra" => "dynamic_cast" , "Color" => "Codigo_Azul"),
array("Palabra" => "static_cast" , "Color" => "Codigo_Azul"),
array("Palabra" => "const_cast" , "Color" => "Codigo_Azul"),
array("Palabra" => "namespace" , "Color" => "Codigo_Azul"),
array("Palabra" => "protected" , "Color" => "Codigo_Azul"),
array("Palabra" => "operator" , "Color" => "Codigo_Azul"),
array("Palabra" => "template" , "Color" => "Codigo_Azul"),
array("Palabra" => "typename" , "Color" => "Codigo_Azul"),
array("Palabra" => "unsigned" , "Color" => "Codigo_Azul"),
array("Palabra" => "typedef" , "Color" => "Codigo_Azul"),
array("Palabra" => "defined" , "Color" => "Codigo_Azul"),
array("Palabra" => "virtual" , "Color" => "Codigo_Azul"),
array("Palabra" => "default" , "Color" => "Codigo_Azul"),
array("Palabra" => "private" , "Color" => "Codigo_Azul"),
array("Palabra" => "comment" , "Color" => "Codigo_Azul"),
array("Palabra" => "wchar_t" , "Color" => "Codigo_Azul"),
array("Palabra" => "delete" , "Color" => "Codigo_Azul"),
array("Palabra" => "public" , "Color" => "Codigo_Azul"),
array("Palabra" => "inline" , "Color" => "Codigo_Azul"),
array("Palabra" => "return" , "Color" => "Codigo_Azul"),
array("Palabra" => "static" , "Color" => "Codigo_Azul"),
array("Palabra" => "sizeof" , "Color" => "Codigo_Azul"),
array("Palabra" => "signed" , "Color" => "Codigo_Azul"),
array("Palabra" => "struct" , "Color" => "Codigo_Azul"),
array("Palabra" => "friend" , "Color" => "Codigo_Azul"),
array("Palabra" => "switch" , "Color" => "Codigo_Azul"),
array("Palabra" => "double" , "Color" => "Codigo_Azul"),
array("Palabra" => "class" , "Color" => "Codigo_Azul"),
array("Palabra" => "break" , "Color" => "Codigo_Azul"),
array("Palabra" => "const" , "Color" => "Codigo_Azul"),
array("Palabra" => "short" , "Color" => "Codigo_Azul"),
array("Palabra" => "while" , "Color" => "Codigo_Azul"),
array("Palabra" => "false" , "Color" => "Codigo_Azul"),
array("Palabra" => "float" , "Color" => "Codigo_Azul"),
array("Palabra" => "bool" , "Color" => "Codigo_Azul"),
array("Palabra" => "case" , "Color" => "Codigo_Azul"),
array("Palabra" => "true" , "Color" => "Codigo_Azul"),
array("Palabra" => "void" , "Color" => "Codigo_Azul"),
array("Palabra" => "enum" , "Color" => "Codigo_Azul"),
array("Palabra" => "else" , "Color" => "Codigo_Azul"),
array("Palabra" => "char" , "Color" => "Codigo_Azul"),
array("Palabra" => "long" , "Color" => "Codigo_Azul"),
array("Palabra" => "this" , "Color" => "Codigo_Azul"),
array("Palabra" => "for" , "Color" => "Codigo_Azul"),
array("Palabra" => "new" , "Color" => "Codigo_Azul"),
array("Palabra" => "int" , "Color" => "Codigo_Azul"),
array("Palabra" => "do" , "Color" => "Codigo_Azul"),
array("Palabra" => "if" , "Color" => "Codigo_Azul")
);

A pesar de que todas las palabras reservadas van en color azul, prefiero preservar el espacio para indicar el color por si algún día nos sorprenden en una nueva versión con colores nuevos.

Ahora que tenemos el diccionario y los delimitadores, vamos a recordar cómo funcionaba el algoritmo para pintar código. Teníamos que recorrer todo el código carácter a carácter para encontrar pistas que nos permitieran determinar el color a utilizar.

Para hacer esto nos basábamos en una variable $Estado la cual nos indicaba que teníamos que buscar. Esta vez dividiré la función en tres partes, la primera parte será el principio del pre-escaneo y el estado "" que podemos llamarla "Sin estado" :

Función PintarC 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
// Función que colorea el texto C/Cpp especificado según el esquema de colores de VisualStudio
// - $Texto : Texto C/Cpp que queremos colorear
function PintarC($Texto) {
global $_DiccionarioC;
$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, Directiva, Directiva_Fin
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_Verde'>/*";
$i++;
}
else if ($Texto[$i] == "/" && $Texto[$i + 1] == "/") { // Principio Comentario
$Estado = "Comentario";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Verde'>//";
$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] == "#" && $Texto[$i + 1] != "#" && $Texto[$i - 1] != "#") {
$Estado = "Directiva";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Azul'>#";
}
else { // Cualquier letra
$PalabraActual .= $Texto[$i];
if (_BuscarDelimitadorC($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 (_BuscarDelimitadorC($Texto[$i + 1]) != true) {
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "";
}
}
}
break;

Cuando nos encontramos en el estado "Sin estado" nos dedicamos a buscar algún carácter que nos ayude a identificar otro posible estado que por ende significara un color distinto. Por ejemplo en la línea 18 miramos si el carácter actual es "/" y el que viene después es "*", en caso de que la comprobación sea cierta sabemos con seguridad de que nos encontramos al principio de un comentario multilinea, y por ello cambiamos el estado a "ComentarioML", además abrimos un span con la clase "Codigo_Verde" para que todo lo que introduzcamos después quede de ese color. También separamos todo el texto anterior y lo almacenamos en un espacio del array $Palabras.

Si la comprobación de estados no da ningún resultado pasamos al else en la línea 45, y lo que hacemos es añadir ese carácter en la variable $PalabraActual, luego miramos si el siguiente carácter es un delimitador, ya que en caso de serlo nos interesa mantener la palabra en un espacio del array separada del resto. Otra cosa a añadir es que miramos que el siguiente carácter no sea un delimitador, ya que por ejemplo si tenemos 10 espacios juntos uno detrás de otro estos acabarían introduciéndose cada uno en una posición del array de $Palabras, y esto para códigos enormes puede resultar malo para la memoria, y no queremos quedarnos sin memoria a medio parsing. De esta forma si hay varios delimitadores juntos se quedaran todos en una posición del array $Palabras en vez de ocupar un espacio cada uno.

Pasemos a ver la parte de los estados :

Función PintarC 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
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
// Estado : dentro de una directiva ej : #include
case "Directiva" :
if ($Texto[$i] == " ") {
$Palabras[$TotalPalabras ++] = $PalabraActual." </span>";
$PalabraActual = "";
$Estado = "Directiva_Fin";
}
else $PalabraActual .= $Texto[$i];
break;
// Estado : justo despues de una directiva.
// Hay que mirar por si empieza un simbolo <
case "Directiva_Fin" :
switch ($Texto[$i]) {
case "&" : // posible < o >
if (substr($Texto, $i, 4) == "<") { // <
$PalabraActual .= "<span class='Codigo_Rojo'><";
$i+=3;
}
else if (substr($Texto, $i, 4) == ">") { // >
$PalabraActual .= "></span>";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "";
$Estado = "";
$i+=3;
}
else $PalabraActual .= "&";
break;
case "/" : // Pasamos el estado a String1
if ($Texto[$i + 1] == "/") {
$Estado = "Comentario";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Verde'>//";
$i++;
}
else if ($Texto[$i + 1] == "*") {
$Estado = "ComentarioML";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Verde'>/*";
$i++;
}
else $PalabraActual .= $Texto[$i];
break;
case "'" : // Pasamos el estado a String1
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Rojo'>'";
$Estado = "String1";
break;
case '"' : // Pasamos el estado a String2
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = '<span class="Codigo_Rojo">"';
$Estado = "String2";
break;
case chr(10) : case chr(13) :
$PalabraActual .= $Texto[$i];
$Estado = "";
break;
default :
$PalabraActual .= $Texto[$i];
break;
}
break;
// Estado : dentro de un comentario multilinea
case "ComentarioML" :
if ($Texto[$i] == "*" && $Texto[$i + 1] == "/") {
$Palabras[$TotalPalabras ++] = $PalabraActual."*/</span>";
$PalabraActual = "";
$Estado = "";
$i++;
}
else $PalabraActual .= $Texto[$i];
break;
// Estado : dentro de un 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 : dentro de un string con comillas dobles
case "String2" :
if (_FinString2($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual.'"</span>';
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
// Estado : dentro de un string con comillas simples
case "String1" :
if (_FinString1($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual.'"</span>';
$PalabraActual = "";
$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;

La gran mayoría de estados ya los vimos en profundidad en los anteriores tutoriales, en este caso nos interesa pensar específicamente en los estados "Directiva" y "Directiva_Fin".

El estado "Directiva" indica que nos encontramos dentro de una directiva que empieza por #, como por ejemplo #include. Todo sería muy fácil si los includes fueran siempre entre comillas, pero no es el caso, ya que podemos encontrarnos includes que van entre "<" y ">". Estos includes buscan los archivos en los directorios especificados en las opciones del compilador, el caso es que esa parte de código va en rojo y no hay ningún otro momento en que esto suceda. Para solucionar este problema miramos cuando se encuentra un espacio después de la directiva y asignamos el estado "Directiva_Fin".

Al llegar al estado "Directiva_Fin" prácticamente es como si estuviéramos en la fase "Sin estado" pero con la diferencia de que necesitamos encontrar los caracteres "<" y ">" para poner en rojo su texto. Si os fijáis en la línea 14 miramos si el carácter actual es "&", y os preguntareis, porque? pues bien al principio de todo de la parte 1 de la función PintarC estamos re-emplazando los caracteres "<" y ">" por "&lt;" y "&gt;" ya que en html meter los caracteres "<" y ">" no suele ir muy bien por la simple razón de que las etiquetas van encapsuladas dentro de esos caracteres.

Más tarde en las líneas 15 y 19 se ve un if que contiene un substr que obtiene 4 caracteres el cual se compara con "<" y ">" realmente no se está utilizando esos caracteres si no que se está utilizando "&lt;" y "&gt;", pero al pasar el código a formato HTML se ven así.

Además de buscar el carácter "&" luego empezamos a buscar caracteres que nos indiquen el inicio de un comentario o el inicio de un string. De ser el caso cambiamos directamente el estado a "Comentario", "ComentarioML", "String1" y "String2" según convenga.

Por ultimo también miramos si se ha llegado al final de la línea de código, en ese caso volvemos al estado "Sin estado".

Ahora que ya tenemos más o menos claro el tema de los estados podemos ver la parte final de la función PintarC, en la que se realiza el post-escaneo :

Función PintarC parte 3, post-escaneo
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
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 ($_DiccionarioC 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 = _BuscarDelimitadorC($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 = _BuscarDelimitadorC($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 "PintarC<pre>"; print_r($Palabras); echo "</pre>"; }
return $TextoColoreado."</span>";
}

Esta ultima parte tiene por objetivo recorrer el array $Palabras que se ha construido anteriormente y determinar que partes del array no han sido inspeccionadas, para ellos simplemente miramos el primer carácter de cada posición del array y comprobamos si el carácter es "<". En caso de que no sea el carácter "<" sabemos que esa posición del array no empieza con un "span", y por lo tanto necesita ser analizada para buscar si tiene alguna palabra reservada que necesite ser pintada.

A parte de este código nos quedan varias funciones por comentar : _FinString1. _FinString2, y _BuscarDelimitadorC. Pero como son en esencia prácticamente iguales por no decir idénticas que en los dos últimos ejemplos las omitiré por hoy.

Y esto es todo! espero que este tutorial os sirva de ayuda. Como siempre podéis descargar el ejemplo, o ver la versión online.

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