ToBeDone
Każdy prowadzący technicznego bloga wcześniej czy później napotyka problem: jak przedstawić swój kod na stronie? Do wyboru mamy teoretycznie wiele możliwości. Jest popularny formatter Manoli, jest Actipro CodeHighlighter, o jeszcze innym rozwiązaniu pisał ostatnio Jacek Ciereszko. Gdy jednak rozpoczynalem swoją przygodę z blogowaniem ponad pół roku temu, nie mogłem znaleźć rozwiązania satysfakcjonującego mnie w 100%. Czego oczekiwałem? Pełnej kontroli nad otrzymanym kodem, łatwości dostosowania do własnych potrzeb oraz… możliwości momentalnego wprowadzenia poprawek, gdyby okazało się że coś nie jest tak jak powinno. I tak powstał Procent.Samples.CodeFormatter:). Chwilę po założeniu bloga powstała wersje “pre-alfa”(?) służąca mi aż do niedawna. Ostatnio postanowiłem doprowadzić ją do ładniejszego stanu i zaprezentować tutaj (po zakończeniu tych czynności kod robi więcej niż robił, a jest go 2x mniej! refactoring rulz). Zanim jednak przejdziemy do technicznych rozważań – ONLINE DEMO prezentujące główną funkcjonalność (czyli “paste -> format -> use”) znajduje się pod adresem http://www.maciejaniserowicz.com/samples/CodeFormatter.aspx. CzkItOłt.
Rzut oka z lotu ptoka – UML
Na początek przedstawienie koncepcji – jak widzę problem od strony “techniczno-obiektowej”. Całość procesu to nic innego jak odpowiednie przetwarzanie otrzymanego z zewnątrz tekstu: krok po kroku, reguła po regule. Każdy z tych kroków da się logicznie wyodrębnić i opisać, a co za tym idzie – opakować w osobną klasę. Separation of concerns, baby. Kolorowanie słów kluczowych nie powinno teoretycznie mieć wpływu na numerowanie linii, co z kolei ma się nijak do doklejenia notki od autora narzędzia, prawda?
Takie założenie pozwoliło mi traktować modyfikacje przekazanego tekstu jako łańcuch następujących po sobie czynności. Aż pozwolę sobie podkreślić najważniejsze słowo: łańcuch. Co za tym idzie – każda pojedyncza modyfikacja to ogniwo tej struktury mająca za zadanie wykonać robotę i przekazać efekt swojej pracy dalej. Tak oto powstała ChainElement. Banalne – w sam raz na pierwszy na tym blogu diagram klas(y) (sic!)!
Myślimy dalej i wymyślamy: tak naprawdę można rozdzielić wszystkie możliwe czynności na dwie logiczne grupy. Elementy łańcucha z pierwszej z nich operują na tekście jako całości (np. wspomniane doklejenie notki “Generated by…”), a z drugiej grzebią się w zawartości i zdobią odpowiednie fragmenty tekstu kodem CSS (np. “ozdabiacze” słów kluczowych czy komentarzy). O ile nie możemy przewidzieć co mogą robić ogniwa przynależące do pierwszej grupy, o tyle można domniemywać, że cała druga grupa ogranicza się do zdefiniowana odpowiedniego wyrażenia regularnego i tekstu mającego zastąpić wyszukaną wartość. Tak scharakteryzowana funkcjonalność da się zaimplementować w klasie bazowej zawierąjacej całą potrzebną logikę. A więc przedstawiamy CssDecoratingElement na diagramie numer dwa:
Nowa klasa implementuje abstrakcyjną ProcessString, ponieważ wie co ma zrobić z otrzymanym tekstem. Właściwość CssClass pozwala na skonfigurowanie stylu css wykorzystanego przez dany element, a abstrakcyjna właściwość _regexPattern wymusza na klasach dziedziczących zdefiniowanie kryteriów wyszukiwania napisu do zastąpienia.
Po takich przemyśleniach przychodzi czas na zdefiniowanie kolejnych stacji, na których musi się zatrzymać czysty tekst aby wyjechać “w chmurę” jako śliczny, kolorowy HTML. Nazwy klas mówią same za siebie, zatem kolejny diagram (ale dziś ze mnie sztywniak) powinien być wystarczający:
Po zaimplementowaniu takiej struktury można pokusić się o dołożenie kilku bonusów. Jeden z nich to możliwość konfiguracji sposobu, w jaki CSS zostaje umieszczony w wynikowym tekście. Na moje potrzeby całkowicie wystarcza generacja atrybutu “class” z odpowiednią nazwą, resztę i tak mam podefiniowaną w zewnętrznych plikach. Wychodząc do ludzi wypada jednak zaoferować możliwość kolorowania składni “inline”, aby otrzymany wynik był gotów do natychmiastowego użycia. Do tego celu utworzyłem minihierarchię “Inline style providers” wraz z dwoma przykładowymi implementacjami. Pierwsza z nich (CSharpStyleProvider) pobiera na sztywno zdefiniowane style prosto z zasobów aplikacji, a druga (CustomStyleProvider) poprzez dziedziczenie z Dictionary<string, string> pozwala na własne dowolne definicje:
Zauważmy jedną rzecz: taka architektura sprawiła, że mamy omówione prawie całe zagadnienie, a nigdzie nie pojawiła się jeszcze nazwa CSharpCodeFormatter! Przy przedstawionych założeniach każdy konkretny CodeFormatter to nic innego jak zbiór odpowiednich klocków – wybierz czynności jakie chcesz wykonać, zbuduj z nich łańcuch i voila!
Dodatkowo wprowadziłem jeszcze jeden podział w zaimplementowanych elementach łańcucha. Większość z nich jest niezależna od składni C#, więc warto było zaznaczyć możliwość ich bezpośredniego wykorzystania przy implementacji kolejnych języków:
C# diving
Po takim zarysowaniu zagadnienia czas na kilka rzutów oczami prosto w najbardziej interesujące miejsca w kodzie.
Inicjalizacja łańcucha
Krótko: wybieramy elementy, ustawiamy w odpowiedniej kolejności i:
1: protected override void InitializeChain() 2: { 3: Chain = new HtmlEncoder(); 4: Chain.Attach(new DocumentationFormatter()) 5: .Attach(new SingleLineCommentsFormatter()) 6: .Attach(new MultiLineCommentsFormatter()) 7: .Attach(new DirectivesFormatter()) 8: .Attach(new StringsFormatter()) 9: .Attach(new KeywordsFormatter(_keywords)) 10: .Attach(new LinesCounter()) 11: .Attach(new Wrapper()) 12: .Attach(new CopyrightWriter());
Inline CSS vs CSS class
Decyzję o tym jaki kod generować można podjąć na dwóch poziomach: całego formattera oraz poszczególnych elementów łańcucha. Faktyczne wprowadzenie tej decyzji w życie odbywa się w CssDecoratingElement i wygląda tak:
1: protected string GetStyleFragment() 2: { 3: if (InjectInlineCss) 4: return string.Format("style=\"{0}\"", InlineStyleProvider.GetStyle(CssClass)); 5: 6: return string.Format("class=\"{0}\"", CssClass); 7: } 8:
Przed wykorzystaniem elementy muszą zostać odpowiednio skonfigurowane, CSharpFormatter robi to tak:
1: private void PrepareChainForStyleInlining() 2: { 3: foreach (var element in Chain.AsEnumerable().OfType<CssDecoratingElement>()) 4: { 5: element.InjectInlineCss = true; 6: element.InlineStyleProvider = this.InlineStyleProvider; 7: } 8: }
ChainElement.AsEnumerable
Na strukturach takich jak omawiany łańcuch chce się czasem poszaleć przy użyciu LINQ. Poniższa metoda umożliwia to zwracając wszystkie elementy łańcucha od obecnego w głąb:
1: public IEnumerable<ChainElement> AsEnumerable() 2: { 3: yield return this; 4: if (_next == null) 5: yield break; 6: 7: foreach (var element in _next.AsEnumerable()) 8: yield return element; 9: }
Embedded resources
Domyślne style “inline” oraz słowa kluczowe C# (ściągnięte stąd) zapisane są w plikach tekstowych dołączonych do projektu jako “embedded resource”. Dzięki temu podczas pisania aplikacji możemy łatwo edytować ich zawartość, a po kompilacji nic nam nie zaśmieca folderu wynikowego. Oznaczenie pliku jako wewnętrzne zasoby odbywa się w VS następująco:
, natomiast wykorzystanie takiego pliku w kodzie demonstruje poniższy wycinek inicjalizujący kolekcję słów kluczowych w programie:
1: private static IEnumerable<string> InitializeKeywords() 2: { 3: using (Stream resourceStream = Assembly.GetExecutingAssembly() 4: .GetManifestResourceStream(("Procent.Samples.CodeFormatter.Resources.CSharpKeywords.txt"))) 5: { 6: using (StreamReader reader = new StreamReader(resourceStream)) 7: { 8: // reading a stream... 9:
Najciekawszy Regex
Manipulacje tekstem odbywają się przy użyciu wyrażeń regularnych. Nie ma sensu wypisywania tutaj wszystkich, z których skorzystałem, jednak jedno jest warte osobnego przedstawienia. Proces formatowania kodu odbywa się krok po kroku, co oznacza że element formatujący słowa kluczowe otrzyma tekst już częściowo sformatowany, a co za tym idzie – usiany tagami <span>, których nie powinien ruszać. Dodatkowo przyjąłem założenie, że każdy fragment tekstu nie może zostać sformatowany więcej niż raz. Dzięki temu przykładowo “int” będące częścią wcześniej pokolorowanego literału napisowego (bo StringsFormatter jest dołączony do łancucha przed KeywordsFormatter) nie zostanie dodatkowo oznaczone klasą słów kluczowych – i dobrze. Za takie sztuczki odpowiedzialne jest raptem kilka znaków pilnujących, aby odnaleziony tekst nie znajdował się wewnątrz taga XML bądź nie był jego częścią. Efekt ten osiągam doklejając je z przodu właściwego wyrażenia odpowiedzialnego za odnalezienie innych fragmentów tekstu. Szczegółowe zapoznanie się i rozgryzienie tego wyrażenia pozostawiam zainteresowanym jako pracę domową:
1: const string _outsideTagPatternStart = "(?<!<.*?(>.*)?)";
Najciekawszy CSS
Mistrzem CSS nie jestem, więc zamiast się wymądrzać odeślę do źródła tajemnej wiedzy o odpowiednim zawijaniu linii omijającym luki w poszczególnych przeglądarkach. Nie jestem w 100% pewny czy to zawsze działa, ale mimo to link: “Making preformated <pre> text wrap in CSS3, Mozilla, Opera and IE”.
Przykładowy element łańcucha
I na koniec przykład jednego z “kroków” mielenia tekstu: klasa odpowiedzialna za formatowanie dyrektyw kompilatora (czyli linii w których pierwszym “niebiałym” znakiem jest #):
1: public class DirectivesFormatter : CssDecoratingElement 2: { 3: protected override string _defaultCssClass 4: { 5: get { return "csDirective"; } 6: } 7: 8: protected override string _regexPattern 9: { 10: get { return @"^\s*#.*"; } 11: } 12: }
Proste, łatwe i przyjemne – wszystko mamy wyciągnięte do odpowiednich klas bazowych.
To tyle przykładów z kodu. Zachęcam do ściągnięcia źródeł, zapoznania się z nimi “sam na sam” i wyrażenia swojej opinii. Co można zrobić lepiej/inaczej/wydajniej/bardziej interesująco?
Samary
Mechanizmy .NET wykorzystane w praktyce:
- regular expressions
- embedded resources
- słówko kluczowe yield
Możliwości rozwoju:
- implementacja kolejnych języków
- implementacja innego zdobienia nieparzystych linii
- więcej możliwości konfiguracyjnych (np. możliwe do wyłączenia dodawanie numerów linii czy konfigurowanie generowanych nazw klas, tak jak teraz możliwe jest definiowanie własnych styli inline)
- parser CSS dla CSharpStyleProvider – aktualnie style przechowywane w wewnętrznym pliku tekstowym mają postać “nazwaKlasy|wartości”, a można by się pokusić o odczytywanie “full-blown” CSS
- implementacja osobnego kolorowania nieparzystych linii
- i wreszcie, co by było chyba rzeczą najfajniejszą z fajnych – implementacja wtyczki do VS która robi to wszystko sama!
Kod źródłowy…
…zawierający bibliotekę oraz demko WinForms tak jak poprzednimi razy dostępny do ściągnięcia na stronie SAMPLES. Z ciekawości można również zajrzeć do kodu źródłowego wspomnianego wcześniej Manoli formatter. Cel i efekt te same, mimo że już na etapie koncepcyjnych rozważań obraliśmy inne drogi.