Formatowanie kodu XML bez użycia dodatkowych bibliotek
Patyk
Poniższa funkcja, formatująca kod XML, ustawiając prawidłowe wcięcia przed każdym znacznikiem, stanowi prostą alternatywę dla zaawansowanych bibliotek.
// Formatowanie kodu XML bez użycia dodatkowych bibliotek
// Copyright (C) 2006, by Bartosz Pieńkowski
const
NL = #13#10; // Znak nowej linii, zależnie od systemu
TAB = #32#32; // Pojedyńcze wcięcie
TAB0 = 0; // Początkowe wcięcie
function formatXML(s: AnsiString): AnsiString;
var
text: AnsiString;
stack: array of AnsiString;
stackt: array of ShortInt;
posa, posb: Integer;
i, j, tabs: Integer;
begin
// Pozbycie się znaków nowej linii, obcięcie białych znaków:
s := Trim(StringReplace(s, NL, '', [rfReplaceAll]));
while Length(s) > 0 do
begin
posa := pos('<', s);
posb := pos('>', s);
if posa*posb = 0 then // Nieprawidłowa ilość klamer
begin
Result := Trim(s); // Zwrócenie początkowego łańcucha
Exit;
end;
if posa > 1 then // Jeżeli znacznik jest poprzedzony łańcuchem
begin
text := Copy(s, 1, posa-1);
if Trim(text) <> '' then // Jeżeli tym łańcuchem nie są same białe znaki
begin
SetLength(stack, Length(stack)+1);
stack[High(stack)] := text; // Wrzucenie łańcucha na stos
end;
end;
SetLength(stack, Length(stack)+1);
stack[High(stack)] := Copy(s, posa, posb-posa+1); // Wrzucenie znacznika na stos
// Usunięcie znacznka i poprzedzającego go tekstu z łańcucha początkowego:
Delete(s, 1, posb);
end;
SetLength(stackt, Length(stack));
for i := 0 to Length(stack)-1 do
begin
// Ustalamy typ danych znajdujących się na stosie:
if (stack[i][1] = '<') and (stack[i][Length(stack[i])] = '>') then // Znacznik
if stack[i][Length(stack[i])-1] = '/' then
stackt[i] := 2 // Znacznik pusty
else if stack[i][2] = '/' then
stackt[i] := 3 // Znacznik zamykający
else if (stack[i][Length(stack[i])-1] = '?') and (stack[i][2] = '?') then
stackt[i] := 4 // Nagłówek
else if (Copy(stack[i], 2, 3) = '!--')
and (Copy(stack[i], Length(stack[i])-2, 2) = '--') then
stackt[i] := 5 // Komentarz
else
stackt[i] := 1 // Znacznik otwierający lub nierozpoznany
else
stackt[i] := 0; // Tekst lub inny, nierozpoznany typ
end;
tabs := TAB0; // Początkowe wcięcie
for i := 0 to Length(stackt)-1 do
begin
// Różne działania w zależności od typu danego elementu stosu
case stackt[i] of
0 : s := Concat(s, stack[i]); // Tekst
1 : begin // Znacznik otwierający
for j := 1 to tabs do
s := Concat(s, TAB); // Dodanie wcięcia
s := Concat(s, stack[i]); // Dodanie znacznika
if i+1 < Length(stackt) then
// Jeżeli następny element to znacznik otwierający lub komentarz:
if (stackt[i+1] = 1) or (stackt[i+1] = 2) or (stackt[i+1] = 5) then
begin
s := Concat(s, NL); // Dodanie znaku nowej linii
Inc(tabs); // Powiększenie wcięcia
end;
end;
2 : begin // Znacznik pusty
// Jeżeli poprzedni element jest znacznikiem:
if (i-1 >= 0) and (stackt[i-1] <> 0) then
for j := 1 to tabs do
s := Concat(s, TAB); // Dodanie wcięcia
s := Concat(s, stack[i], NL); // Dodanie znacznika i znaku nowej linii
if i+1 < Length(stackt) then
// Jeżeli następny element jest znacznikiem zamykającym:
if stackt[i+1] = 3 then
Dec(tabs); // Zmnejszenie wcięcia
end;
3 : begin // Znacznik zamykający
// Jeżeli poprzedni element jest znacznikiem:
if (i-1 >= 0) and (stackt[i-1] <> 0) then
for j := 1 to tabs do
s := Concat(s, TAB); // Dodanie wcięcia
s := Concat(s, stack[i], NL); // Dodanie znacznika i znaku nowej linii
if i+1 < Length(stackt) then
// Jeżeli następny element jest znacznikiem zamykającym:
if stackt[i+1] = 3 then
Dec(tabs);
end;
4 : begin // Nagłówek
// Jeżeli poprzedni element jest znacznikiem:
if (i-1 >= 0) and (stackt[i-1] <> 0) then
for j := 1 to tabs do
s := Concat(s, TAB); // Dodanie wcięcia
s := Concat(s, stack[i], NL); // Dodanie znacznika i znaku nowej linii
end;
5 : begin // Komentarz
// Jeżeli poprzedni element jest znacznikiem:
if (i-1 >= 0) and (stackt[i-1] <> 0) then
for j := 1 to tabs do
s := Concat(s, TAB); // Dodanie wcięcia
s := Concat(s, stack[i], NL); // Dodanie znacznika i znaku nowej linii
end;
end;
end;
Result := s;
end;
Sposób użycia
Aby przystosować sposób formatowania kodu do własnych potrzeb mamy do dyspozycji 3 stałe:
const
NL = #13#10; // Znak nowej linii, zależnie od systemu
TAB = #32#32; // Pojedyńcze wcięcie
TAB0 = 0; // Początkowe wcięcie
Stała NL określa znak nowej linii. Wykorzystywana jest podczas usuwania białych znaków z wejściowego łańcucha oraz przy dodawaniu znaków nowej linii do wyjściowego łańcucha. Znak nowej linii zależy od systemu (więcej informacji).
Stała TAB to dowolny łańcuch, stanowiący pojedyńcze wcięcie. Wedle uznania mogą to być np. 3 spacje (#32#32#32), jak również znak tabulacji (#9).
Ostatnia ze stałych - TAB0 określa wcięcie początkowe, a dokładniej ilość pojedyńczych wcięć, które zostaną dodane przed każdym wierszem kodu.
Przykładowo dla wartości 1, kod:
<a><b>tekst</b></a>
...po formatowaniu będzie wyglądał tak:
<a>
<b>tekst</b>
</a>
Po niezbędnym ustaleniu powyższych stałych, użycie funkcji sprowadza się do prostego zapisu:
var
XMLCode: AnsiString;
begin
XMLCode := formatXML(XMLCode);
end;
Opis działania
Funkcję można podzielić na 3 fazy:
- Wyodrębnienie poszczególnych elementów (znaczniki, tekst) przekazanego przez argument łańcucha, wrzucenie ich na stos.
- Ustalenie typu każdego elementu znajdującego się na stosie. Jeżeli elementem jest znacznik - ustalenie jego rodzaju (otwierający, zamykający, pusty, nagłówek, komentarz)
- Formatowanie każdego elementu w zależności od jego typu.
Działanie poszczególnych faz omówię na przykładzie kodu XML:
<a><b>
<c/><d>tekst</d>
</b>
</a>
I. Faza
Pierwszym działaniem, jakie trzeba wykonać jest usunięcie znaków nowej linii z wejściowego łańcucha. Po tym zabiegu przykładowy kod XML wyglada tak:
<a><b> <c/><d>tekst</d> </b></a>
Teraz, aż do pozostawienia pustego łańcucha, wycinamy z niego fragment od początku, do pierwszego odnalezionego znacznika (włącznie).
Jeżeli w wyciętym fragmencie znacznik poprzedzony jest łańcuchem oraz łańcuch ten nie składa się tylko z białych znaków zostaje on wrzucony na stos, gdzie zaraz potem zostaje umieszczany sam znacznik.
Podczas tego zabiegu kolejne wycinane fragmenty przykładowego kodu wygladałyby w ten sposób:
<a>
<b>
<c/>
<d>
tekst</d>
</b>
</a>
W przypadku znaczników <c/>
i </b></code></span> poprzedzający je łańcuch jest ignorowany, gdyż zawiera same białe znaki. W przypadku znacznika <span style="font-family: Courier New;"><code></d>
poprzedzający go tekst zostaje wrzucony na stos.
Po zakończeniu pierwszej fazy stos prezentuje się w ten sposób:
<a>
<b>
<c/>
<d>
tekst
</d>
</b>
</a>
II. Faza
W zależności od pewnych cech każdego elementu stosu, ustalany jest jego typ. Obsługiwane przez funkcję rodzaje to:
*0 - Zwykły tekst
*1 - Znacznik otwierający (pierwszy znak to "<" a ostatni ">")
*2 - Znacznik pusty (przedostatni znak to "/")
*3 - Znacznik zamykający (drugi znak to "/")
*4 - Nagłówek (drugi i przedostatni znak to "?")
*5 - Komentarz (zaczyna się ciągiem "!--", a kończy "--")
Po zakończeniu drugiej fazy, stos typów elementów to:
1
1
2
1
0
3
3
3
</b>
### III. Faza
Ostatnim zadaniem funkcji jest sformatowanie każdego elementu stosu w zależności od jego rodzaju. Formatowanie to opiera się na wstawieniu odpowiedniej ilości wcięć przed znacznikami.
Wyznacznikiem poziomu wcięcia w powyższym kodzie jest zmienna <b>tabs</b>, która jest inicjowana wartością <b>TAB0</b>. Określa ona ile razy przed każdym znacznikiem zostanie dodane pojedyńcze wcięcie (czyli stała <b>TAB</b>).
Kiedy pętla formatująca napotka <b>znacznik otwierający</b> (oraz następny element jest również znacznikiem otwierającym, bądź komentarzem) zmienna <b>tabs</b> jest inkrementowana i dodawany jest znak nowej linii.
Analogicznie jest w przypadku napotkania <b>znacznika zamykającego</b> - następuje dekrementacja zmiennej <b>tabs</b> (o ile następny element jest znacznikiem zamykającym) i dodanie znaku nowej linii. Identycznie funkcja zachowa się napotykając na <b>znacznik pusty</b>.
Jeżeli elementem stosu jest <b>zwykły tekst</b>, zostaje on najzwyklej (bez poprzedzających wcięć, ani znaków nowej linii) dodany do wyjściowego łańcucha.
W przypadku <b>nagłówka</b> lub <b>komentarza</b> oprócz samego znacznika funkcja dodaje po nim jeszcze znak nowej linii.
____
Zgodnie z powyższymi zasadami, wcięcia w przykładowym kodzie XML będą wyglądały tak: (znak ⇔ symbolizuje pojedyńcze wcięcie)
```xml
<a>
⇔<b>
⇔⇔<c/>
⇔⇔<d>tekst</d>
⇔</b>
</a>
W ten sposób doszliśmy do rezultatu funkcji, która dla przykładowego kodu zwróci łańcuch:
<a>
<b>
<c/>
<d>tekst</d>
</b>
</a>
"linijka (...) usuwa entery również z danych!"
Niestety, funkcja ta opiera się na zasadzie pozbycia się wszystkich znaków nowej linii z formatowanego łańcucha.
Nadaje się ona idealnie do formatowania dokumentów XML o prostej strukturze, do bardziej skomplikowanych plików polecam alternatywne, bardziej rozbudowane i przemyślane rozwiązania... :)
Coldpeer, z przymusu i z niechęcią.
Marooned, czyżby powrót do Delphi? :>
Bardzo poważny błąd, miałem zgłosić na forum.
linijka
s := Trim(StringReplace(s, NL, '', [rfReplaceAll]));
usuwa entery również z danych! Niestety, zmieniając dane wewnątrz tagów ta funkcja w obecnej postaci jest średnio użyteczna.
A ponieważ używam jej i jest mi ona potrzebna, liczę, że pojawi się wersja poprawiona :)
b. fajny art, nie testowalem jeszcze ale wyglada OK :)
Od razu zaznaczam, że kod nie nadaje się do porządkowania kodu XHTML :P