Recientemente me interesé en cómo se organiza el resaltado del código desde adentro. Al principio parecía que todo era muy complicado allí: un árbol de sintaxis, recursión, y eso era todo. Sin embargo, después de una inspección más cercana, resultó que no hay nada difícil aquí. Todo el trabajo se puede hacer en un ciclo con miradas de un lado a otro, además, las expresiones regulares casi nunca se usan en el script resultante.
Página de demostración:
resaltador de código JavascriptIdea principal
Declaramos la variable de
estado , que almacenará información sobre en qué parte del código estamos. Si, por ejemplo, el
estado es igual a uno, esto significa que estamos dentro de una cadena con comillas simples. El script esperará la cita de cierre e ignorará todo lo demás. Lo mismo con resaltar comentarios, expresiones regulares y otros elementos, cada uno tiene su propio valor de
estado . Por lo tanto, diferentes caracteres de apertura y cierre no entrarán en conflicto; en otras palabras, un código como este:
let a = '"\'"';
se resaltará correctamente, es decir, tales casos causaron la mayoría de las dificultades.
Empezando
Determinamos los posibles valores de la variable de estado, así como el color en el que se pintará esta o aquella parte del código, así como una lista de palabras clave de Javascript (que también se resaltará):
estados const = {... const states = { NONE : 0, SINGLE_QUOTE : 1,
A continuación, creamos una función que tomará una línea con el código y devolverá el HTML terminado con el código resaltado. Para resaltar, los caracteres se envolverán en SPAN con el color especificado en la variable de
colores .
La función tendrá solo un ciclo, que analiza cada carácter y agrega SPAN de apertura / cierre cuando sea necesario.
function highlight(code) { let output = ''; let state = states.NONE; for (let i = 0; i < code.length; i++) { let char = code[i], prev = code[i-1], next = code[i+1];
Primero, resalte los comentarios: una línea y varias líneas. Si el carácter actual y el siguiente es una barra diagonal y no están dentro de la línea (el
estado es 0, es decir,
estados .
NINGUNO ), entonces este es el comienzo del comentario. Cambiar
estado y abrir SPAN con el color deseado:
if (state == states.NONE && char == '/' && next == '/') { state = states.SL_COMMENT; output += '<span style="color: ' + colors.SL_COMMENT + '">' + char; continue; }
continuar es necesario para que las siguientes comprobaciones no funcionen y no se produzca un conflicto.
A continuación, esperamos el final de la línea: si el carácter actual es un salto de línea y en
estado un comentario de
una sola línea, cierre el SPAN y cambie el
estado a cero:
if (state == states.SL_COMMENT && char == '\n') { state = states.NONE; output += char + '</span>'; continue; }
Del mismo modo, estamos buscando comentarios de varias líneas, el algoritmo es exactamente el mismo, solo los caracteres que está buscando son diferentes:
if (state == states.NONE && char == '/' && next == '*') { state = states.ML_COMMENT; output += '<span style="color: ' + colors.ML_COMMENT + '">' + char; continue; } if (state == states.ML_COMMENT && char == '/' && prev == '*') { state = states.NONE; output += char + '</span>'; continue; }
El resaltado de las cadenas se produce de manera similar, solo debe tenerse en cuenta que la comilla de cierre se puede escapar con una barra diagonal inversa y, por lo tanto, ya no es una barra diagonal.
if (state == states.NONE && char == '\'') { state = states.SINGLE_QUOTE; output += '<span style="color: ' + colors.SINGLE_QUOTE + '">' + char; continue; } if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
El código es similar a lo que ya estaba arriba, solo que ahora no registramos el final de la línea si hubo una barra invertida antes de la cotización.
La definición de cadenas entre comillas dobles ocurre exactamente de la misma manera, y tiene poco sentido analizarlas en detalle. Para completar la imagen, los colocaré debajo del spoiler.
if (state == states.NONE && char == '' '') {... if (state == states.NONE && char == '"') { state = states.DOUBLE_QUOTE; output += '<span style="color: ' + colors.DOUBLE_QUOTE + '">' + char; continue; } if (state == states.DOUBLE_QUOTE && char == '"' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; } if (state == states.NONE && char == '`') { state = states.ML_QUOTE; output += '<span style="color: ' + colors.ML_QUOTE + '">' + char; continue; } if (state == states.ML_QUOTE && char == '`' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
Los literales Regexp, que se confunden fácilmente con el signo de división, merecen una consideración aparte. Volveremos a este problema al final del artículo, pero por ahora estamos haciendo lo mismo con expresiones regulares que con cadenas.
if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; } if (state == states.REGEX_LITERAL && char == '/' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
Esto finaliza casos simples cuando el principio y el final de un literal se pueden determinar con 1-2 caracteres. Comencemos por resaltar números: como saben, siempre comienzan con un número, pero pueden tener letras en la composición (
0xFF ,
123n ).
if (state == states.NONE && /[0-9]/.test(char) && !/[0-9a-z$_]/i.test(prev)) { state = states.NUMBER_LITERAL; output += '<span style="color: ' + colors.NUMBER_LITERAL + '">' + char; continue; } if (state == states.NUMBER_LITERAL && !/[0-9a-fnx]/i.test(char)) { state = states.NONE; output += '</span>' }
Aquí estamos buscando el comienzo de un número: el carácter anterior no debe ser un número o letra, de lo contrario, se resaltarán los números en los nombres de las variables. Tan pronto como el carácter actual no sea un número o una letra que pueda estar contenida en el literal de un número, cierre el SPAN y establezca el
estado en cero.
Todos los tipos posibles de literales están resaltados, la búsqueda de palabras clave permanece. Para hacer esto, necesita un bucle anidado que mire hacia adelante y determine si el carácter actual es el comienzo de la palabra clave.
if (state == states.NONE && !/[a-z0-9$_]/i.test(prev)) { let word = '', j = 0; while (code[i + j] && /[az]/i.test(code[i + j])) { word += code[i + j]; j++; } if (keywords.includes(word)) { state = states.KEYWORD; output += '<span style="color: ' + colors.KEYWORD + '">'; } }
Aquí miramos, el carácter anterior no puede estar en el nombre de la variable; de lo contrario,
deje que la palabra clave se resalte en la
salida de la palabra. Luego, el bucle anidado recopila la palabra más larga posible hasta que se encuentre un carácter no alfabético. Si la palabra recibida está en la matriz de
palabras clave , abra el SPAN y comience a resaltar la palabra. Tan pronto como se encuentre un carácter no alfabético, esto significa el final de la palabra; en consecuencia, cierre el SPAN:
if (state == states.KEYWORD && !/[az]/i.test(char)) { state = states.NONE; output += '</span>'; }
Lo más simple sigue siendo: el resaltado de operadores, aquí simplemente puede comparar con el conjunto de caracteres que pueden aparecer en los operadores:
if (state == states.NONE && '+-/*=&|%!<>?:'.indexOf(char) != -1) { output += '<span style="color: ' + colors.OPERATOR + '">' + char + '</span>'; continue; }
Al final del ciclo, si no se activa ninguna de las condiciones que
continúan la causa
continua , simplemente agregamos el carácter actual a la variable resultante. Cuando se produce el comienzo o el final de un literal o palabra clave, abrimos / cerramos el SPAN con color; en todos los demás casos, por ejemplo, cuando la línea ya está abierta, solo lanzamos un carácter a la vez. También vale la pena proteger los soportes de ángulo de apertura, de lo contrario pueden romper el diseño.
output += char.replace('<', '&' + 'lt;');
Corrección de errores
Todo parecía de alguna manera demasiado simple, y no en vano: con pruebas más exhaustivas, había casos en los que la luz de fondo no funcionaba correctamente.
La división se reconoce como regexp, para distinguir una de la otra, será necesario cambiar la forma en que se determina regexp. Declaramos que la variable
esRegex = true , después de lo cual trataremos de "probar" que esto no es regexp, sino un signo de división. No puede haber palabras clave o corchetes de apertura antes de la operación de división; por lo tanto, creamos un bucle anidado y vemos a qué se enfrenta la barra diagonal.
Como era antes if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; }
if (state == states.NONE && char == '/') { let word = '', j = 0, isRegex = true; while (i + j >= 0) { j--;
Aunque este enfoque resuelve el problema, todavía no está exento de defectos. Puede ajustarlo para que este algoritmo también se resalte incorrectamente, por ejemplo:
if (a) / regex / or so:
1 / / regex / / 2 . ¿Por qué una persona que divide los números en expresiones regulares necesita resaltar el código? Esta es otra pregunta; El diseño es sintácticamente correcto, aunque no ocurre en la vida real.
Hay problemas con la coloración regexp en muchas obras, por ejemplo en
prism.js . Aparentemente, para resaltar correctamente las expresiones regulares, debe comprender completamente la sintaxis, como lo hacen los navegadores.
El segundo error con el que tuve que lidiar estaba relacionado con las barras invertidas. No se reconoció una comilla de cierre en una cadena de la forma
'test \\' debido a la presencia de una barra invertida frente a ella. Volver a la condición que atrapa el final de la línea:
if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\')
La última parte de la condición debe cambiarse: si la barra invertida se escapa (es decir, hay otra barra invertida antes), entonces registre el final de la línea.
const closingCharNotEscaped = prev != '\\' || prev == '\\' && code[i-2] == '\\';
Se deben hacer los mismos reemplazos en la búsqueda de cadenas con comillas dobles e inversas, así como en la búsqueda de expresiones regulares.
Eso es todo, puede probar el resaltado mediante el enlace al comienzo del artículo.