Freitag, 10. September 2010

C# Syntax Highlighter für HTML Version 2

Ich habe nun das Programm, mit dem ich die hier gezeigten Codebeispiele aus dem C# Editor in HTML - Form bringe, überarbeitet.
Eine Erklärung, was das Programm genau macht und wie es funktioniert, gibt es im Post mit der alten Version des Prorgamms.
Hier werde ich nur kurz den Quellcode neu posten, der im Grundgerüst gleich geblieben ist und nur an einigen Stellen verändert und verbessert wurde.
Die neuen Funktionen umfassen unter anderem das Syntax - Highlighting von Klassen sowie eine verbesserte Erkennung von Kommentaren (Dokumentationskommentare und mehrzeilige Kommentare werden nun auch erkannt). Meiner Meinung nach sieht jetzt der farblich gekennzeichnete Quellcode genauso aus, wie er es auch in der .Net Entwicklungsumgebung tut (von der Färbung von .Net Klassen einmal abgesehen).
Der Quellcode:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Text.RegularExpressions;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        List<string> Keywords = new List<string>(); // Liste mit .Net Schlüsselwörtern
        StringBuilder Code; // zu konvertierender Code
        List<string> CustomClasses = new List<string>(); // Liste von im Programm vorkommenden Klassen

        // benutzte Farben
        string KeywordColor = "#0000FF"; // Farbe für Schlüsselwörter
        string ClassColor = "#2B91AF"; // Farbe für Klassen
        string LineCommentColor = "#008000"; // Farbe für Zeilenkommentare
        string QuoteColor = "#A31414"; // Farbe für Strings
        string DocCommentColor = "#808080"; // Farbe für Dokumentationskommentare

        private void button1_Click(object sender, EventArgs e)
        {
            FillKeywords(); // Schlüsselwörter einlesen

            Code = new StringBuilder(System.Windows.Forms.Clipboard.GetText()); // zu bearbeitenden Code aus Zwischenablage einfügen

            SearchClasses(); // benutzerdefinierte Klassen suchen

            // Klassennamen werden die Schlüsselwörter behandelt, deswegen Hinzufügen zur Liste der Keywords
            foreach (string s in CustomClasses)
            {
                Keywords.Add(s);
            }

            EscapeCharacters(); // Sonderzeichen escapen

            string[] Lines = SplitInLines(); // Code in Zeilen zerlegen

            Code = new StringBuilder(); // Code leeren

            bool OpenComment = false;

            // Zeilen einzeln einfärben und zu Code hinzufügen
            for (int i = 0; i < Lines.Length; i++)
            {
                Code.Append(ColorLines(Lines[i], ref OpenComment));

                if (i < Lines.Length - 1)
                    Code.Append("<br/>" + Environment.NewLine);
            }

            // doppelte Leerzeichen auch in HTML als Leerzeichen darstellen
            Code = Code.Replace("  ", @"&nbsp;&nbsp;");

            // HTML Code in Zwischenablage schreiben
            System.Windows.Forms.Clipboard.SetText(Code.ToString());

            MessageBox.Show("Konvertierung abgeschlossen. Code ist in Zwischenablage gespeichert.");
        }

        /// <summary>
       /// Keywords einlesen
        /// </summary>
        private void FillKeywords()
        {
            // aus der Datei "Keywords.txt" im Anwendungsverzeichnis Schlüsselwörter einlesen und speichern
            StreamReader sr = new StreamReader(Application.StartupPath + "\\Keywords.txt");
            string Input = "";
            while ((Input = sr.ReadLine()) != null)
            {
                Keywords.Add(Input);
            }
        }

        /// <summary>
       /// Im Quelltext erwähnte Klassen finden.
        /// </summary>
        private void SearchClasses()
        {
            string CodeString = Code.ToString();
            // alle Klassen im Quellcode suchen
            int ClassIndex = CodeString.IndexOf(" class ", 0);
            while (ClassIndex >= 0)
            {
                // Klassenname ist die Zeichenkette, die nach "class " anfängt und bis zum nächsten Leerzeichen oder Zeilenumbruch geht
                int NextSpace = CodeString.IndexOf(" ", ClassIndex + 7);
                int NextBreak = CodeString.IndexOf(Environment.NewLine, ClassIndex + 7);

                if (NextSpace == -1)
                    NextSpace = int.MaxValue;
                if (NextBreak == -1)
                    NextBreak = int.MaxValue;

                int FirstSeperator;
                if (NextSpace < NextBreak)
                    FirstSeperator = NextSpace;
                else
                    FirstSeperator = NextBreak;

                CustomClasses.Add(CodeString.Substring(ClassIndex + 7, FirstSeperator - (ClassIndex + 7)));
                ClassIndex = CodeString.IndexOf(" class ", ClassIndex + 1);
            }
        }

        /// <summary>
       /// Sonderzeichen escapen
        /// </summary>
        private void EscapeCharacters()
        {
            // & auch escapen, da sonst HTML - Escapezeichen angezeigt werden
            Code = Code.Replace("&", "&amp;");

            // Größer / Kleiner Zeichen escapen, denn schlimmstenfalls werden diese als HTML Tags verstanden
            Code = Code.Replace("<", "&lt;");
            Code = Code.Replace(">", "&gt;");
        }

        /// <summary>
       /// Code in Zeilen aufteilen
        /// </summary>
        private string[] SplitInLines()
        {

            string CodeString = Code.ToString();
            string[] Lines = CodeString.Split(Environment.NewLine.ToCharArray());
            if (Environment.NewLine == "\r\n")
            {   // falls ein Zeilenumbruch durch \r\n dargestellt wird,
                // wird für jedes dieser Zeichen eine neue Zeile erzeugt,
                // also ist jede 2. Zeile überflüssing
                string[] BackupLines = new string[Lines.Length];
                // Zeilen in BackupLines sichern
                for (int i = 0; i < BackupLines.Length; i++)
                {
                    BackupLines[i] = Lines[i];
                }
                // Lines um die Hälfte verkleinern und nur jede 2. aus BackupLines übernehmen
                Lines = new string[BackupLines.Length / 2 + 1];
                for (int i = 0; i < Lines.Length; i++)
                {
                    Lines[i] = BackupLines[i * 2];
                }
            }

            // kleinste Anzahl an Leerzeichen herausfinden, die vor jeder Zeile stehen
            int Counter;
            int MinCountBlanks = int.MaxValue;
            foreach (string Line in Lines)
            {
                Counter = 0;
                // Anzahl an Leerzeichen am Anfang der aktuellen Zeile zählen
                while (Counter < Line.Length && Line.Substring(Counter, 1) == " ")
                    Counter++;
                // kleinste Anzahl speichern
                if (Counter < MinCountBlanks)
                    MinCountBlanks = Counter;
            }

            // diese Anzahl an Leerzeichen vor jeder Zeile löschen,
            // so dass Text linksbündig formattiert wird
            for (int i = 0; i < Lines.Length; i++)
            {
                Lines[i] = Lines[i].Substring(MinCountBlanks, Lines[i].Length - MinCountBlanks);
            }

            return Lines;
        }

        /// <summary>
       /// Übergebene Zeile einfärben.
        /// </summary>
        private string ColorLines(string line, ref bool openComment)
        {

            string Keyword;
            // Positionen von Schlüsselwörtern, Strings, Kommentaren suchen
            int PosKey = LookForKeyword(line, out Keyword);
            int PosQuote = LookForQuote(line);
            int PosCom = LookForComment(line, openComment);

            int PosIndex = 0;

            /* Wenn es einen offenen mehrzeiligen Kommentar gibt und dieser in der aktuellen Zeile
             * noch nicht geschlossen wurde, die ganze Zeile
             * einfärben. */
            if (openComment && !OpenCommentClosed(line))
            {
                return ColorComment(line, PosCom, out PosIndex, true, ref openComment);
            }

            // solange noch Merkmale vorkommen, Schleife weiterführen
            while (PosKey != -1 || PosQuote != -1 || PosCom != -1)
            {
                if (PosKey == -1)
                    PosKey = line.Length;
                if (PosQuote == -1)
                    PosQuote = line.Length;
                if (PosCom == -1)
                    PosCom = line.Length;

                if (openComment) // wenn offener Kommentar und diese Codezeile erreicht ist, wurde er in dieser Zeile geschlossen, zuerst den Kommentar färben
                    line = ColorComment(line, PosCom, out PosIndex, false, ref openComment);
                else
                {
                    if (PosKey < PosQuote)
                    {
                        if (PosKey < PosCom)
                        {
                            // Keyword kommt zuerst vor, einfärben
                            line = ColorKeyword(line, PosKey, Keyword, out PosIndex);
                        }
                        else
                        {
                            // Kommentar kommt zuerst vor, einfärben
                            line = ColorComment(line, PosCom, out PosIndex, false, ref openComment);
                        }
                    }
                    else
                    {
                        if (PosQuote < PosCom)
                        {
                            // Anführungszeichen kommt zuerst vor, String einfärben
                            line = ColorQuote(line, PosQuote, out PosIndex);
                        }
                        else
                        {
                            if (PosCom < line.Length)
                            {
                                // Kommentar kommt zuerst vor, einfärben
                                line = ColorComment(line, PosCom, out PosIndex, false, ref openComment);
                            }
                            else
                            {
                                // ansonsten wurde gar nichts gefunden
                            }
                        }

                    }
                }

                // weiter nach Merkmalen suchen, aber nur ab zuletzt gefundenem Wert und wenn kein Kommentar geöffnet wurde, der nicht geschlossen wurde

                if (PosIndex <= line.Length)
                {
                    PosKey = LookForKeyword(line.Substring(PosIndex), out Keyword);
                    PosQuote = LookForQuote(line.Substring(PosIndex));
                    PosCom = LookForComment(line.Substring(PosIndex), openComment);
                }
                else
                    break;

                // wenn die Merkmale gefunden wurden, Indexe an veränderten Suchraum anpassen
                if (PosKey != -1)
                    PosKey += PosIndex;
                if (PosQuote != -1)
                    PosQuote += PosIndex;
                if (PosCom != -1)
                    PosCom += PosIndex;

            }

            return line;
        }

        /// <summary>
       /// Prüft ob in der übergebenen Zeile ein vorher begonnener mehrzeiliger Kommentar geschlossen wurde.
        /// </summary>
        /// <param name="line">zu prüfende Zeile</param>
        /// <returns>true wenn Kommentar geschlosen</returns>
        private bool OpenCommentClosed(string line)
        {
            int CommentEnd = line.IndexOf(@"*/");
            return (CommentEnd != -1);
        }

        /// <summary>
       ///  Gibt den kleinsten Index eines im übergebenen String befindlichen Schlüsselworts zurück (hierzu zählen auch Klassen).
        /// </summary>
        private int LookForKeyword(string searchString, out string foundKeyword)
        {
            int BestPos = int.MaxValue;

            Regex MatchKeyword;

            int TempIndex;

            foundKeyword = "";

            foreach (string keyword in Keywords)
            {
                // Position aller eingelesenen Keywords im übergebenen String ermitteln
                MatchKeyword = new Regex("\\b" + keyword + "\\b");

                // falls keyword früher als bisher gefundene vorkommt,
                // Index und keyword speichern
                TempIndex = (MatchKeyword.Match(searchString)).Index;
                if (MatchKeyword.Match(searchString).Success && TempIndex < BestPos)
                {
                    BestPos = TempIndex;
                    foundKeyword = keyword;
                }
            }

            if (BestPos == int.MaxValue)
                return -1;
            else
                return BestPos;
        }

        /// <summary>
       /// Gibt die kleinste Position eines nicht escapten Anführungszeichen im übergebenen String zurück.
        /// </summary>
        private int LookForQuote(string searchString)
        {
            bool UnescapedQuoteFound = false;
            int PositionQuote = 0;

            while (!UnescapedQuoteFound && PositionQuote >= 0)
            {
                // Index des nächsten Anführungszeichens ermitteln
                PositionQuote = searchString.IndexOf("\"", PositionQuote);

                // um zu prüfen, ob Anführungszeichen escaped wurde,
                // Anzahl an Backslashes davor prüfen
                int i = 1;
                while (PositionQuote - i >= 0 && searchString.Substring(PositionQuote - i, 1) == "\\")
                    i++;

                // bei einer ungeraden Anzahl von "\" ist das Anführungszeichen escaped
                if (i % 2 == 0)
                {
                    PositionQuote++;
                    continue;
                }
                else
                    UnescapedQuoteFound = true;
            }

            if (UnescapedQuoteFound)
            {
                return PositionQuote;
            }
            else
                return -1;
        }

        /// <summary>
       /// Gibt den ersten Index eines Kommentarzeichens zurück.
        /// </summary>
        private int LookForComment(string searchString, bool openComment)
        {
           
            int LineComment = searchString.IndexOf(@"//"); // Zeilenkommentar
            int MultiLineComment; /* mehrzeiliger Kommentar */
           int DocComment = searchString.IndexOf(@"///"); /// Dokumentationskommentar

            if (openComment)
                MultiLineComment = searchString.IndexOf(@"*/");
            else
                MultiLineComment = searchString.IndexOf(@"/*");

            /* Wenn in vorherigen Zeilen offener Kommentar vorhanden und in dieser Zeile das Ende
            * gefunden wurde, die Zeile direkt bis zum Ende des Kommentars einfärben. */
            if (openComment && MultiLineComment != -1)
                return MultiLineComment;

            // erstes Kommentarzeichen der Zeile suchen
            if (LineComment == -1)
                LineComment = int.MaxValue;
            if (MultiLineComment == -1)
                MultiLineComment = int.MaxValue;
            if (DocComment == -1)
                DocComment = int.MaxValue;

            // wenn kein Kommentar gefunden wurde, abbrechen
            if (LineComment == int.MaxValue && MultiLineComment == int.MaxValue && DocComment == int.MaxValue)
                return -1;

            // erstes gefundenes Kommentarzeichen zurückgeben
            if (LineComment < MultiLineComment)
            {
                if (LineComment < DocComment)
                    return LineComment;
                else
                    return DocComment;
            }
            else
            {
                if (MultiLineComment < DocComment)
                    return MultiLineComment;
                else
                    return DocComment;
            }
        }

        /// <summary>
       /// Färbt das übergebene Schlüsselwort in der übergebenen Zeile ein.
        /// </summary>
        /// <param name="line">zu färbende Zeile</param>
        /// <param name="posKey">Position des Schlüsselworts</param>
        /// <param name="keyword">gefundenes Schlüsselwort</param>
        /// <param name="posIndex">Index, bis zu welchem die Zeile bereits bearbeitet wurde</param>
        /// <returns>bearbeitete Zeile</returns>
        private string ColorKeyword(string line, int posKey, string keyword, out int posIndex) 
        {
            string RightColor = KeywordColor;

            // je nachdem, ob gefundenes Schlüsselwort eine Klasse ist oder nicht, andere Farbe benutzen
            foreach (string s in CustomClasses)
            {
                if (keyword == s)
                {
                    RightColor = ClassColor;
                    break;
                }
            }

            // Ist das gefundene Keyword keine Klasse oder ein Klassenname der nicht im Konstruktor dieser verwendet wird, wird es eingefärbt.
            if (RightColor == KeywordColor || posKey < 14 || line.Substring(posKey - 14, 6) != "public")
            {
                line = line.Substring(0, posKey) + "<span style=\"color:" + RightColor + ";\">" + keyword + "</span>" + line.Substring(posKey + keyword.Length);
                posIndex = posKey + ("<span style=\"color" + RightColor + ";\">" + keyword + "</span>").Length;
            }
            else // Ansonsten (nur bei Konstruktornamen der Fall) nicht einfärben.
                posIndex = posKey + keyword.Length;

            return line;
        }

        /// <summary>
       /// Färbt einen Kommentar in der übergebenen Zeile ein.
        /// </summary>
        /// <param name="line">zu färbende Zeile</param>
        /// <param name="posCom">Position des Kommentars</param>
        /// <param name="posIndex">Index, bis zu welchem die Zeile bereits bearbeitet wurde</param>
        /// <param name="wholeLine">true wenn ganze Zeile eingefärbt wegen soll wegen offenen mehrzeiligem Kommentar</param>
        /// <param name="openComment">ref - Parameter der ggf. angibt, ob der gefundene mehrzeilige Kommentar in dieser Zeile noch nicht geschlossen wurde.</param>
        /// <returns>bearbeitete Zeile</returns>
        private string ColorComment(string line, int posCom, out int posIndex, bool wholeLine, ref bool openComment)
        {
            posIndex = 0;

            // offener mehrzeiliger Kommentar, der sich über die ganze Zeile erstreckt, komplett einfärben
            if (wholeLine)
            {
                line = "<span style=\"color:" + LineCommentColor + ";\">" + line + "</span>";
                posIndex = line.Length;
            }
            else
            {              
                if (line.Substring(posCom, 2) == @"//")
                {
                    if (line.Substring(posCom, 3) == @"///") // Dokumentationskommentar
                    {
                        // Kommentar nach ">" aufsplitten, um XML - Tags in anderer Farbe als den Kommentar einzufärben
                        string[] DocParts = line.Split(new string[] {@"&gt;"}, int.MaxValue, StringSplitOptions.None);
                     
                        if (DocParts.Length == 1)
                        {   // ist kein ">" im Kommentar vorhanden, hat er die Form "/// kommentartext ..."
                            line = "<span style=\"color:" + DocCommentColor + ";\">" + line.Substring(0, 3) + "</span>" + "<span style=\"color:" + LineCommentColor + ";\">" + line.Substring(4) + "</span>";
                            posIndex = line.Length;
                        }
                        else
                        {   // andernfalls hat der Kommentar die Form "/// <tag>[kommentartext</tag>]"
                            for (int i = 0; i < DocParts.Length; i ++)
                            {
                                if (DocParts[i] != "")
                                    DocParts[i] += @"&gt;";
                            }

                            int PosInLine = 0;
                            string ColoredLine = "";
                            for (int i = 0; i < DocParts.Length; i++)
                            {
                                if (DocParts[i] == "")
                                    continue;

                                if (i % 2 == 0)
                                {   // die geraden Teile sind die XML - Tags "<tag>", diese grau einfärben
                                    ColoredLine += "<span style=\"color:" + DocCommentColor + ";\">" + line.Substring(PosInLine, DocParts[i].Length) + "</span>";
                                }
                                else
                                {   // die ungeraden Teile sind die Stellen zwischen den Tags, hier noch einmal aufteilen um den schließenden Tag "</tag>" gesondert zu färben
                                    int XMLTagStart = DocParts[i].IndexOf("&lt;");
                                    ColoredLine += "<span style=\"color:" + LineCommentColor + ";\">" + line.Substring(PosInLine, XMLTagStart) + "</span>" + "<span style=\"color:" + DocCommentColor + ";\">" + line.Substring(PosInLine + XMLTagStart) + "</span>";
                                }
                                PosInLine += DocParts[i].Length;
                            }
                            line = ColoredLine;
                            posIndex = line.Length;
                        }
                    }
                    else // Zeilenkommentar, komplette Zeile einfärben
                    {
                        line = line.Substring(0, posCom) + "<span style=\"color:" + LineCommentColor + ";\">" + line.Substring(posCom) + "</span>";
                        posIndex = line.Length;
                    }
                }

                // mehrzeiliger Kommentar
                if (line.Substring(posCom, 2) == @"/*")
                {
                    int QuoteFinish = line.IndexOf(@"*/", posCom + 2);

                    // kein Ende gefunden, Kommentar geht in nächsten Zeilen weiter
                    if (QuoteFinish == -1)
                    {
                        QuoteFinish = line.Length - 2;
                        openComment = true;
                    }
                    else
                        openComment = false;

                    string LineAfterComment = "";
                    // Zeile nach ggf. Ende des Kommentars speichern
                    if (QuoteFinish + 2 < line.Length)
                    {
                        LineAfterComment = line.Substring(QuoteFinish + 2, line.Length - (QuoteFinish + 2));
                    }

                    // Zeile bis Ende des Kommentars (oder bis Ende der Zeile) grün färben, dann den Rest der Zeile in line schreiben
                    line = line.Substring(0, posCom) + "<span style=\"color:" + LineCommentColor + ";\">" + line.Substring(posCom, QuoteFinish - posCom + 2) + "</span>" + LineAfterComment;

                    posIndex = QuoteFinish + 2 + ("<span style=\"color:" + LineCommentColor + ";\">" + "</span>").Length;
                }

                // offener Kommentar zuende, komplette Zeile bis dahin grün färben
                if (line.Substring(posCom, 2) == @"*/")
                {
                    line = "<span style=\"color:" + LineCommentColor + ";\">" + line.Substring(0, posCom + 2) + "</span>" + line.Substring(posCom + 2, line.Length - (posCom + 2));
                    posIndex = posCom + 2 + ("<span style=\"color:" + LineCommentColor + ";\">" + "</span>").Length; ;
                    openComment = false;
                }
            }

            return line;
        }

        /// <summary>
       /// Zeichenkette einfärben
        /// </summary>
        /// <param name="line">einzufärbende Zeile</param>
        /// <param name="posQuote">Position der Beginn der Zeichenkette</param>
        /// <param name="posIndex">Index, bis zu welchem die Zeile bereits bearbeitet wurde</param>
        /// <returns>bearbeitete Zeile</returns>
        private string ColorQuote(string line, int posQuote, out int posIndex)
        {
            // nächstes nicht escapted Anführungszeichen suchen
            int PosNextQuote = LookForQuote(line.Substring(posQuote + 1));

            if (PosNextQuote == -1)
            {
                if (line.Substring(posQuote - 1, 1) == "@")
                    posQuote--;
                line = line.Substring(0, posQuote) + "<span style=\"color:" + QuoteColor + ";\">" + line.Substring(posQuote) + "</span>";
                posIndex = line.Length;
            }
            else
            {
                PosNextQuote += posQuote + 1;
                if (line.Substring(posQuote - 1, 1) == "@")
                    posQuote--;
                line = line.Substring(0, posQuote) + "<span style=\"color:" + QuoteColor + ";\">" + line.Substring(posQuote, PosNextQuote - posQuote + 1) + "</span>" + line.Substring(PosNextQuote + 1);
                posIndex = posQuote + ("<span style=\"color:" + QuoteColor + ";\">" + line.Substring(posQuote, PosNextQuote - posQuote) + "</span>").Length;
            }

            return line;
        }
    }
}

Kommentare:

  1. Hallo,

    den Inhalt der EscapeCharacters-Methode kann man durch folgende Zeile ersetzen:

    Code = System.Web.HttpUtility.HtmlEncode(Code);

    Somit werden auch alle anderen Sonderzeichen berücksichtigt. Außerdem muss System.Web.dll eingebunden werden (Hinweis: Das Framework muss auf das volle Framework (.NET 4.0) umgestellt werden. Im ClientProfile-Framework fehlt diese Assembly).

    Weiterer Hinweis:

    pascalCase erfolgt nur für Parameter, private Felder, lokale Variablen und lokale Konstanten. Ansonsten wird CamelCase verwendet.

    Viel Glück mit dem Blog!

    AntwortenLöschen
  2. Hi, super cooles Programm.
    Ich hab aber noch ein paar Verbesserungsvorschläge:

    Bei den XML-Kommentaren sind in der zweiten Zeile die drei Schrägstriche auch grün.

    Außerdem werden die Sonderzeichen (ä, ö, ü, ...) falsch angezeigt. Mit der Methode von Anonym :) geht es aber.

    Vielleicht wäre es noch ganz schön, wenn auch die .NET Klassen hellblau wären. Ich weiß nicht, ob das geht, aber vieleicht über die usings?

    Auf jeden Fall super und weiter so!!!!!

    AntwortenLöschen