Recentemente, fiquei interessado em como o destaque do código é organizado por dentro. A princípio, parecia que tudo era muito complicado lá - uma árvore de sintaxe, recursão, e isso era tudo. No entanto, após uma inspeção mais detalhada, verificou-se que não há nada difícil aqui. Todo o trabalho pode ser feito em um ciclo com espiadas, além disso, expressões regulares quase nunca são usadas no script resultante.
Página de demonstração:
Marcador de código JavascriptIdeia principal
Declaramos a variável
state , que armazenará informações sobre em que parte do código estamos. Se, por exemplo,
state for igual a um, isso significa que estamos dentro de uma string com aspas simples. O script aguardará a citação de fechamento e ignorará todo o resto. A mesma coisa ao destacar comentários, regexp e outros elementos, cada um tem seu próprio valor de
estado . Assim, diferentes caracteres de abertura e fechamento não entrarão em conflito; em outras palavras, um código como este:
let a = '"\'"';
serão corretamente destacados, ou seja, esses casos causaram mais dificuldades.
Introdução
Determinamos os possíveis valores da variável state, bem como a cor na qual essa ou aquela parte do código será colorida, bem como uma lista de palavras-chave Javascript (que também serão destacadas):
estados const = {... const states = { NONE : 0, SINGLE_QUOTE : 1,
Em seguida, criamos uma função que pegará uma linha com o código e retornará o HTML final com o código destacado. Para realçar, os caracteres serão agrupados em SPAN com a cor especificada na variável
colors .
A função terá apenas um ciclo, que analisa cada caractere e adiciona SPANs de abertura / fechamento quando necessário.
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];
Primeiro, destaque os comentários: linha única e multilinha. Se o caractere atual e o próximo for uma barra, e eles não estiverem dentro da linha (
estado é 0, ou seja,
states.NONE ), então este é o começo do comentário. Mude o
estado e abra o SPAN com a cor desejada:
if (state == states.NONE && char == '/' && next == '/') { state = states.SL_COMMENT; output += '<span style="color: ' + colors.SL_COMMENT + '">' + char; continue; }
continue é necessário para que as seguintes verificações não funcionem e não ocorra um conflito.
Em seguida, aguardamos o final da linha: se o caractere atual for uma quebra de linha e no
estado um comentário de linha única, feche o SPAN e mude o
estado para zero:
if (state == states.SL_COMMENT && char == '\n') { state = states.NONE; output += char + '</span>'; continue; }
Da mesma forma, estamos procurando comentários com várias linhas, o algoritmo é exatamente o mesmo, apenas os caracteres que você procura são 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; }
O realce de strings ocorre de maneira semelhante, mas é necessário levar em consideração que as aspas finais podem ser escapadas com uma barra invertida e, portanto, já deixam de ser uma barra final.
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; }
O código é semelhante ao que já estava acima, só que agora não registramos o final da linha se houver uma barra invertida antes da cotação.
A definição de cadeias de caracteres duplas ocorre exatamente da mesma maneira e faz pouco sentido analisá-las em detalhes. Para completar a imagem, colocarei sob o 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; }
Literais regexp, que são facilmente confundidos com o sinal de divisão, merecem uma consideração separada. Voltaremos a esse problema no final do artigo, mas por enquanto estamos fazendo o mesmo com regexps e com strings.
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; }
Isso termina casos simples quando o início e o fim de um literal podem ser determinados por um a dois caracteres. Vamos começar a destacar números: como você sabe, eles sempre começam com um número, mas podem ter letras na composição (
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>' }
Aqui estamos procurando o início de um número: o caractere anterior não deve ser um número ou letra, caso contrário, os números nos nomes das variáveis serão destacados. Assim que o caractere atual não for um número ou uma letra que possa estar contida no literal de um número, feche o SPAN e defina o
estado como zero.
Todos os tipos possíveis de literais são destacados, a pesquisa por palavras-chave permanece. Para fazer isso, você precisa de um loop aninhado que olhe para o futuro e determine se o caractere atual é o início da palavra-chave.
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 + '">'; } }
Aqui nós olhamos, o caractere anterior não pode estar no nome da variável; caso contrário,
deixe que a palavra-chave seja destacada na
saída de palavras. Em seguida, o loop aninhado coleta a palavra mais longa possível até que um caractere não alfabético seja encontrado. Se a palavra recebida estiver na matriz de
palavras -
chave , abra o SPAN e comece a destacar a palavra. Assim que um caractere não alfabético for encontrado, isso significa o fim da palavra - portanto, feche o SPAN:
if (state == states.KEYWORD && !/[az]/i.test(char)) { state = states.NONE; output += '</span>'; }
A coisa mais simples permanece - o destaque dos operadores, aqui você pode simplesmente comparar com o conjunto de caracteres que pode ocorrer nos operadores:
if (state == states.NONE && '+-/*=&|%!<>?:'.indexOf(char) != -1) { output += '<span style="color: ' + colors.OPERATOR + '">' + char + '</span>'; continue; }
No final do loop, se nenhuma das condições que
continuar continua a causa for acionada, simplesmente adicionamos o caractere atual à variável resultante. Quando ocorre o início ou o fim de um literal ou palavra-chave, abrimos / fechamos o SPAN com cores; em todos os outros casos - por exemplo, quando a linha já está aberta, jogamos apenas um caractere de cada vez. Também vale a pena proteger os colchetes angulares de abertura, caso contrário, eles podem quebrar o layout.
output += char.replace('<', '&' + 'lt;');
Bug fix
De alguma forma, tudo parecia simples demais, e não em vão: com testes mais detalhados, houve casos em que a luz de fundo não funcionava corretamente.
A divisão é reconhecida como regexp. Para diferenciar uma da outra, será necessário alterar a maneira como a regexp é determinada. Declaramos a variável
isRegex = true , após o que tentaremos "provar" que isso não é regexp, mas um sinal de divisão. Não pode haver palavras-chave ou colchetes antes da operação de divisão - portanto, criamos um loop aninhado e vemos o que a barra enfrenta.
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--;
Embora essa abordagem resolva o problema, ainda não está isenta de falhas. Você pode ajustá-lo para que esse algoritmo também destaque incorretamente, por exemplo:
if (a) / regex / ou mais:
1 / / regex / / 2 . Por que uma pessoa que divide números em regexp precisa de destaque de código - essa é outra questão; o design é sintaticamente correto, embora não ocorra na vida real.
Existem problemas com a coloração regexp em muitos trabalhos, por exemplo, em
prism.js . Aparentemente, para o realce correto dos regexps, você precisa entender completamente a sintaxe, como os navegadores.
O segundo bug com o qual eu tive que lidar estava relacionado a barras invertidas. As aspas de fechamento não foram reconhecidas em uma sequência do formulário
'test \\' devido à presença de uma barra invertida na frente dele. De volta à condição que captura o final da linha:
if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\')
A última parte da condição precisa ser alterada: se a barra invertida for escapada (ou seja, houver outra barra invertida antes dela), registre o final da linha.
const closingCharNotEscaped = prev != '\\' || prev == '\\' && code[i-2] == '\\';
As mesmas substituições devem ser feitas na pesquisa de cadeias com aspas duplas e reversas, bem como na pesquisa de regexp.
Isso é tudo, você pode testar o destaque pelo link no início do artigo.