Lektion für den Abend: Syntax-Hervorhebung schreiben

Kürzlich habe ich mich dafür interessiert, wie die Code-Hervorhebung von innen angeordnet ist. Zuerst schien es, dass dort alles wild kompliziert war - ein Syntaxbaum, eine Rekursion, und das war alles. Bei näherer Betrachtung stellte sich jedoch heraus, dass es hier nichts Schwieriges gibt. Die gesamte Arbeit kann in einem Zyklus mit kurzen Hin- und Herbewegungen ausgeführt werden. Außerdem werden reguläre Ausdrücke im resultierenden Skript so gut wie nie verwendet.

Demo-Seite: Javascript Code Highlighter

Hauptidee


Wir deklarieren die Statusvariable, die Informationen darüber speichert, in welchem ​​Teil des Codes wir uns befinden. Wenn beispielsweise state gleich eins ist, bedeutet dies, dass wir uns in einer Zeichenfolge mit einfachen Anführungszeichen befinden. Das Skript wartet auf das abschließende Zitat und ignoriert alles andere. Dasselbe gilt für das Hervorheben von Kommentaren, regulären Ausdrücken und anderen Elementen. Jedes Element hat seinen eigenen Statuswert. Unterschiedliche öffnende und schließende Zeichen stehen daher nicht in Konflikt. Mit anderen Worten, ein Code wie dieser:

let a = '"\'"'; 

wird richtig hervorgehoben, nämlich solche Fälle verursachten die meisten Schwierigkeiten.

Erste Schritte


Wir bestimmen die möglichen Werte der Zustandsvariablen, die Farbe, in der dieser oder jener Teil des Codes gezeichnet wird, sowie eine Liste von Javascript-Schlüsselwörtern (die ebenfalls hervorgehoben werden):

const states = {...
 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('|'); 


Als Nächstes erstellen wir eine Funktion, die eine Zeile mit dem Code erstellt und den fertigen HTML-Code mit dem hervorgehobenen Code zurückgibt. Zum Hervorheben werden Zeichen mit der in der Variablen colors angegebenen Farbe in SPAN eingeschlossen.

Die Funktion hat nur einen Zyklus, der jedes Zeichen analysiert und bei Bedarf Öffnungs- / Schließ-SPANs hinzufügt.

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

Markieren Sie zunächst die Kommentare: einzeilig und mehrzeilig. Wenn das aktuelle und das nächste Zeichen ein Schrägstrich sind und sich nicht in der Zeile befinden ( state ist 0, d. H. States.NONE ), ist dies der Anfang des Kommentars. Ändere den Zustand und öffne SPAN mit der gewünschten Farbe:

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

continue ist erforderlich, damit die folgenden Überprüfungen nicht funktionieren und kein Konflikt auftritt.

Als nächstes warten wir auf das Ende der Zeile: Wenn das aktuelle Zeichen ein Zeilenumbruch und im Status ein einzeiliger Kommentar ist, schließen Sie das SPAN und ändern Sie den Status auf Null:

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

Ebenso suchen wir nach mehrzeiligen Kommentaren, der Algorithmus ist genau der gleiche, nur die Zeichen, die Sie suchen, sind unterschiedlich:

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

Das Hervorheben von Zeichenfolgen erfolgt auf ähnliche Weise, nur muss berücksichtigt werden, dass das schließende Anführungszeichen mit einem Backslash maskiert werden kann und somit bereits kein schließender Schrägstrich mehr ist.

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

Der Code ist ähnlich wie oben, nur dass wir das Ende der Zeile nicht registrieren, wenn vor dem Anführungszeichen ein Backslash steht.

Die Definition von Strings in doppelten Anführungszeichen erfolgt genauso und es macht wenig Sinn, sie im Detail zu analysieren. Um das Bild zu vervollständigen, lege ich sie unter den 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; } 


Regexp-Literale, die leicht mit dem Divisionszeichen verwechselt werden können, sind gesondert zu betrachten. Wir werden am Ende des Artikels auf dieses Problem zurückkommen, aber im Moment machen wir dasselbe mit regulären Ausdrücken wie mit Zeichenfolgen.

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

Dies beendet einfache Fälle, in denen der Anfang und das Ende eines Literal durch 1-2 Zeichen bestimmt werden können. Beginnen wir mit der Hervorhebung von Zahlen: Wie Sie wissen, beginnen sie immer mit einer Zahl, können jedoch Buchstaben in der Komposition enthalten ( 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>' } 

Hier suchen wir nach dem Anfang einer Zahl: Das vorherige Zeichen darf keine Zahl oder kein Buchstabe sein, da sonst die Zahlen in den Variablennamen hervorgehoben werden. Sobald das aktuelle Zeichen keine Zahl oder kein Buchstabe ist, die im Literal einer Zahl enthalten sein können, schließen Sie das SPAN und setzen Sie den Status auf Null.

Alle möglichen Literaltypen sind markiert, die Suche nach Stichwörtern bleibt erhalten. Dazu benötigen Sie eine verschachtelte Schleife, die nach vorn schaut und bestimmt, ob das aktuelle Zeichen der Anfang des Schlüsselworts ist.

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

Hier sehen wir, das vorherige Zeichen darf nicht im Variablennamen enthalten sein, da sonst das Schlüsselwort im Wort outlet hervorgehoben wird . Dann sammelt die verschachtelte Schleife das längste Wort, das möglich ist, bis ein nicht-alphabetisches Zeichen angetroffen wird. Befindet sich das empfangene Wort im Keyword- Array, öffnen Sie das SPAN und markieren Sie das Wort. Sobald ein nicht-alphabetisches Zeichen angetroffen wird, bedeutet dies das Ende des Wortes - schließen Sie dementsprechend das SPAN:

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

Das Einfachste bleibt, die Hervorhebung von Operatoren. Hier können Sie einfach die Zeichen vergleichen, die in Operatoren vorkommen können:

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

Wenn am Ende der Schleife keine der Bedingungen ausgelöst wird, die andauern , addieren wir einfach das aktuelle Zeichen zur resultierenden Variablen. Wenn ein Literal oder ein Schlüsselwort beginnt oder endet, öffnen / schließen wir das SPAN mit Farbe. In allen anderen Fällen - zum Beispiel, wenn die Zeile bereits offen ist - werfen wir jeweils nur ein Zeichen nach dem anderen. Es ist auch empfehlenswert, die Öffnungswinkel abzuschirmen, da sie sonst das Layout beschädigen können.

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

Fehlerbehebung


Alles schien irgendwie zu einfach und nicht umsonst: Bei gründlicheren Tests gab es Fälle, in denen die Hintergrundbeleuchtung nicht richtig funktionierte.

Die Division wird als regulärer Ausdruck erkannt. Um einen Unterschied zwischen den beiden zu machen, muss die Art und Weise geändert werden, in der der reguläre Ausdruck bestimmt wird. Wir deklarieren die Variable isRegex = true und werden danach versuchen zu "beweisen", dass dies kein regulärer Ausdruck ist, sondern ein Divisionszeichen. Vor dem Divisionsvorgang dürfen keine Schlüsselwörter oder öffnenden Klammern vorhanden sein. Daher erstellen wir eine verschachtelte Schleife und sehen, worauf der Schrägstrich zeigt.

Wie es vorher war
 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; } } 

Obwohl dieser Ansatz das Problem löst, ist er immer noch nicht fehlerfrei. Sie können es so einstellen, dass dieser Algorithmus auch falsch hervorhebt, zum Beispiel: if (a) / regex / oder so: 1 / / regex / / 2 . Warum braucht eine Person, die Zahlen in reguläre Ausdrücke unterteilt, eine Code-Hervorhebung - dies ist eine andere Frage; Das Design ist syntaktisch korrekt, obwohl es im wirklichen Leben nicht vorkommt.

Bei vielen Arbeiten, beispielsweise in prism.js, treten Probleme mit der Regexp-Färbung auf. Um reguläre Ausdrücke korrekt hervorheben zu können, müssen Sie die Syntax genau verstehen, so wie es die Browser tun.

Der zweite Fehler, mit dem ich mich befassen musste, betraf Backslashes. Ein schließendes Anführungszeichen wurde in einer Zeichenfolge der Form 'test \\' aufgrund eines umgekehrten Schrägstrichs nicht erkannt. Zurück zu der Bedingung, die das Ende der Zeile fängt:

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

Der letzte Teil der Bedingung muss geändert werden: Wenn der umgekehrte Schrägstrich maskiert ist (d. H. Ein weiterer Schrägstrich davor steht), registrieren Sie das Ende der Zeile.

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

Dieselben Ersetzungen müssen bei der Suche nach Zeichenfolgen mit doppelten und umgekehrten Anführungszeichen sowie bei der Suche nach regulären Ausdrücken vorgenommen werden.

Das ist alles, Sie können das Highlight über den Link am Anfang des Artikels testen.

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


All Articles