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:

  1. Wyodrębnienie poszczególnych elementów (znaczniki, tekst) przekazanego przez argument łańcucha, wrzucenie ich na stos.

  2. 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)

  3. 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>

6 komentarzy

"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