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*#.*\n"; } 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.
tak offtopic, IE ucina obrazki i to kiepsko wyglada… zas w Chrome :) naprawde, pokazuej cale obrazki.
Ps.: Ucina Ci obrazki jak wchodza na panel z prawej strony, a w chodzą. Mowa tu tylko i IE.
Tak to ocenę wystawiłem :)
To ja jeszcze dodam, ze jest cos takiego jak:
http://code.google.com/p/syntaxhighlighter/
daje to baardzo fajny efekt oraz mozliwosci. Odrazu copy to itp. Ma tez plugin do dot NET Blog Engine (czy jak to sie tam zwie) jak i to Live Writer.
Gutek
Pewną słabością przedstawionego rozwiązania jest brak mechanizmu umożliwiającego kontekstowe kolorowanie słów kluczowych. Przykład:
string from = “john@example.com”;
Symbol “from” w tym kontekście nie powinien być traktowany jako słowo kluczowe, lecz nazwa zmiennej. W języku C# takich kontekstowych słów kluczowych jest cała masa, np. from (wyrażenia kwerendowe), where (wyrażenia kwerendowe, więzy typów/metod generycznych), get (właściwości), add (zdarzenia) czy value (akcesor set właściwości, zdarzenia). Jeszcze gorzej wygląda to w Visual Basic, gdzie są literały XML lub w plikach ASPX, w których może być przemieszany kod HTML, JS, CSS, XML, C# i znaczniki ASP.NET. Niestety wyrażeniami regularnymi nie jesteśmy w stanie zbyt wiele w tej kwestii zdziałać. Obawiam się, że nie jesteśmy w stanie efektywnie pokolorować takiego kodu bez własnego parsera, który zdecyduje o znaczeniu poszczególnych symboli nie tylko na podstawie ich treści, lecz również kontekstu.
Przy okazji, wyrażenie regularne kolorujące słowa kluczowe powinno sprawdzać, czy słowo nie jest poprzedzone operatorem literalizacji, tj. @. Przykładowo:
bool @bool = true;
Jedynie symbole “bool” i “true” powinny zostać potraktowane jako słowa kluczowe – znak @ powinien “unieszkodliwić” drugi symbol.
@Gutek:
Obrazki to wina IE lub/i CommunityServer w tej starej wersji, a nie moja:). Więc olewam.
A SyntaxHighlighter… tak jak pisałem jest tego cała masa i z żadnym “konkurować” nie zamierzam:). Chciałem przedstawić jak można cos takiego zrobić – i że nie jest to wcale takie trudne. Dodatkowo ze wszystkich rozwiązań na które się natknąłem tylko to przedstawione przez Jacka (link w poście) robi analizę wychodzącą poza proste wyrażenia regularne (to o czym wspomniał Olek). Tak mi się przynajmniej wydaje, bo umie kolorować typy i wywołania metod. Chyba że “metoda” to regex zaczynający się z wielkiej litery i mający kropkę z przodu, ale nie wydaje mi się;).
@Olek:
Oczywiście, z tego zdaję sobie sprawę. Jednak analiza wychodzenie poza regexy to większe rzeźbienie. Tak jak pisałeś – tworzenie własnego parsera dla każdego ze wspieranych języków… Własna wersja System.CodeDom nie do końca mi się uśmiecha:).
A co do @identyfikator to z jednej strony racja – i poprawka zajęłaby sekundę. Z drugiej strony – wymusiłaby przeniesienie “koloratora” słów kluczowych z sekcji Generic do CSharp.
Tak czy siak dzięki za uwagi.
Witam.
Maciej, czy bylaby taka mozliwosc, zebys zamiescil gdzies styl odpowiedzialny za wyglad VS? Domyslam sie, ze formatowanie kodu w VS u Ciebie jest identyczne jak w przykladowych kodach, co wklejasz na bloga. Bylbym bardzo wdzieczny.
Pozdrawiam
@Arek C:
Akurat kod w moim VS wygląda inaczej niż w samplach na stronie:). Wyglądem sampli steruję CSSem tak, zeby byly czytelne na stronie. Chociaż w VS tez mam czarne tlo, jesli chcesz sprobowac to tu jest link: http://www.maciejaniserowicz.com/procent_vssettings.zip. Podpatrzylem prawie identyczny wyglad u Kuby Binkowskiego, gdy prowadzil sesje na wg.net, i bardzo mi sie spodobalo.
Kolorowanie kodu nie jest takie proste, jak to się wydaje niektórym.
proszę spróbować ‘wrzucić’ do swojego formatera ten fragment kodu:
/// <summary>
/// Znajduje w nagłówku RTF tablicę kolorów i wypełnia ją nazwami i wartościami
/// </summary>
/// <returns></returns>
public bool findColorTbl()
{
if (inString == "") return false;
colrTblPoz = inString.IndexOf("\\colortbl ;", 1);
if (colrTblPoz == 0) return false;
fCols X;
X.name = "cf0";
X.val = "#000000";
Cols.Add(X);
int strt = 0;
for (; ; )
{
if (!getCol(strt, ref colrTblPoz) ) break;
strt++;
}
return true;
}
ale ponieważ nie mam innej alternatywy, to być może skorzystam z pańskiego formatera.