NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Colorear código con PHP (Parte 3 JavaScript)

18 de Octubre del 2011 por Josep Antoni Bover, 0 visitas, 0 comentarios, 0 votos
Categorías : PHP, HTML, Programación, JavaScript.
La web está en segundo plano, animación en pausa.
Cargando animación...
Colorear código con PHP (Parte 3 JavaScript)

Siguiendo los tutoriales para pintar código utilizando PHP, hoy toca ver como pintaremos un archivo JavaScript.

A diferencia de los dos últimos tutoriales, este ya empieza a complicarse, ya que los códigos escritos en javascript tienen palabras clave que deben utilizar un tipo de color, y que debemos diferenciar del resto.

Por ello vamos a tener que hacer un diccionario de palabras clave para JavaScript de forma que nos sea fácil añadir palabras que vayamos descubriendo que van en otro color.

También hay que remarcar que las cadenas de caracteres tienen complicaciones añadidas ya que funcionan al estilo C, y estas pueden contener ciertos caracteres engañosos.

Para empezar vamos a ver un código JavaScript básico, de forma que nos podamos hacer una idea de que cosas necesitamos pintar :

Ejemplo absurdo de JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function NombreFuncion(Parametro) {
// Comentario con trampas \' "" '' if return alert parseInt
/* ComentarioML con trampas \" "" ''
if return alert parseInt */
var MiCadena = "Cadena Caracteres con trampas \" '' if return alert parseInt";
var MiNumero = 33;
switch (Parametro) {
case "Cosa" :
return;
default :
break;
}
if (MiCadena == parseInt(MiNumero)) alert("Imposible!");
}

A primera vista este código JavaScript no sirve para nada xD, pero podemos ver una aproximación de los colores que vamos a necesitar, y en qué casos necesitamos aplicarlos.

Hay tres tipos de palabras clave, las que son más comunes de cualquier lenguaje de programación : 'if', 'switch', 'break', 'return', 'default', 'var', etc... y estas van en un color azul oscuro. Las que son mas para tratamiento interno de datos java script, que en este caso solo vemos 'parseInt', pero hay otras como por ejemplo 'setInterval', 'setTimeout', 'eval', etc.. que irán en un color verde azulado. Y por ultimo palabras claves relacionadas con acciones del navegador que en este caso solo vemos 'alert' pero también existen otras como 'document', 'window', 'scroll', etc.. y estas van en un color lila.

Además de las palabras clave, vemos que las cadenas de caracteres van en un color azul más brillante, y que los valores numéricos van en color rojo.

Con este análisis ya podemos empezar a pensar un algoritmo viable para hacer el trabajo.

Lo ideal como se comento antes, seria tener un array que nos sirva de diccionario, al que fácilmente le podamos añadir más palabras clave con su color pertinente. Además de tener un diccionario de palabras también estaría bien tener un diccionario de delimitadores, ya que en este caso lo mas fácil va a ser separar todas las palabras que podamos en strings independientes, y para ello no nos vale separarlas solo si tienen un espacio como delimitador ya que por ejemplo podemos tener la sentencia "2+2", y los números deberían ir en rojo, pero el símbolo sumar debería ir en azul oscuro, y analizar esta cadena podría ser un infierno.

Bien entonces la propuesta más prometedora es hacer un array de palabras clave con su color correspondiente, y un array que contenga únicamente los delimitadores.

Arrays para el diccionario y los 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
// Array de los caracteres delimitadores para JavaScript
$_DelimitadoresJavaScript = array(" ", "(", ")", "[", "]", "+", "-", "/", "*", "=", "!", ",", ".", ";", "@", " ", "\n", "\r");
// Array con el diccionario de palabras y su color pertinente para JavaScript
$_DiccionarioJavaScript = array( array("Palabra" => "if" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "var" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "case" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "break" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "return" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "switch" , "Color" => "Codigo_AzulOscuro"),
array("Palabra" => "default" , "Color" => "Codigo_AzulOscuro"),
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" => "{" , "Color" => "Codigo_Azul"),
array("Palabra" => "}" , "Color" => "Codigo_Azul"),
array("Palabra" => "eval" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "Math" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "floor" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "String" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "parseInt" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "setTimeout" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "setInterval" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "clearInterval" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "fromCharCode" , "Color" => "Codigo_VerdeClaro"),
array("Palabra" => "stop" , "Color" => "Codigo_Lila"),
array("Palabra" => "alert" , "Color" => "Codigo_Lila"),
array("Palabra" => "scroll" , "Color" => "Codigo_Lila"),
array("Palabra" => "window" , "Color" => "Codigo_Lila"),
array("Palabra" => "document" , "Color" => "Codigo_Lila")
);

Creados los arrays con el diccionario y los delimitadores, ahora lo inmediatamente siguiente seria empezar a definir un css con los colores necesarios para pintar el código JavaScript. Además de los colores que vemos declarados en el diccionario, nos hace falta un color para los comentarios que irán en gris, y un color para los valores numéricos que irán en rojo :

CSS con los colores necesarios para el código
1
2
3
4
5
6
.Codigo_Gris { font-family:Courier New; font-size:12px; color:rgb(128, 128, 128); }
.Codigo_Lila { font-family:Courier New; font-size:12px; color:rgb(153, 0, 153); }
.Codigo_Azul { font-family:Courier New; font-size:12px; color:#0000FF; }
.Codigo_AzulOscuro { font-family:Courier New; font-size:12px; color:#009; }
.Codigo_VerdeClaro { font-family:Courier New; font-size:12px; color:rgb(0, 153, 153); }
.Codigo_Rojo { font-family:Courier New; font-size:12px; color:#CC0000; }

Llegados a este punto ahora toca empezar a idear el algoritmo. 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. La idea sería tener un array de cadenas de caracteres que contenga el texto separado según las siguientes normas :

Con esto claro en mente pasemos a ver la primera parte de la función PintarJavaScript :

Función PintarJavaScript 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
// Función que colorea el texto JavaScript especificado según el esquema de colores de Dreamweaver
// - $Texto : Texto JavaScript que queremos colorear
function PintarJavaScript($Texto) {
global $_DiccionarioJavaScript;
// $_Debug = true;
$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, y Numero
for ($i = 0; $i < $TotalCaracteres; $i++) {
switch ($Estado) {
case "" : // Normal
if ($Texto[$i] == "/" && $Texto[$i + 1] == "*") { // Principio ComentarioML
$Estado = "ComentarioML";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Gris'>/*";
$i++;
}
else if ($Texto[$i] == "/" && $Texto[$i + 1] == "/") { // Principio Comentario
$Estado = "Comentario";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Gris'>//";
$i++;
}
else if ($Texto[$i] == '"' && $Texto[$i - 1] != "\\") { // Principio String2
$Estado = "String2";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = '<span class="Codigo_Azul">"';
}
else if ($Texto[$i] == "'" && $Texto[$i - 1] != "\\") { // Principio String1
$Estado = "String1";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Azul'>'";
}
else if (_EsNumero($Texto[$i]) == true && _BuscarDelimitadorJavaScript($Texto[$i - 1])) {
$Estado = "Numero";
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "<span class='Codigo_Rojo'>".$Texto[$i];
}
else { // Cualquier letra
$PalabraActual .= $Texto[$i];
if (_BuscarDelimitadorJavaScript($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 (_BuscarDelimitadorJavaScript($Texto[$i + 1]) != true) {
$Palabras[$TotalPalabras ++] = $PalabraActual;
$PalabraActual = "";
}
}
}
break;
case "ComentarioML" :
if ($Texto[$i] == "*" && $Texto[$i + 1] == "/") {
$Palabras[$TotalPalabras ++] = $PalabraActual."*/</span>";
$PalabraActual = "";
$Estado = "";
$i++;
}
else $PalabraActual .= $Texto[$i];
break;
case "Comentario" :
if ($Texto[$i] == chr(10) || $Texto[$i] == chr(13)) {
$Palabras[$TotalPalabras ++] = $PalabraActual."</span>".$Texto[$i];
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
case "String2" :
if (_FinString2($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual.'"</span>';
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
case "String1" :
if (_FinString1($Texto, $i) == true) {
$Palabras[$TotalPalabras ++] = $PalabraActual."'</span>";
$PalabraActual = "";
$Estado = "";
}
else $PalabraActual .= $Texto[$i];
break;
case "Numero" :
if (_BuscarDelimitadorJavaScript($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.

Para entendernos hay varios estados : '', 'Comentario', 'ComentarioML', 'String1', 'String2', y 'Numero'.

Si el estado esta vacio es que podemos buscar cualquier carácter sin restricciones para determinar que debemos hacer con él. Por ejemplo si encontramos unas comillas dobles nos indica que empieza un string, por lo que el estado cambiaria a 'String2'.

Si el estado por ejemplo es 'String2' sabemos que solo podemos salir de ese estado si encontramos unas comillas dobles, pero como los strings del tipo C son algo quisquillosos tenemos que tener cuidado, ya que podemos encontrarnos por ejemplo esta secuencia \", que nos estaría indicando que dentro del string hay unas comillas dobles, y ojo porque estas no nos valen para salir del estado.

Por último hace falta remarcar que cuando salimos de cualquier estado creamos una nueva posición en el array $Palabras con el texto que andábamos analizando, y lo dejamos allí para el post-análisis.

Veamos el post-análisis :

Función PintarJavaScript 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
// Post analisis para el array de palabras separadas
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 ($_DiccionarioJavaScript 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 = _BuscarDelimitadorJavaScript($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 = _BuscarDelimitadorJavaScript($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 (hay que declarar la variable $_Debug y ponerla a true)
if ($_Debug == true) { echo "<pre>"; print_r($Palabras); echo "</pre>"; }
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 $_DiccionarioJavaScript 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 PintarJavaScript 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;
}

Anteriormente os contaba que las cadenas del estilo C son algo quisquillosas y ahora es voy a explicar bien el porqué. Si empezamos una cadena con dobles comillas esta debe terminar con dobles comillas, hasta aquí todo normal, pero que pasa si queremos que dicha cadena de caracteres tenga unas comillas dobles dentro? pues lo más fácil es utilizar la siguiente secuencia \" que indicara al navegador que queremos añadir una doble comilla. El carácter \ es bastante peculiar en cadenas del estilo C, ya que se usa principalmente para describir caracteres problemáticos como por ejemplo \t (tabulador), \n (salto de línea), etc... y si lo que queremos introducir precisamente el carácter \ debemos hacerlo mediante dos caracteres \ seguidos, por ejemplo "\\".

Bien veis la paradoja? si queremos indicar una anti barra al final de un string queda "\\" por lo que si miramos solo el carácter anterior a la doble comilla veremos la anti barra y creeremos que es una secuencia que describe una comilla dentro del string, cuando realmente no es cierto, si no que esa anti barra es la segunda anti barra indicando que queríamos introducir una anti barra en el string.

Pues esta es la razón de que existan estas dos funciones, y aunque podía haberlas incluido directamente dentro de la función PintarJavaScript, las tengo separadas porque en mi clase devildrey33_PintarCodigo las re-utilizo para varias funciones.

Por último veamos las funciones _EsNumero y _BuscarDelimitadorJavaScript :

Funciones _EsNumero y _BuscarDelimitadorJavaScript
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 JavaScript
// - $Caracter : Caracter que queremos compribar con la lista de delimitadores
function _BuscarDelimitadorJavaScript($Caracter) {
global $_DelimitadoresJavaScript;
foreach($_DelimitadoresJavaScript 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 $_DelimitadoresJavaScript.

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 JavaScript 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.