Leçon du soir: écriture de la coloration syntaxique

Récemment, je me suis intéressé à la façon dont la mise en évidence du code est organisée de l'intérieur. Au début, il semblait que tout y était extrêmement compliqué - un arbre de syntaxe, une récursivité, et c'était tout. Cependant, à y regarder de plus près, il s'est avéré qu'il n'y a rien de difficile ici. Tout le travail peut être effectué en un seul cycle avec des allers-retours, de plus, les expressions régulières ne sont presque jamais utilisées dans le script résultant.

Page de démonstration: Surligneur de code Javascript

Idée principale


Nous déclarons la variable d' état , qui stockera des informations sur la partie du code dans laquelle nous nous trouvons. Si, par exemple, l' état est égal à un, cela signifie que nous sommes à l'intérieur d'une chaîne avec des guillemets simples. Le script attendra la citation de clôture et ignorera tout le reste. La même chose avec la mise en évidence des commentaires, des expressions rationnelles et d'autres éléments, chacun a sa propre valeur d' état . Ainsi, différents caractères d'ouverture et de fermeture n'entreront pas en conflit; en d'autres termes, un code comme celui-ci:

let a = '"\'"'; 

seront correctement mis en évidence, à savoir, ces cas ont causé le plus de difficultés.

Pour commencer


Nous déterminons les valeurs possibles de la variable d'état, ainsi que la couleur dans laquelle telle ou telle partie du code sera colorée, ainsi qu'une liste de mots-clés Javascript (qui seront également mis en évidence):

états const = {...
 const states = { NONE : 0, SINGLE_QUOTE : 1, // 'string' DOUBLE_QUOTE : 2, // "string" ML_QUOTE : 3, // `string` REGEX_LITERAL : 4, // /regex/ SL_COMMENT : 5, // // single line comment ML_COMMENT : 6, // /* multiline comment */ NUMBER_LITERAL : 7, // 123 KEYWORD : 8 // function, var etc. }; const colors = { NONE : '#000', SINGLE_QUOTE : '#aaa', // 'string' DOUBLE_QUOTE : '#aaa', // "string" ML_QUOTE : '#aaa', // `string` REGEX_LITERAL : '#707', // /regex/ SL_COMMENT : '#0a0', // // single line comment ML_COMMENT : '#0a0', // /* multiline comment */ NUMBER_LITERAL : '#a00', // 123 KEYWORD : '#00a', // function, var etc. OPERATOR : '#07f' // null, true etc. }; const keywords = 'async|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|of|package|private|protected|public|return|set|static|super|switch|throw|try|typeof|var|void|while|with|yield|catch|finally'.split('|'); 


Ensuite, nous créons une fonction qui prendra une ligne avec le code et retournera le code HTML fini avec le code en surbrillance. Pour la mise en évidence, les caractères seront enveloppés dans SPAN avec la couleur spécifiée dans la variable de couleurs .

La fonction n'aura qu'un seul cycle, qui analyse chaque caractère et ajoute des SPAN d'ouverture / fermeture si nécessaire.

 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]; //     } return output; } 

Tout d'abord, mettez en surbrillance les commentaires: monoligne et multiligne. Si le caractère actuel et suivant est une barre oblique, et qu'ils ne sont pas à l'intérieur de la ligne (l' état est 0, c'est-à-dire, states.NONE ), alors c'est le début du commentaire. Changez d' état et ouvrez SPAN avec la couleur souhaitée:

 if (state == states.NONE && char == '/' && next == '/') { state = states.SL_COMMENT; output += '<span style="color: ' + colors.SL_COMMENT + '">' + char; continue; } 

continue est nécessaire pour que les vérifications suivantes ne fonctionnent pas et qu'aucun conflit ne se produise.

Ensuite, nous attendons la fin de la ligne: si le caractère actuel est un saut de ligne et dans l' état un commentaire sur une seule ligne, fermez le SPAN et changez l' état à zéro:

 if (state == states.SL_COMMENT && char == '\n') { state = states.NONE; output += char + '</span>'; continue; } 

De même, nous recherchons des commentaires sur plusieurs lignes, l'algorithme est exactement le même, seuls les caractères que vous recherchez sont différents:

 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; } 

La mise en surbrillance des chaînes se produit de la même manière, seulement il doit être pris en compte que le guillemet fermant peut être échappé avec une barre oblique inverse, et ainsi, il cesse déjà d'être une barre oblique de fermeture.

 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; } 

Le code est similaire à ce qui était déjà ci-dessus, seulement maintenant nous n'enregistrons pas la fin de la ligne s'il y avait une barre oblique inverse avant le devis.

La définition des chaînes entre guillemets doubles se produit exactement de la même manière et il est peu logique de les analyser en détail. Pour compléter l'image, je vais les placer sous le 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; } 


Les littéraux d'expression régulière, qui sont facilement confondus avec le signe de division, méritent une considération séparée. Nous reviendrons sur ce problème à la fin de l'article, mais pour l'instant nous faisons la même chose avec les regexps qu'avec les chaînes.

 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; } 

Cela met fin à des cas simples lorsque le début et la fin d'un littéral peuvent être déterminés par 1 à 2 caractères. Commençons par mettre en évidence les nombres: comme vous le savez, ils commencent toujours par un nombre, mais peuvent avoir des lettres dans la composition ( 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>' } 

Ici, nous recherchons le début d'un nombre: le caractère précédent ne doit pas être un chiffre ou une lettre, sinon les chiffres dans les noms de variables seront mis en évidence. Dès que le caractère actuel n'est pas un nombre ou une lettre pouvant être contenu dans le littéral d'un nombre, fermez le SPAN et définissez l' état sur zéro.

Tous les types de littéraux possibles sont mis en évidence, la recherche de mots clés reste. Pour ce faire, vous avez besoin d'une boucle imbriquée qui regarde vers l'avenir et détermine si le caractère actuel est le début du mot-clé.

 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 + '">'; } } 

Ici, nous regardons, le caractère précédent ne peut pas être dans le nom de la variable, sinon laissez comme le mot-clé sera mis en évidence dans le mot sortie . Ensuite, la boucle imbriquée recueille le mot le plus long possible jusqu'à ce qu'un caractère non alphabétique soit rencontré. Si le mot reçu se trouve dans le tableau des mots clés , ouvrez l'ENVERGURE et commencez à mettre le mot en surbrillance. Dès qu'un caractère non alphabétique est rencontré, cela signifie la fin du mot - en conséquence, fermez le SPAN:

 if (state == states.KEYWORD && !/[az]/i.test(char)) { state = states.NONE; output += '</span>'; } 

La chose la plus simple reste - la mise en évidence des opérateurs, ici vous pouvez simplement comparer avec le jeu de caractères qui peut se produire dans les opérateurs:

 if (state == states.NONE && '+-/*=&|%!<>?:'.indexOf(char) != -1) { output += '<span style="color: ' + colors.OPERATOR + '">' + char + '</span>'; continue; } 

À la fin de la boucle, si aucune des conditions qui continuent, la cause n'est déclenchée, nous ajoutons simplement le caractère actuel à la variable résultante. Lorsque le début ou la fin d'un littéral ou d'un mot clé se produit, nous ouvrons / fermons le SPAN avec la couleur; dans tous les autres cas - par exemple, lorsque la ligne est déjà ouverte, nous ne jetons qu'un caractère à la fois. Il convient également de protéger les supports d'angle d'ouverture, sinon ils peuvent casser la disposition.

 output += char.replace('<', '&' + 'lt;'); //  +      < 

Correction d'un bug


Tout semblait en quelque sorte trop simple, et pas en vain: avec des tests plus approfondis, il y avait des cas où le rétro-éclairage ne fonctionnait pas correctement.

La division est reconnue comme expression rationnelle, afin de distinguer l'une de l'autre, il sera nécessaire de changer la façon dont l'expression rationnelle est déterminée. Nous déclarons la variable isRegex = true , après quoi nous essaierons de «prouver» qu'il ne s'agit pas d'une expression rationnelle, mais d'un signe de division. Il ne peut pas y avoir de mots clés ou de crochets ouvrants avant l'opération de division - par conséquent, nous créons une boucle imbriquée et voyons à quoi la barre oblique fait face.

Comme avant
 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--; //        if ('+/-*=|&<>%,({[?:;'.indexOf(code[i+j]) != -1) break; //   ;   -   if (!/[0-9a-z$_]/i.test(code[i+j]) && word.length > 0) break; //  ,     if (/[0-9a-z$_]/i.test(code[i+j])) word = code[i+j] + word; //   - ,     if (')]}'.indexOf(code[i+j]) != -1) { isRegex = false; break; } } //      -    //  : return /test/g - , plainWord /test/g -  if (word.length > 0 && !keywords.includes(word)) isRegex = false; if (isRegex) { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; } } 

Bien que cette approche résout le problème, elle n'est toujours pas sans défauts. Vous pouvez l'ajuster pour que cet algorithme soit également mis en surbrillance de manière incorrecte, par exemple: si (a) / regex / ou alors: 1 / / regex / / 2 . Pourquoi une personne qui divise des nombres en expression régulière a-t-elle besoin de mettre en évidence le code - c'est une autre question; la conception est syntaxiquement correcte, bien qu'elle ne se produise pas dans la vraie vie.

Il y a des problèmes avec la coloration regexp dans de nombreux travaux, par exemple dans prism.js . Apparemment, pour la mise en évidence correcte des expressions rationnelles, vous devez bien comprendre la syntaxe, comme le font les navigateurs.

Le deuxième bug que j'ai dû gérer était lié aux barres obliques inverses. Un guillemet de fermeture n'a pas été reconnu dans une chaîne de la forme «test \\» en raison de la présence d'une barre oblique inverse devant. Retour à la condition qui rattrape la fin de la ligne:

 if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\') 

La dernière partie de la condition doit être modifiée: si la barre oblique inversée est échappée (c'est-à-dire qu'il y a une autre barre oblique inverse avant), enregistrez la fin de la ligne.

 const closingCharNotEscaped = prev != '\\' || prev == '\\' && code[i-2] == '\\'; // ... if (state == states.SINGLE_QUOTE && char == '\'' && closingCharNotEscaped) 

Les mêmes remplacements doivent être effectués dans la recherche de chaînes avec des guillemets doubles et inverses, ainsi que dans la recherche d'expressions rationnelles.

C'est tout, vous pouvez tester le surlignage par le lien au début de l'article.

Source: https://habr.com/ru/post/fr482884/


All Articles