Ab ovo

Czym jest xml? Dokumentem. Zasadniczo tekstowym, ale ze ścisłymi wymaganiami dotyczącymi struktury. Przykład (głupiego) pliku zawierającego większość xml’owych zjawisk poniżej:

01/goopi.xml

Ale my o xslt mamy mówić. Powiemy wykorzystując ten dziwny plik (nie róbcie takich!). W celu wymuszenia transformacji w przeglądarce w dokumencie, tuż za prologiem, umieszczamy następującą instrukcję sterującą (pozdrowienia dla fanatyków od tej na O):

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="plik.xsl"?>
<ElementGłówny>
  ...
</ElementGłówny>

Szkielet transformacji

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="UTF-8" doctype-public="-//W3C//DTD HTML_4.01//EN"/>
  ...
</xsl:stylesheet>

Ma być taki i już (to nie uczelnia, nie trzeba na tym etapie wiedzieć dlaczego tak :-). Kodowanie zadeklarowane zostało pro-forma. Dokumenty bez deklaracji traktowane są jako UTF-8 właśnie. xsl:output decyduje o tym, co ma się pojawiać na wyjściu. My chcemy HTML 4.01Strict. A co! Stać nas.

Działanie transformacji polega na dopasowywaniu szablonów do węzłów i wykonaniu zawartych w nich instrukcji.

Niektóre z instrukcji to polecenia wybrania (kolekcji) węzłów oraz dopasowania do nich kolejnych szablonów. I kolejnych. I kolejnych. I kolejnych. Do pomyślnego wyniku. Albo do informacji procesora xslt o możliwej nieskończonej pętli ;-)

Dopasowania

Szablony definiowane są przez polecenie xsl:template match="wyrażenie". Nasze pierwsze dopasowanie to match="/".Przy wykonaniu transformacji z instrukcji sterującej właśnie ten szablon będzie najlepiej dopasowany a więc i wykonany. To najlepszy – moim zdaniem – sposób zaczynania transformacji do HTML’a.

DOM’owe: xmlDoc.transformNode(xslDoc); nie jest tym samym co: xmlDoc.documentElement.transformNode(xslDoc); bo w drugim przypadku wysyłamy węzeł, więc załapią się dopasowania typu:match="/*", match="*" czy match="Nazwa". → Kilka nieporadnych obrazków o DOM'ie i osiach

Pierwszy szablon:

<xsl:template match="/">
  <html>
    <head>
      <link type="text/css" rel="stylesheet" href="xml2html.css"/>
      <title>XML wyglądający jak XML ale w HTML</title>
    </head>
    <body>
      <xsl:apply-templates/>
    </body>
  </html>
</xsl:template>

Zwracamy HTML’a możemy więc korzystać z całego dobrodziejstwa css’a, skryptów i innych takich. Trzeba tylko pamiętać, że to działa na wygenerowanego HTML’a, który nie widzi xml’a z którego powstał. Da się to powiązać, ale o tym – jak dożyje – w kolejnych odcinkach.

Polecenie <xsl:apply-templates/> prosi o dopasowanie wszystkich węzłów (nie mylić z elementami aka tagami...). I jeżeli na tym skończymy nic się nie powinno wydarzyć, no może kolor w przeglądarce od css’a się zmieni. Tymczasem...

01/1.xml 01/1.xsl

W wielkim skrócie: procesor xslt stworzył kolekcję wszystkich węzłów podrzędnych i dopasował do nich odpowiednie szablony wbudowane, które dla wezła-węzła wykonuje znane nam już <xsl:apply-templates/> a dla tekstu zwraca jego wartość.

Szczegóły: http://www.w3.org/TR/xslt#built-in-rule.

Jak widać: <xsl:apply-templates/> to to samo co: <xsl:apply-templates select="node()"/> ponieważ dla: <xsl:apply-templates select="processing-instruction()|comment()"/> nic się według domyślnych reguł nie dzieje.

Niewinne <xsl:apply-templates/> zmusiło procesor xslt do wykonania masy działań, z których część przy standardowych zastosowaniach transformacji jest absolutnie niepotrzebna. Warto brać to pod uwagę bo zarabiać mamy my a nie producenci procesorów i pamięci.

Węzły i węzły

Dla uproszczenia można pamiętać, że node() to wszystko co można znaleźć wewnątrz xml’owego dokumentu i co nie jest atrybutem. Będę się starał dla węzła-węzła używać określenia element.

Spróbujemy przeanalizować zawartość goopiego dokumentu przy pomocy transformacji:

<xsl:template match="node()">
  <div>
    <span class="nodeType">
      <xsl:choose>

Sprawdzamy xsl’owym wielokrotnym if’em (to nie jest switch – wykonywane są wszystkie test do pierwszego który zwróci true). A w ten sposób ponieważ nie mamy do dyspozycji DOM’owego nodeType )-:

<xsl:when test="self::*">Element</xsl:when>

Pierwsza oś! (self::) Tutaj mocno przydatna. O innych na razie cisza – skupmy uwagę na kaskadowym przetwarzaniu dokumentu. → Kilka nieporadnych obrazków o DOM'ie i osiach

<xsl:when test="self::comment()">Komentarz</xsl:when> <xsl:when test="self::text()">Tekst</xsl:when> <xsl:when test="self::processing-instruction()">Instrukcja sterująca</xsl:when> <xsl:otherwise> <xsl:attribute name="class">nodeType orange</xsl:attribute>

Przydatna ciekawostka: ze względu na to, że w elemencie nie może być dwóch atrybutów o tej samej nazwie, dany atrybut możemy ustawiać dowolną ilość razy. Na wyjściu dostaniemy ostatnią ustawioną wartość. Na początku przygody z xsl'em bardzo denerwuje fakt że nie da się tutaj (jak i przy innych standardowych wołaniach szablonów) inkrementować.

<xsl:text>Coś innego</xsl:text>

Proponuję na chwilę zamienić miejscami powyższe linie. Okaże się, że próba ustawienia atrybutu po wrzuceniu tekstu (jak i każdego innego węzła) zostanie zignorowana. O tym trzeba pamiętać!

</xsl:otherwise> </xsl:choose> <xsl:text>:</xsl:text> </span> <span class="nodeName"> <xsl:value-of select="name()"/>

Niektóre typy węzłów nie mają nazwy...

<xsl:if test="not(name())"> <i>bez nazwy</i> </xsl:if> </span> <span class="nodeValue"> <xsl:text> </xsl:text> <xsl:value-of select="."/>

...a niektóre wartości (no, nie do końca wartości).

<xsl:if test=".">

W ramach zabaw proponuję zmienić w obu powyższych linijkach text() na . – to taki szczegół, który daje po tyłku, jak się o nim nie pamięta. Ale . dla elementów zachowuje się na pozór niestandardowo. I między innymi dlatego w obrabianych na co dzień używam wyłącznie atrybutów.

<i>bez wartości</i> </xsl:if> </span> <div class="atrybuty">

Dodamy wyświetlanie atrybutów, które nie są węzłami, więc trzeba wybrać (select="@*") ich kolekcję z prośbą o dopasowanie i wykonanie szablonu (xsl:apply-templates):

<xsl:apply-templates select="@*"/>

na razie bez konkretnego szablonu (match="@*"), który to obsłuży.

<xsl:if test="not(@*)"> <i>brak atrybutów</i> </xsl:if> </div> <xsl:apply-templates/> </div> </xsl:template>

Do dokumentu dodałem pozory DTD – transformacja w IE tego nie zauważy, Mozilla owszem. I nie do końca jest to plus dla Mozilli – specyfikacja xslt nie przewiduje tego typu węzła. No ale może czegoś nie doczytałem.

Zobaczmy:

01/2.xml 01/2.xsl

Przyznaje, żadnej rewelacji. Dopiero następny przykład będzie kolorowy. Ale widać które typy węzłów mają nazwy, które wartości, a które jedno i drugie. Mam nadzieję, że:

Niestety po porównaniu wyniku transformacji w IE i Mozilli przestaniemy cokolwiek rozumieć. I to jest kolejny argument na trzymaniem wszystkiego w atrybutach – ich zachowanie da się przewidzieć bez habilitacji. Co na początku przygody z transformacjami jest bardzo cenne.

Widać też, że prolog dokumentu jest nie do końca instrukcją sterującą, pomimo skutecznie działającego DOM’owego: xmlDoc.createProcessingInstruction('xml','version="1.0" encoding="UTF-8"') Widać również – o czym stoi przykładzie dokumentu - że z punktu widzenia xml’a reszta instrukcji sterującej stanowi całość.

Ładniej (i szybciej?)

Pokazałem, że <xsl:apply-templates/> robi znacznie więcej niż zajmuje miejsca. Dla przypomnienia:

  1. wymusza stworzenie kolekcji wszystkich węzłów podrzędnych,
  2. wymusza dopasowanie do nich szablonów,
  3. wymusza ich wykonanie...
  4. ... zawierające we wbudowanym szablonie przejście do pkt.1.

Dlatego właśnie irytują mnie popularne w przykładach konstrukcje typu:

    ...
    <xsl:apply-templates select="@* | node()"/>
    ...
<xsl:template match="@* | node()">
  ...

To co, że działają? Po co zmuszać procesor xslt do szukania atrybutów węzłach tekstowych czy komentarzy wewnątrz atrybutów? Dlatego zapominamy o poprzednim przykładzie. Zwłaszcza, że testów robi o wiele za dużo. W pierwszej chwili chcemy zrobić serie szablonów:

<xsl:template match="*">
  ...
<xsl:template match="@*">
  ...
<xsl:template match="text()">
  ...
<xsl:template match="processing-instruction()">
  ...
<xsl:template match="text()">
  ...

Kierunek słuszny, ale zastanówmy się nad naszym dokumentem z punktu widzenia HTML’a którego mamy wyprodukować. Zamykanie węzłów okaże się rzeczą niebanalną. Zwłaszcza jeżeli zechcemy uniknąć powielania kodu w szablonach.

Dlatego w pierwszym kroku skorzystamy z filozofii nazywanej przeze mnie od dupy strony (bo jak wiadomo ważne jest jak się kończy a nie jak zaczyna). Spróbujmy pokazać naszego xml’a na modę IE (bardziej do mnie przemawia niż moda Mozillowa).

Krok pierwszy – elementy:

<xsl:template match="node()">

Następne osie (::). Ale proszę się nie przywiązywać do ich istnienia. Przetwarzanie (w odróżnieniu od uskutecznianego tutaj prezentowania) dobrze zaprojektowanego dokumentu nie wymaga korzystania z tego zjawiska. xml jako domyślna wartość atrybutu method w xsl:output to nie przypadek!

<xsl:if test="parent::*">

Jeżeli jest co zamykać, znaczy mamy węzeł nad sobą, znaczy nie jesteśmy w documentElement.

<xsl:if test="not(preceding-sibling::node())">

Jeżeli nie ma poprzedzającego węzła trzeba wstawić HTML’ową reprezentację końca elementu nadrzędnego:

<span class="gt">&gt;</span> </xsl:if> </xsl:if>

Konstrukcja if w if'ie bez innego if'a jest w tym miejscu co najmniej głupia, więc w transformacji połączyłem te warunki przy pomocy and.

<div> <xsl:apply-templates select="." mode="details"/>

Ileż ja się nakombinowałem przed okryciem mode :-( Wysyłamy do przetworzenia kolekcję elementów (tutaj składającą się z bieżącego węzła) w inny niż normalnie sposób. Cudowne zjawisko!!! Zdecydowanie lepiej nadużywać niż nie korzystać!

</div> </xsl:template>

Korzystamy z czarodziejskiego mode:

<xsl:template match="*" mode="details">

Wstawiamy znak rozpoczęcia węzła:

<span class="lt">&lt;</span>

Nazwę elementu:

<span class="element"> <xsl:value-of select="name()"/> </span> <xsl:if test="not(node())">

Jeżeli element nie ma podrzędnych węzłów (atrybuty za chwile), to zamykamy go na krótko.

<span class="gt">/&gt;</span> </xsl:if>

Żebyśmy nie zapomnieli o atrybutach:

<xsl:apply-templates select="@*"/>

Element może mieć pod sobą wszystkie typy węzłów wiec bez skrupułów ta postać żądania dopasowania.

<xsl:apply-templates/> <xsl:if test="node()">

Jeżeli podrzędne elementy są - standardowy znacznik zamykający:

<span class="lt">&lt;/</span> <span class="element"><xsl:value-of select="name()"/></span> <span class="gt">&gt;</span> </xsl:if> </xsl:template>

I wszystko (w temacie elementów). Wartości atrybutów świadomie wypuściliśmy na wyjście by default żeby za chwilę nie tłumaczyć się dlaczego ich xsl:apply-templates select="@*" jest w tym a nie innym miejscu.

01/3.xml 01/3.xsl

Węzły tekstowe zachowały się wreszcie jak ludzie. I można by snuć wnioski dlaczego. Tylko po co skoro na tym etapie spokojnie możemy ich unikać?

Ten powrót na standardową ścieżkę przetwarzania jest tak naprawdę powrotem do standardowego drzewa przetwarzania. Trzeba pamiętać, że z punktu widzenia transformacji niektóre rzeczy dzieją się jednocześnie. To oszczędza rozczarowań przy próbach korzystania z (głupawego) <xsl:for-each ..., którego kariera wynika wyłącznie z ładnego wyglądu przykładów pałętających się po sieci (wprowadzających początkujących w niezłe krzaki).

Prawie jak Żywiec :-( właśnie się skończył )-:

Pora na atrybuty:

<xsl:template match="@*">
  <xsl:text> </xsl:text>
  <span class="name">
    <xsl:value-of select="name()"/>
  </span>
  <span class="attContainer">
    <xsl:text>="</xsl:text>
    <span class="value">
      <xsl:value-of select="."/>

Ale select="text()" tutaj już nie zadziała – wyciągnij wnioski!

</span> <xsl:text>"</xsl:text> </span> </xsl:template>

Komentarze:

<xsl:template match="comment()" mode="details">
  <div class="comment">
    <span class="commentContainer">
      <xsl:text>&lt;!-- </xsl:text>
      <span class="comment">
        <xsl:value-of select="."/>
      </span>
      <xsl:text> --&gt;</xsl:text>
    </span>
  </div>
</xsl:template>

Instrukcje sterujące:

<xsl:template match="processing-instruction()" mode="details">
  <div class="processingInstruction">
    <xsl:text>&lt;?</xsl:text>
    <xsl:value-of select="name()"/>
    <xsl:text> </xsl:text>
    <xsl:value-of select="."/>
    <xsl:text>?&gt;</xsl:text>
  </div>
</xsl:template>

Węzły tekstowe:

<xsl:template match="text()">
  <div class="text value"><xsl:value-of select="."/></div>
</xsl:template>

I już!

01/4.xml 01/4.xsl

Prawie jak IE. Prawie jak Żywiec.

Można te węzły w nieskomplikowany sposób chować i pokazywać, ale to po pierwsze DHTML, a po drugie naszym celem było na mocno abstrakcyjnym przykładzie zrozumieć strukturę xml’a i zaprzyjaźnić się z transformacjami. Pozostaje kwestia nierozróżnialności węzłów tekstowych i CDATA – ale według mojej wiedzy w xslt (przynajmniej w 1.0) po prostu niedasie.

Coś nie tak? Coś się nie zamyka?

Zajrzyj do <xsl:template match="*" mode="details">:

Ten kawałek:

  <xsl:if test="not(node())">
    <span class="gt">/&gt;</span>
  </xsl:if>

Trzeba zamienić na:

  <xsl:if test="not(node())">
    <span class="gt">/</span>
  </xsl:if>
  <span class="gt">&gt;</span>

I będzie dobrze. Przemyśl i uzasadnij :-)

W następnym odcinku (jeżeli dożyje):

Dokument bliższy życia, którego HTML'owa reprezentacja rozumie dokument wyjściowy. A w jeszcze następnym go zmodyfikuje.

UWAGA: To jest robocza wersja dokumentu i jego załączników. Udostępniona zasadniczo w celu testowania oraz zgłaszania uwag i spostrzeżeń.

Zasady korzystania:

  1. Do celów edukacyjnych, niezwiązanych z wykonywaną pracą zawodową (na przykład uczniowie, studenci i inni hobbyści nie zarabiający pieniędzy szeroko rozumianą działalnością informatyczną) – na własne potrzeby bezpłatnie i bez ograniczeń, ale w przypadku osiągania dochodów prośba o rozważenie skorzystania zasad zawartych w punkcie 2a.
  2. Do celów edukacyjnych związanych bezpośrednio z wykonywaną pracą/sposobem osiągania przychodów (na przykład programiści i koderzy) – na własne potrzeby (w tym wykonywania pracy zawodowej/osiągania przychodów) bez ograniczeń po spełnieniu następujących wymagań:
    1. Wsparcie kwotą jaką korzystający uzna za słuszną:
      Fundacja SERCE – Europejskie Centrum Przyjaźni Dziecięcej
      Organizacja Pożytku Publicznego
      58-100 Świdnica, ul. Kościelna 15
      numer konta: 42 1020 5138 0000 9102 0067 8961
      tytuł wpłaty: darowizna rzecz organizacji pożytku publicznego
      Będzie mi miło jeżeli dostanę na maila (skompresowany:-) skan/zrzut z ekranu z informacją o wpłacie.
    2. W przypadku bezpośredniego korzystania z udostępnionego kodu (w tym zmodyfikowanego, bez ingerencji w istotę rozwiązań) proszę o zamieszczenie w kodzie informacji o kursie z jego adresem: http://szomiz.republika.pl/.
    3. Przy wysyłaniu do przeglądarki wyników działania kodu wykonanego na serwerze zamieszczenie informacji o korzystaniu z kursu w tagu META.
    4. Poinformowanie mnie (jeżeli nie zabraniają tego zobowiązania wobec osób trzecich) o projektach i rozwiązaniach w których kod udostępniony w kursie został wykorzystany.
  3. Do wszelkich innych celów, zwłaszcza związanych z wykonywaną/prowadzoną działalnością edukacyjna, szkoleniową, wydawniczą lub publikacyjną – wykorzystanie możliwe wyłącznie po indywidualnym określeniu zasad i warunków pomiędzy autorem kursu a korzystającym.
  4. Osoby, wnoszące swój wkład w treści zawarte w kursie udzielają autorowi kursu nieodpłatnej licencji umożliwiającej co wykorzystanie i udostępnienie co najmniej na określonych tutaj zasadach.
  5. Oczywiście nie biorę żadnej odpowiedzialności za rezultaty działania kodu i jego modyfikacji :-)
© Sławomir Zimosz - wszelkie prawa zastrzeżone.
Przedruk, publikacja części lub całości bez wiedzy i zgody autora zabronione.
Kontakt: szomiz@poczta.onet.pl