Montag, 14. Juni 2010

C# Syntax Highlighter für HTML

Ohne Syntax Highlighting, das heißt ohne farbliche Hervorhebung des Codes, sähen längere Codebeispiele ziemlich unübersichtlich aus.
In diesem Blog versuche ich, den Code genau wie in der .Net Entwicklungsumgebung aussehen zu lassen.
Das Programm, welches Schlüsselwörter, Kommentare u.ä. einfärbt, habe ich selber geschrieben mit, wie sollte es auch anders sein, C#.
Für alle Interessierte poste ich hier den Quellcode.
Ich mache hierbei keinen Anspruch auf Vollständigkeit und Korrektheit des Programms, wahrscheinlich gibt es noch einige Bugs etc., für Hinweise und Verbesserungsvorschläge wäre ich dankbar!
Den Code werde ich auch nicht beschreiben, denn er ist ziemlich umfangreich.
Ich möchte einfach für alle Interessierte ein Beispiel geben, wie so ein Projekt verwirklicht werden kann und wer möchte, darf sich natürlich den Code auch einfach ziehen und selber für seine Projekte verwenden.
Nun nur noch kurz eine kleine Beschreibung des Programms:
Ihr müsst auf dem Formular einfach einen Button anlegen und das Ereignis Click() mit button1_Click_1 verknüpfen.
Das Programm liest beim Start eine Liste mit allen möglichen Keywords der .Net Entwicklungsumgebung aus der Datei "Keywords.txt", welche sich im gleichen Ordner wie die .exe - Datei befinden muss, ein. Die Datei gibt's hier.
Klickt der Benutzer auf den Button (oder ruft entsprechende Funktion anderweitig auf), wird der C# - Code aus der Zwischenablage in gültigen HTML - Code umgewandelt, so dass das Ergebnis auf einer Homepage, einem Blog o.a. (fast) so aussieht, wie in der Entwicklungsumgebung.

Und hier der Code:


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 bekannten .Net Schlüsselwörtern
        string Code; // zu konvertierender Code

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

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

            EscapeCharacters(); // Sonderzeichen escapen

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

            Code = "";
            for (int i = 0; i < Lines.Length; i++)
            {
                Code += ColorLines(Lines[i]);
                if (i < Lines.Length - 1)
                    Code += 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);

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

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

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

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

            string[] Lines = Code.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;
        }

        private string ColorLines(string line)
        {
            // übergebene Zeile farbig einfärben

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

            int PosIndex = 0;

            // 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 (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);
                    }
                }
                else
                {
                    if (PosQuote < PosCom)
                    {
                        // Anführungszeichen kommt zuerst vor, String einfärben
                        line = ColorQuote(line, PosQuote, out PosIndex);
                    }
                    else
                    {
                        if (PosCom > -1)
                        {
                            // Kommentar kommt zuerst vor, einfärben
                            line = ColorComment(line, PosCom, out PosIndex);
                        }
                        // ansonsten wurde gar nichts gefunden
                    }
                }

                // weiter nach Merkmalen suchen, aber nur ab zuletzt gefundenem Wert
                if (PosIndex <= line.Length)
                {
                    PosKey = LookForKeyword(line.Substring(PosIndex), out Keyword);
                    PosQuote = LookForQuote(line.Substring(PosIndex));
                    PosCom = LookForComment(line.Substring(PosIndex));
                }

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

        private int LookForKeyword(string searchString, out string foundKeyword)
        {
            // gibt den kleinsten Index eines im übergebenen String befindlichen Schlüsselworts zurück

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

        private int LookForQuote(string searchString)
        {
            // gibt die kleinste Position eines nicht escapten Anführungszeichen im übergebenen String zurück

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

        private int LookForComment(string searchString)
        {
            // gibt den ersten Index eines Kommentarzeichens zurück
            return searchString.IndexOf(@"//");
            // /* Kommentar */ wird noch nicht beachtet
        }

        private string ColorKeyword(string line, int posKey, string keyword, out int posIndex)
        {
            line = line.Substring(0, posKey) + "<span style=\"color:#0000ff;\">" + keyword + "</span>" + line.Substring(posKey + keyword.Length);
            posIndex = posKey + ("<span style=\"color:#0000ff;\">" + keyword + "</span>").Length;
            return line;
        }

        private string ColorComment(string line, int posCom, out int posIndex)
        {
            // Zeilenkommentar, komplette Zeile einfärben
            if (line.Substring(posCom, 2) == @"//")
            {
                line = line.Substring(0, posCom) + "<span style=\"color:#008000;\">" + line.Substring(posCom) + "</span>";
                posIndex = line.Length;
            }
            else
                posIndex = 0; // anderer Kommentar
            return line;
        }

        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:#A31414;\">" + 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:#A31414;\">" + line.Substring(posQuote, PosNextQuote - posQuote + 1) + "</span>" + line.Substring(PosNextQuote + 1);
                posIndex = posQuote + ("<span style=\"color:#A31414;\">" + line.Substring(posQuote, PosNextQuote - posQuote) + "</span>").Length;
            }

            return line;
        }
    }
}

Kommentare:

  1. Hallo, wie du selbst an deinem Post sehen kannst, ist das Highlighting nicht ganz vollständig. "partial" sollte auch ein Keyword sein.

    Zudem wäre ein Feature aus der IDE auch sehr nett und zwar, dass Klassennamen automatisch als Typ erkannt werden und auch farblich hervorgehoben werden.

    AntwortenLöschen
  2. Hallo,
    ja nach einiger Zeit waren echt mal ein paar Verbesserungen fällig, die neue Version in der unter anderem die von dir bemängelten Fehler behoben sind gibt's jetzt unter http://csharp-tricks.blogspot.com/2010/09/c-syntax-highlighter-fur-html-version-2.html .

    AntwortenLöschen