2012年11月26日月曜日

コードをハイライトする

C#のソースコードを、Visual Studioのエディタのように色付けしてHTMLで表示したいときがあります。
ソースコードを開くと、正規表現を使って字句解析し、C#の言語仕様で定義されたキーワードとリテラル文字列に色付けし、変換したHTMLをテキストボックスに出力します。
簡素化のため、ここでは次のルールに従ってソースコードが記述されているものとします。

  • トークンは、区切り文字列、リテラル文字列、識別子のいずれか。
  • 区切り文字列は、区切り記号、演算子、キーワード、改行、空白のいずれか。
  • リテラル文字列は、「"」または「'」で囲まれた文字列で、文字列中の「"」「'」は「\」でエスケープされている。
  • 区切り文字列でもリテラル文字列でもないものは識別子。

リテラル文字列の色は「#990000」、キーワードの色は「blue」にしています。

このアプリケーション自身のコード(MainWindow.xaml.cs)を開いてみる


生成されたHTMLをブラウザで開いてみる

MainWindow.xaml

<Window x:Class="CsHighlighter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.CommandBindings>
        <CommandBinding Command="Open" Executed="OpenCommandHandler"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Menu Grid.Row="0">
            <MenuItem Command="Open" />
        </Menu>
        <TextBox Grid.Row="1" Name="textBox1"
                 AcceptsReturn="True"
                 ScrollViewer.VerticalScrollBarVisibility="Auto"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto" />
    </Grid>
</Window>

MainWindow.xaml.cs

using Microsoft.Win32;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Windows;
using System.Windows.Input;

namespace CsHighlighter
{
    public partial class MainWindow Window
    {
        private static readonly string[] keywords = {
            "abstract","as","base","bool","break","byte","case","catch"
            ,"char","checked","class","const","continue","decimal","default"
            ,"delegate","do","double","else","enum","event","explicit"
            ,"extern","false","finally","fixed","float","for","foreach"
            ,"goto","if","implicit","in","int","interface","internal"
            ,"is","lock","long","namespace","new","null","object","operator"
            ,"out","override","params","private","protected","public"
            ,"readonly","ref","return","sbyte","sealed","short","sizeof"
            ,"stackalloc","static","string","struct","switch","this"
            ,"throw","true","try","typeof","uint","ulong","unchecked"
            ,"unsafe","ushort","using","virtual","void","volatile","while"};

        private static readonly string[] marks = {
            "<<=",">>=","??","::","++","--","&&","||","->","==","!=","<="
            ,">=","+=","-=","*=","/=","%=","&=","|=","^=","<<",">>","=>",
            "{","}","[","]","(",")",".",",",":",";","+","-","*","/","%","&"
            ,"|","^","!","~","=","<",">","?"};

        private static readonly string[] markChars = {
            "{","}","[","]","(",")",".",",",":",";","+","-","*","/","%","&"
            ,"|","^","!","~","=","<",">","?"};

        public MainWindow()
        {
            InitializeComponent();
        }

        private void OpenCommandHandler(object sender, ExecutedRoutedEventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() == true)
            {
                string content;
                using (FileStream fs = new FileStream(ofd.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                using (StreamReader sr = new StreamReader(fs))
                {
                    content = sr.ReadToEnd();
                }

                List<string> tokens = GetTokens(content);
                string htmlCode = GetHtmlCode(tokens);
                this.textBox1.Text = "<!DOCTYPE html><html><head><title></title></head><body>" + htmlCode + "</body></html>";
            }
        }

        private static string GetHtmlCode(List<string> tokens)
        {
            HashSet<string> keywordSet = new HashSet<string>(keywords);
            string htmlCode = "";
            foreach (string token in tokens)
            {
                if (token == "\r\n" || token == "\n")
                {
                    htmlCode += "<br/>";
                }
                else if (keywordSet.Contains(token))
                {
                    htmlCode += "<span style=\"color:blue;\">" + token + "</span>";
                }
                else
                {
                    string tempHtmlCode = HttpUtility.HtmlEncode(token);
                    tempHtmlCode = tempHtmlCode.Replace(" ""&nbsp;");
                    if (token.StartsWith("\"") || token.StartsWith("'"))
                    {
                        tempHtmlCode = "<span style=\"color:#990000;\">" + tempHtmlCode + "</span>";
                    }
                    htmlCode += tempHtmlCode;
                }
            }
            return htmlCode;
        }

        private static List<string> GetTokens(string content)
        {
            string marksRegex = "";
            foreach (string mark in marks)
            {
                foreach (char c in mark)
                {
                    marksRegex += "\\" + c;
                }
                marksRegex += "|";
            }
            string markCharsRegex = "";
            foreach (string c in markChars)
            {
                markCharsRegex += "\\" + c;
            }
            Regex regex = new Regex("(\"(?:[^\"\\\\]|\\\\.)*\")|('(?:[^\'\\\\]|\\\\.)*')|\\r?\\n|\\s+|" +
                marksRegex + "[^\\s\"\'\\r\\n" + markCharsRegex + "]+");
            List<string> tokens = new List<string>();
            MatchCollection mc = regex.Matches(content);
            foreach (Match in mc)
            {
                tokens.Add(m.Value);
            }
            return tokens;
        }
    }
}

0 件のコメント:

コメントを投稿