Wyrażenia regularne w php 4 czesc II. Czyli jak zrobić kolorator składni.

Witam w kolejnej czesci tutoriala nt wyrazen regularnych. Zapewne zdziwili sie Ci z Was, którzy widząc ten tytuł i znając funkcję, highlight_file, dostępna w php wiedzą już jak kolorować składnie bez potrzeby korzystania z wyrażeń regularnych. Otóż już wyjaśniam dlaczego pokazuje to na takim przykładzie.



Moim zdaniem funkcja highlight_file, ma swoje ograniczenia:

1. Koloruje ona tylko składnie php.
2. Sposób kolorowania jest ustalany przez kogoś kto definiuje nasz plik php.ini i jezeli nie mamy dostępu do konfiguracji serwera nie mamy na to żadnego wpływu.



Poza tym kolorowanie składni to jest super sposób na wytłumaczenie zawiłości zaawansowanych wyrażeń regularnych ;))



Zaczniemy od wytłumaczenia różnicy miedzy dwoma zapisami:
.*
.*?

Pewnie niektórzy z Was dziwili sie w poprzedniej części dlaczego po gwiazdce raz następuje znak zapytania a innym razem już nie.

Wytłumaczenie jest dość proste. Domyślnie kropka po której występuje gwiazdka stara sie pobrać z danego ciągu znaków jak największa ich ilość, natomiast jeżeli za gwiazdką znajduje sie jeszcze znak zapytania kropka stara się pobrać minimalną możliwą ilość znaków z danego ciągu.

Pewnie takie teoretyzowanie nic narazie nie wyjaśnia, ale przeanalizujmy jakiś przykład. Jeżeli np chcemy z naszego tekstu wyciąć komentarze w stylu c++ czyli


/* costam costam */

kod

/* costam costam */

kod

/* costam costam */

kod

to stosując pierwszy sposób i zapisując wyrażenie jako:


//uwaga na "uniki" dla gwiazdek, // oraz modyfikator \s - wlaczajacy nowe linie preg_match_all( '/\*.*\*/s', $s, $m )

zostaniemy zaskoczeni przez rezultat gdyż będzie on zawierał ciąg znaków pomiędzy pierwszym otwarciem komentarza a ostatnim zakończeniem, czyli zdecydowanie za dużo.

Korzystając z drugiego sposobu, już wszystko będzie ok.

preg_match_all( '/\*.*?\*/s', $s, $m )


Teraz mamy do dyspozycji to co trzeba, ale jak z tego skorzystać.

No można to wrzucić do funkcji preg_replace (w celu sprawdzenia poprawności usuniemy je najpierw)

preg_replace('%/\*.*?\*/%s','',$kod);

No okazuje sie, że nie mamy już wogóle komentarzy w stylu c++ w naszym kodzie natomiast wszystko pozostałe zostało nietkniete, brawo o to właśnie chodziło !.

Skoro już jesteśmy przy komentarzach zajmijmy sie od razu komentarzami jednolinijkowymi, ja znam 2 wersje stosowane w php. Zaczynające sie od // o raz o # natomiast kończące sie na znaku nowej linii \n. Tutaj sprawa jest prosta jeżeli ktoś zwrócił uwage w poprzedniej częsci na modyfikator \s. Otóż jak pamiętacie przełącznik ten sprawia, że przeszukiwany ciąg znaków jest traktowany jako jedna linia i dzięki temu kropka .* lub .+ przechodzi też do nowych linii. Teraz natomiast wystarczy zrobić coś odwrotnego. Chcemy znależć ciąg zaczynający sie od // lub # aż do \n, więc wymusimy na wyrażeniach regularnych aby traktowało ciąg znaków jako wieloliniowy - służy do tego przełącznik \m (multiline).

oto nasze wyrażenie: //.*

A tutaj wersja do przetestowania - usuwanie ich z kodu:

preg_replace('%//.*/m','',$kod);

Jak widać nasze komentarze zaczynające sie od // zostały usunięte, ale co zrobić z komentarzami zaczynającymi się od #. Trzeba zastosować alternatywę: (?://|#).* (co robi ?: zostawiam dla chętnych)

preg_replace('%(?://|#).*%m','',$kod);

To chyba rozwiązuje sprawę komentarzy. (Jeżeli ktoś chce to może dodać komentarze w stylu VisualaBasica, a zaczynają sie one tam od znaku ', jednak wtedy należy pamiętać o zmianie regułek zastępujących ciągi znaków).

Teraz zajmiemy sie składnią danego języka.

Tutaj sprawa wygląda dość skomplikowanie, dlatego, że odróżnienie funkcji użytkownika od funkcji zdefiniowanych w składni odbywa sie na podstawie porównania ich nazw, wiec najpierw trzeba te nazwy niestety wprowadzić, a to czasem może być dość żmudna i cięzka robota.

No to ja Wam pokaże jak wpisać podstawowe rzeczy a poźniej już sobie poradzicie:



$functions[]='(preg_(match|replace|match)'; $functions[]='image(createTrueColor|copyResampled)';


Następnie trzeba je wszystkie połączyć w jedna regułkę - zrobimy to korzystając z funkcji implode:

$reg=implode('|',$functions);

No i teraz trzeba przetestować to wyrażenie:

//wrzucilem modyfikator "i" // zeby nie zwracał uwagi na wielkosc liter preg_replace('%'.$reg.'%i','',$kod);

No wszystko ładnie pięknie, nasze nazwy funkcji zostają zastąpione, ale co zrobić jeżeli:

1. Korzystamy ze zmiennej nazwanej tak jak funkcja.

2. Nazywamy nasza funkcje bardzo podobnie jak ta zdefiniowana w php tylko dodajac jej słowo 'moja',np. moja_preg_replace

W obu wypadkach słowa te zostaną potraktowane jako nazwy funkcji i zamienione zgodnie ze schematem, a o to nam zupełnie nie chodzi, ale jest rozwiązanie chroniące nasz przed tym. Można poinstruować silnik wyrażeń, żeby odczytywał to słowo od początku do końca, skorzsytamy do tego z operatorów ^ oraz $, mianowicie:

echo preg_replace('%^('.$reg.')$%si','',$kod);

Pamiętacie jak wspominałem o tym w pierwszej części, że aby słowo w których występuje poszukiwany przez nas wyraz nie były brane pod uwagę należy tak postąpić - tutaj macie praktyczny sposób na skorzystanie z tej wiedzy ;)).


W ten sam sposób należy skonstruować regułkę do zastępowania (for,return):

$fs='return|for|break|continue';

I zostaną one zastąpione w ten sam sposób.

Aby dobrze pokolorować dany kod trzeba jeszcze wziąć pod uwagę, że występują w nim także dane typu string, które też możnaby wyróżnić w jakiś szczególny sposób,

Ale dla niektórych z Was którzy przeczytali pierwszą część tego cyklu myśle, że nie powinno to sprawiać problemów

("|')(.*?)("|')

No i sprawa załatwiona stringi będą już wykoszone z naszego tekstu - no to sprawdzamy jak to działa:

preg_replace('%("|\')(.*?)("|\')%s','',$kod);

No i fajnie wszystko dziala....

...czy aby napewno wszystko ?? No to sprawdzcie sobie co sie stanie jeżeli zawrzecie jedne cudzysłowy w drugich np.

$str = '"to jest moj ciag znakow"';

Pięknie, rozpoczyna od pierwszego pojedynczego cudzysłowia, ale kończy na drugim podwójnym - czyli o co chodzi ??

No to idziemy dalej tym tropem, rozbijmy to na 2 wyrażenia, przecież jako argument możemy przekazać tablice:

preg_replace(array('%".*?"%s',"%'.*?'%s"),array('',''),$kod);

No ok pozbyliśmy sie tego co trzeba, czyli wyrażenie już jest dobre i nie powinienm mieć zastrzeżeń - no w końcu działa przecież :P.

Otóż ja Wam powiem, że można prościej korzystając z nawiasów przekształcanych w zmienne:

("|\').*?\\1

No już wyjaśniam co się tutaj dzieje:

W pierwszym nawiasie napotykamy na drodze apostrof pojedynczy lub podwojny i zachowujemy tą wartość do późniejszego wykorzystania. Następnie pobieramy możliwie jak najmniej znaków, aż do czego ? Do tego co już wcześniej znaleźliśmy na początku czyli do pojedyńczego albo podwójnego apostrofa. Noo teraz już zagnieżdżone ciagi znaków nam niestraszne.

No świetnie....

...ale co będzie jak ktoś piszący kod zdecyduje się na korzystanie z jednego typu apostrofów i zacznie nam stosować "uniki" czyli np. ciąg typu:

$str="To jest ciąg znaków \"Tutaj coś relacjonujemy\". Koniec relacji";
//niestety ze względu na błędną obsługę tej linii przez funkcję highlight_string nie jest ona pokolorowna

No to pojawia sie problem, bo wyrażenie regularne jest tak skonstruowane, że zakończy nam działanie po napotkaniu kolejnego apostrofa i nie będzie go obchodziło czy jest on poprzedzony znakiem uniku.

No więc żeby temu zaradzić wylaczymy ciag \' lub \" z tych ktore maja być brane pod uwage poprzez zastosowanie negacji w klasie, czyli podania znakow które NIE MAJĄ być brane pod uwagę.

Do tego będzie nam potrzebny znak ^. Jak pewnie pamietacie w poprzedniej części opisywałem, że można go używać do zaznaczenia, że cos ma występować na początku szukanego ciągu znaków. Owszem, ale jego drugim zastosowaniem jest negacja. Jeżeli chcemy żeby znaki w klasie nie były brane pod uwagę należy to zrobic tak:

[^sxc]*

To wyrażenie będzie reagowało na dowolny znak oprócz s lub x lub c.

Teraz już możemy wrócić do naszego przykładu, aby zastanowić się co zrobić, żeby wyłączyć ukośnik. No jest to bardzo podobne:

("|').*?([^\\\\]\\1)

Teraz mówimy silnikowi wyrażeń regularnych, żeby szukał podwójnego lub pojedyńczego cudzysłowia na początku ciągu znaków i pobrał jak najmniej dowolnych znaków (? - ungreedy), oraz szukał do czasu znalezienia dowolnego dwuznakowego ciagu nie zaczynającego się od ukośnika. Teraz przełóżmy to na silnik php:

preg_replace('/("|\').*?([^\\\\]\\1)/s','',$tekst);

Dodałem
- unik na pojedyńczy ukośnik (bo inaczej php uznałoby że zakończyłem zbyt wcześnie ciąg znaków)
- modyfikator /s dzięki któremu wyrażenie jest traktowane jakby było w jednej linii i kropka pobiera także kolejne linie

To by było na tyle jeżeli chodzi o poszczególne części składni, teraz przejdzmy do zrobienia prawdziwego koloratora składni, aby temu podołać potrzebny nam będzie jeszcze jeden modyfikator /e. Zachowuje sie on podobnie jak php'owa funkcja eval, dzięki czemu możemy wywołać potrzebna funkcje z kodu php.

Budujemy css'y z nazwami kolorów jako nazwy klas.
<style>
.red{color:red;}
.pink{color:pink;}
.green{color:green;}
.blue{color:blue;}
</style>

Ustalamy, co będzie za co odpowiedzialne i stosujemy wyrażenia regularne:
$sCode = file_get_contents('plik.php');
$sColoredCode = '';//tutaj bedzie przechowywany pokolorowany kodzik //aby sie zabezpieczyc przed niepoprawnym wyswietlaniem //zamienmy tagi html na ich odpowiedniki na potrzeby wyswietlania $sColoredCode = str_replace( array( '<' , '>' ) , array( '<','>' ) , $sCode ); //zaczniemy od stringow $sColoredCode = preg_replace( '/("|\').*?([^\\\\]\\1)/s' , '\\0' , $sColoredCode ); //tutaj powinno byc kolorowanie elementow skladni ktore pomine //$sColoredCode = //kolorowanie komentarzy $sColoredCode = preg_replace( '%/\*.*?\*/%se' , '"".strip_tags("\\0").""' , $sColoredCode ); /** Mysle ze ta czesc wymaga komentarza. Otoz jako ze w komentarzach moga sie zdarzyc takze pozostale elemety skladni, dlatego ich kolorowanie powinno sie odbyc na samym koncu, ponadto powinny one byc "odarte" z tagow html'owych czyli wczesniejszych zmian. W celu umozliwienia wlasnie tutaj jest uzyty modyfikator /e dzieki ktoremu mozliwe jest skorzystanie z php'owej funkcji strip_tags
*/

Elementem, którego do tej pory nie poruszyłem, a jak myślę jest dość ważmy dla większości programistów (dla mnie jest) są odpowiednie wcięcia w tekście. No i czy takowe wcięcia to będa tab'y czy spacje.

Mam na to rozwiązanie, które jest w miare proste do wprowadzenia, ale jego minusem jest to, że nie zachowuje ono oryginalnego formatowania, natomiast zamiast niego wprowadza swoje. Polega ono na zamianie klamerek otwierających na klamerki z dołączonym znacznikiem otwierającym warstwę oraz klasa definiująca margines, natamiast klamerki zamykające tylko ze znacznikiem zamykajacym warstwę.
$sColoredCode = str_replace( array( '{' , '}' ) , array( '
{' , '< /div>}' ) , $sColoredCode );
Dzieki takiemu zabiegowi nasz kodzik otrzyma odpowiednie wcięcia, więc razem z kolorami będzie wszystko wyglądało super.
Jedyne co nam teraz pozostało to wyśwyietlić kod i po sprawie:

echo $sColoredCode;
  • A Django site.