Blog (9)
Komentarze (73)
Recenzje (0)
@pat.wasiewiczC# - kolorowanie składni

C# - kolorowanie składni

15.07.2013 23:34

Wstęp

Napiszmy bibliotekę, która będzie kolorowała nam składnie jakiegoś języka i zwracała widok jako html. Założenia

  • Chcemy umożliwić definiowanie własnych schematów kolorowania
  • Chcemy umożliwić zmienianie skórki

Jako, że nie ma być to zaawansowany system (prościutki raczej) oprzemy jego silę na wyrażeniach reguralnych. Algorytm będzie wyglądał mniej-więcej tak:

  • Załadujemy z definicji języka wszystkie wyrażenia regularne - każdy będzie miał swój priorytet.
  • Poszukamy w tekście wzorców
  • Przefiltrujemy listę wzorców usuwając te z mniejszym priorytetem
  • Zbudujemy html-a

Zaczynamy

Na początek garść wyjątków

public class NoLanguageProvidedException : Exception
    {
        public NoLanguageProvidedException()
            : base("No language provided.")
        {
        }
    }
    public class NoStyleColorProvidedException : Exception
    {
        public NoStyleColorProvidedException()
            : base("No color provided within theme style.")
        {
        }
    }
public class NoThemeProvidedException : Exception
    {
        public NoThemeProvidedException()
            : base("No theme provided.")
        {
        }
    }
public class NoThemeStyleProvidedException : Exception
    {
        public NoThemeStyleProvidedException()
            : base("No theme style provided (style is null)")
        {
        }
    }

Priorytety będziemy ustawiać za pomocą następującego enum-a, który jednocześnie będzie nam służył do rozpoznawania priorytetu kodu oraz stylu na jaki mamy pokolorwać:

    public enum TokenScope
    {
        String = 0x1,
        Comment = 0x2,
        Preprocessor = 0x4,
        Keyword = 0x8
    }

Przygotujmy teraz część odpowiedzialną za skórkę. Podstawową komórką będzie nastepująca klasa:

    public class Style
    {
        public string HexColor { get; set; }

        public bool Bold { get; set; }

        public bool Italic { get; set; }
    }

Bedzie ona reprezentowała styl jakiegoś kawałka kodu. Jakiego? To już zawrzemy w interfejsie:

    public interface ITheme
    {
        string BaseHexColor { get; }

        string BackgroundHexColor { get; }

        Style GetStyle(TokenScope scope);
    }

Mając taki zestaw, możemy przygotować sobie jakieś theme:

public class ObsidianTheme : ITheme
    {
        public string BaseHexColor
        {
            get
            {
                return "#F1F2F3";
            }
        }

        public string BackgroundHexColor
        {
            get
            {
                return "#111";
            }
        }

        public Style GetStyle(TokenScope scope)
        {
            switch (scope)
            {
                case TokenScope.Comment:
                    return this.CommentStyle;
                case TokenScope.Keyword:
                    return this.KeywordStyle;
                case TokenScope.Preprocessor:
                    return this.PreprocessorStyle;
                case TokenScope.String:
                    return this.StringStyle;
                default:
                    return this.KeywordStyle;
            }
        }

        private Style KeywordStyle
        {
            get
            {
                return new Style
                           {
                               Bold = false,
                               HexColor = "#93C763",
                               Italic = false
                           };
            }
        }

        private Style StringStyle
        {
            get
            {
                return new Style
                           {
                               Bold = false,
                               HexColor = "#EC7600",
                               Italic = false
                           };
            }
        }

        private Style PreprocessorStyle
        {
            get
            {
                return new Style
                {
                    Bold = false,
                    HexColor = "#003399",
                    Italic = true
                };
            }
        }

        private Style CommentStyle
        {
            get
            {
                return new Style
                {
                    Bold = false,
                    HexColor = "#888888",
                    Italic = true
                };
            }
        }
    }

Zajmijmy się teraz jęzkami, tj składnią danych języków. Każdy będzie składał się z reguł: wyrażenia regularnego oraz priorytetu aka typu:

    public class Rule
    {
        public string RegularExpression { get; set; }

        public TokenScope Scope { get; set; } 
    }

Język więc będzie zbiorem takich reguł:

    public interface ILanguage
    {
        IEnumerable<Rule> GetRules();
    }

No i przykładowy język

public class Csharp : ILanguage
    {
        public IEnumerable<Rule> GetRules()
        {
            return new[]
                       {
                           new Rule
                               {
                                   RegularExpression =
                                       "\"(?:[^\"\\\\]|\\\\.)*\"",
                                   Scope = TokenScope.String
                               },
                            new Rule
                               {
                                   RegularExpression =
                                       @"'\\[a-zA-Z|\\]'",
                                   Scope = TokenScope.String
                               },
                            new Rule
                               {
                                   RegularExpression =
                                       @"'\\[a-zA-Z|\\]'",
                                   Scope = TokenScope.String
                               },
                               new Rule
                               {
                                   RegularExpression =
                                       @"'\\[x|u|U][0-7ABCDEFabcdef]{1,8}'",
                                   Scope = TokenScope.String
                               },
                           new Rule
                               {
                                   RegularExpression = @"//.*|/\*[\s\S]*\*/",
                                   Scope = TokenScope.Comment
                               },
                           new Rule
                               {
                                   RegularExpression =
                                       @"\b(?: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|struct|sbyte|sealed|sizeof|stackalloc|short|static|string|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|var|virtual|volatile|void|while)\b",
                                   Scope = TokenScope.Keyword
                               },
                           new Rule
                               {
                                   RegularExpression = @"^#.*$",
                                   Scope = TokenScope.Keyword
                               }
                       };

        }

Aby łatwiej było się posługwiać regułami przy budowaniu html-a musimy podać je małej obróbce. Po niej, reguła będzie prezentowana przez nastepującą klasę:

    internal class PreProcessedRule
    {
        public string SourcePiece { get; set; }

        public int StartIndex { get; set; }

        public TokenScope Scope { get; set; }
    }

Przygotujmy pomocniczy interfejs:

    internal interface IPreProcessedRules : IEnumerable<PreProcessedRule>
    {
        IEnumerable<PreProcessedRule> GetPreProcessedRules();
    }

Oraz jego implementację, która będzie już na przeprowadzała "preprocessing":

internal class PreProcessedRules : IPreProcessedRules
    {
        private IEnumerable<PreProcessedRule> preProcessedRules;

        internal PreProcessedRules(string sourceCode, IEnumerable<Rule> rules)
        {
            this.preProcessedRules = Enumerable.Empty<PreProcessedRule>();
            this.PreProcessRules(sourceCode, rules);
        }

        public IEnumerable<PreProcessedRule> GetPreProcessedRules()
        {
            return this.preProcessedRules;
        }

        public IEnumerator<PreProcessedRule> GetEnumerator()
        {
            return this.preProcessedRules.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private void PreProcessRules(string sourceCode, IEnumerable<Rule> rules)
        {
            var matches =
                rules.SelectMany(
                    rule =>
                        Regex.Matches(
                            sourceCode,
                            rule.RegularExpression,
                            RegexOptions.Multiline)
                             .Cast<Match>()
                             .Select(
                                 match =>
                                     new
                                         {
                                             match.Index,
                                             match.Value,
                                             rule.Scope
                                         }))
                     .OrderBy(match => match.Index)
                     .ThenBy(match => match.Scope)
                     .ToList();

            for (var j = 0; i < matches.Count; j++)
            {
                if (j + 1 >= matches.Count)
                {
                    continue;
                }

                var currentMatchIndexEnd = matches[j].Index
                                           + matches[j].Value.Length;
                if (currentMatchIndexEnd <= matches[j + 1].Index)
                {
                    continue;
                }

                matches.RemoveAt(j + 1);
                i--;
            }

            this.preProcessedRules =
                matches.Select(
                    match =>
                        new PreProcessedRule
                            {
                                Scope = match.Scope,
                                SourcePiece = match.Value,
                                StartIndex = match.Index
                            })
                       .ToList();
        }
    }

Siłę stanowi metoda PreProcessRules, która:

  • Szuka wszystkich wzorów w całym tekście
  • Sortuje wg. znalezionego indeksu (pozycji pierwszej litery znalezionego wzorca) a następnie po priorytecie
  • Usuwa zbędne wzorce

Interfejs reprezentujacy "przetworzony" język

internal interface IPreProcessedLanguage : ILanguage
    {
        string SourceCode { get; }

        IPreProcessedRules GetPreProcessedRules();
    }

I jego implementacja:

internal class PreProcessedLanguage : IPreProcessedLanguage
    {
        private readonly ILanguage language;

        private readonly IPreProcessedRules preProcessedRules;

        internal PreProcessedLanguage(string sourceCode, ILanguage language)
        {
            this.language = language;
            this.SourceCode = sourceCode;
            this.preProcessedRules = new PreProcessedRules(sourceCode, this.GetRules());
        }

        public IPreProcessedRules GetPreProcessedRules()
        {
            return this.preProcessedRules;
        }

        public IEnumerable<Rule> GetRules()
        {
            return this.language.GetRules();
        }

        public string SourceCode
        {
            get;
            set;
        }
    }

Teraz parser, który mając kod źródłowy, "theme" oraz poddany obróbce język zbuduje nasz html

internal class HtmlParser
    {
        private readonly IPreProcessedLanguage preProcessedLanguage;

        private readonly ITheme theme;

        internal HtmlParser(IPreProcessedLanguage preProcessedLanguage, ITheme theme)
        {
            this.preProcessedLanguage = preProcessedLanguage;
            this.theme = theme;
        }

        public string Parse()
        {
            var builder =
                new StringBuilder(
                    PreprePreOpenTag(
                        this.theme.BaseHexColor,
                        this.theme.BackgroundHexColor));

            builder.Append(this.ParseSourceCode());

            builder.Append("</pre>");
            return builder.ToString();
        }

        private string ParseSourceCode()
        {
            var source = this.preProcessedLanguage.SourceCode;
            var processedText = new StringBuilder();
            var rules =
                this.preProcessedLanguage.GetPreProcessedRules().ToList();

            if (!rules.Any())
            {
                return source;
            }

            processedText.Append(GetTextBegin(source, rules));

            for (var j = 0; j < rules.Count(); j++)
            {
                var rule = rules[j];
                var style = this.theme.GetStyle(rule.Scope);

                FormatSourcePiece(processedText, rule.SourcePiece, style);

                string restOfSource;
                if (j + 1 < rules.Count())
                {
                    var nextRule = rules[j + 1];
                    var restStartIndex = rule.StartIndex
                                         + rule.SourcePiece.Length;

                    restOfSource =
                        source.Substring(
                           restStartIndex,
                            nextRule.StartIndex - restStartIndex);
                }
                else
                {
                    restOfSource =
                        source.Substring(
                            rule.StartIndex + rule.SourcePiece.Length);
                }

                processedText.Append(restOfSource);
            }

            return processedText.ToString();
        }

        private static void FormatSourcePiece(StringBuilder builder, string source, Style style)
        {
            if (style.Bold)
            {
                source = string.Format("<b>{0}</b>", source);
            }

            if (style.Italic)
            {
                source = string.Format("<i>{0}</i>", source);
            }

            builder.AppendFormat(
                "<span style=\"color: {0};\">{1}</span>",
                style.HexColor,
                source);
        }

        private static string GetTextBegin(
            string sourceCode,
            IEnumerable<PreProcessedRule> rules)
        {
            return sourceCode.Substring(0, rules.ElementAt(0).StartIndex);
        }

        private static string PreprePreOpenTag(
            string hexColor,
            string hexBackgroundColor)
        {
            return
                string.Format(
                    "<pre style=\"color: {0}; background-color: {1};\">\n",
                    hexColor,
                    hexBackgroundColor);
        }
    }

Mając to wszystko możemy wyeksponować użytkownikowi interfejs:

    public interface ICodeColorizer
    {
        ICodeColorizer WithLanguage(ILanguage language);

        ICodeColorizer WithTheme(ITheme theme);

        string ToHtml();
    }

wraz z implementacją:


internal class CodeColorizer : ICodeColorizer
    {
        private readonly string sourceCode;

        private ILanguage language;

        private ITheme theme;

        internal CodeColorizer(string sourceCode)
        {
            if (sourceCode == null)
            {
                throw new ArgumentNullException("sourceCode");
            }

            this.sourceCode = sourceCode;
        }

        private ILanguage Language
        {
            get
            {
                if (this.language == null)
                {
                    throw new NoLanguageProvidedException();
                }

                return this.language;
            }

            set
            {
                this.language = value;
            }
        }

        private ITheme Theme
        {
            get
            {
                if (this.theme == null)
                {
                    throw new NoThemeProvidedException();
                }

                VerifyTheme(this.theme);
                return this.theme;
            }

            set
            {
                this.theme = value;
            }
        }

        public ICodeColorizer WithLanguage(ILanguage language)
        {
            this.Language = language;
            return this;
        }

        public ICodeColorizer WithTheme(ITheme theme)
        {
            this.Theme = theme;
            return this;
        }

        public string ToHtml()
        {
            var htmlParser = new HtmlParser(
                this.PreProcessLanguage(),
                this.Theme);

            return htmlParser.Parse();
        }

        private IPreProcessedLanguage PreProcessLanguage()
        {
            return new PreProcessedLanguage(this.sourceCode, this.Language);
        }

        private static void VerifyTheme(ITheme theme)
        {
            var allTokens = GetValues<TokenScope>();
            foreach (var style in allTokens.Select(theme.GetStyle))
            {
                if (style == null)
                {
                    throw new NoThemeStyleProvidedException();
                }

                if (style.HexColor == null)
                {
                    throw new NoStyleColorProvidedException();
                }
            }

            if (theme.BaseHexColor == null
                || theme.BackgroundHexColor == null)
            {
                throw new NoStyleColorProvidedException();
            }
        }

        public static IEnumerable<T> GetValues<T>()
        {
            if (!typeof(T).IsEnum)
                throw new InvalidOperationException("Type must be enumeration type.");

            return GetValuesImplicit<T>();
        }

        private static IEnumerable<T> GetValuesImplicit<T>()
        {
            return from field in typeof(T).GetFields()
                   where field.IsLiteral && !string.IsNullOrEmpty(field.Name)
                   select (T)field.GetValue(null);
        }
    }

I metoda statyczna umożliwiająca stworzenie instnacji CodeColorizer:

    public static class Colorizer
    {
        public static ICodeColorizer Colorize(string sourceCode)
        {
            return new CodeColorizer(sourceCode);
        }
    }

Przykład użycia:

const string SampleCode = "public void Main()\n{\n\tvar a = new MyObject();\n}";
var language = new Csharp();
var theme = new OblivionTheme();

html = Colorizer.Colorize(SampleCode)
                         .WithLanguage(language)
                         .WithTheme(theme).ToHtml();

Testy jednostkowe:

Testy jednostkowe
Testy jednostkowe

Soolucja na codeplex.com

Wybrane dla Ciebie
Komentarze (0)