Bliżej natury

Ładnie wygląda pierwszy odcinek w pokolorowanym HTML'u?
Pomóż przerobić bieżący!
Wdzięczność gwarantowana:-)

Po abstrakcyjnym wstępie popracujemy z dokumentem bliższym życia. Pracować będziemy na atrybutach (jeżeli komuś będą niezbędne wersje dla wartości tekstowych – chętnie podejmę negocjacje cenowe :-) Przygotowałem prościutki dokument, symbolizujący -for-giw-maj-ignorans- fakturę. Merytorycznie patrząc tam powinno być jeszcze kilka rzeczy, ale to nie jest faktura, tylko dokument ją symbolizujący. Takie demo faktury...

02/invoice.xml

Uwaga natury organizacyjnej – jeżeli jakiś szablon (kawałek szablonu) w transformacji jest już przewałkowany, albo boleśnie oczywisty, to w źródle występuje w jednej linii w celu skupienia uwagi na tym co istotne.

Najpierw rysujemy dokument byle jak:

<xsl:template match="/"><html><head><link type="text/css" rel="stylesheet" ...
<xsl:template match="*">
  <div class="inline">
    <span class="nodeName"><xsl:value-of select="name()"/></span>
    <xsl:apply-templates select="@*"/>
  </div>
  <div class="content">
    <xsl:apply-templates select="*"/>
  </div>
</xsl:template>

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

Ja w ten sposób zaczynam tworzyć transformacje – kilka linijek kodu a można w trakcie pracy sprawdzać co już obsłużyliśmy, a co do obsłużenia jeszcze zostało. Jeżeli czegoś nie chcemy obsługiwać załatwiamy sprawę krótkim:

<xsl:template match="TegoNie"/>

Zajmiemy się pozycjami faktury, bo w nagłówku nic ciekawego (na tym etapie) wydarzyć się nie może:

<xsl:template match="Positions">

Wiem, tabelki są passe – ale żeby facet nie mógł z goła babą w windzie?!?

<table > <thead> <tr>

Najpierw rysujemy nagłówek – wykorzystamy w tym celu pierwszy wiersz (a dokładnie jego atrybuty). Chciałoby się napisać po prostu:

<xsl:apply-templates select="*[1]/@*" mode="head"/>

I nawet by to działało. Ale tylko do pewnego momentu Jak zaczniemy (a kiedyś zaczniemy) modyfikować dokument DOM’em okaże się, że dodawane atrybuty pojawiają na końcu, więc ich kolejność w sąsiednich węzłach może być inna. Nie jest to minus atrybutów. Węzły też najprościej dopina się w kolejności tworzenia. Dlatego (na razie):

<xsl:apply-templates select="*[1]/@Name" mode="head"/> ... <xsl:apply-templates select="*[1]/@Brutto" mode="head"/> </tr> </thead>

Wysyłamy do przetworzenia pozycje faktury (dlaczego nie select="Item"? dlatego, że wiemy, że wszystkie węzły są Item, więc po co się i maszynerię męczyć).

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

I we wszystkich kursokonferencjach, które widziałem w tego typu miejscu pojawia się (głupawy) <xsl:for-each select="*"... Dlaczego? Bo przykład ładnie wygląda :-(

Fakt, wyglądają ładniej. Ale nie zmuszają odbiorcy do wysiłku załapania istoty transformowania. I uczą się ludzie tej konstrukcji generując kod na pięć szerokości ekranu (powodzenia przy konserwacji!). Wykonując później takie numery:

<for-each select="*">
  <xsl:apply-templates select="."/>
 </for-each>
 

To co wyżej znajduje się na pierwszym miejscu w moim prywatnym rankingu xslt’owych (nie bójmy się mocnego słowa) kretynizmów.

Rozgadałem się, ale po prostu pamiętam, jakim bólem było postawienie się pod ścianą, do której prowadzi ta instrukcja (Najprostszy przykład: konieczność powielania kodu, co daje po tyłku przy modyfikacjach. No i nie widać na pierwszy rzut oka gdzie trzeba przerobić, żeby zmienić funkcjonalność).

    </tbody>

Teraz podsumowanie pozycji:

<tfoot> <tr class="sum"> <td colspan="5">RAZEM:</td> <td> <xsl:value-of select="sum(*/@Netto)"/>

Można tak. Ale można ładniej. Sformatujmy wartości:

<xsl:value-of select="format-number(sum(*/@Netto),'0.00')"/> <td> </td> <td> <xsl:value-of select="format-number(sum(*/@VatAmount),'0.00')"/> </td> <td> <xsl:value-of select="format-number(sum(*/@Brutto),'0.00')"/> </td> </tr>

format-number() to zagadnienie na oddzielny odcinek (ale był by strasznie nudny). Proponuję poszukać informacji o xsl:decimal-format definiującym zachowanie funkcji (tam ustawia się niestandardowe delimitery, wartości dla NaN i inne takie).

Teraz zabawa z zestawieniem sprzedaży wg poszczególnych stawek. Na razie umiemy mało, więc metodą brute-force wymusimy wyliczenie wszystkich możliwych:

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

Pierwszy bez parametru, żeby dać sobie szansę na sprawdzenie sposobu działania wartości domyślnych.

<xsl:apply-templates select="." mode="subSum"> <xsl:with-param name="val">15 %</xsl:with-param> </xsl:apply-templates> <xsl:apply-templates select="." mode="subSum"> <xsl:with-param name="val">7 %</xsl:with-param> </xsl:apply-templates> <xsl:apply-templates select="." mode="subSum"> <xsl:with-param name="val">3 %</xsl:with-param> </xsl:apply-templates> <xsl:apply-templates select="." mode="subSum"> <xsl:with-param name="val">0 %</xsl:with-param> </xsl:apply-templates> <xsl:apply-templates select="." mode="subSum"> <xsl:with-param name="val">zw.</xsl:with-param> </xsl:apply-templates>

Oczywiście w następnym podejściu zrobimy to mniej głupio, a jeszcze w następnym całkiem poprawnie.

</tfoot> </table> </xsl:template>
Teraz trzeba namalować zawartość nagłówka, pozycji i zestawienia sprzedaży według stawek. Najpierw pola nagłówka (banał):
<xsl:template match="Item/@*" mode="head">

Wysyłaliśmy: <xsl:apply-templates select="*[1]/@Name" mode="head"/>

<th><xsl:value-of select="name()"/></th> </xsl:template>

Następnie pozycje:

<xsl:template match="Item"> <tr> <xsl:apply-templates select="@*"/>

Nie. Nie można liczyć na właściwą kolejność atrybutów. Z bólem serca wstawiamy brzydką listę dopasowań:

<xsl:apply-templates select="@Name"/> ... <xsl:apply-templates select="@Brutto"/> </tr> </xsl:template>

Moglibyśmy zakończyć, bo istnieje szablon z match="@*". Ale widok wyniku nie byłby budujący (jesteśmy gdzieś wewnątrz tabelki), więc atrybuty w Item obsłużymy osobno:

<xsl:template match="Item/@*">

Powyższe dopasowanie warto czytać jako: dowolny atrybut w wpięty w węzeł Item. Ścieżki w select="..." analizuje się zgodnie z intuicją od lewej do prawej. Ścieżki w match="..." w odwrotnym kierunku. Dopóki nie jest to intuicyjne należy przyjąć, że to żydo-masoński spisek ;-)

<td> <xsl:value-of select="format-number(.,'0.00')"/>

Dlaczego tak? Ponieważ 6 z 9’ciu pól ma wartości liczbowe a jesteśmy w szablonie z najbardziej ogólnym dopasowaniem.

</td> </xsl:template>

Dla pola z ceną zmieniamy format:

<xsl:template match="Item/@Price"> <td> <xsl:value-of select="format-number(.,'0.0000')"/> </td> </xsl:template>

Stawka VAT standardowo, bez formatowania:

<xsl:template match="Item/@VatRate"> <td> <xsl:value-of select="."/> </td> </xsl:template>

Pola z opisami wyrównujemy do lewej (domyślnie wartości idą do prawej):

<xsl:template match="Item/@Name | Item/@Classification | Item/@Unit"> <td class="left"> <xsl:value-of select="."/> </td> </xsl:template>

Jeżeli taka lista w match="..." jest stanowczo za długa można zastosować trick z contains(): match="Item/@*[concatins(' Nazwa1 Nazwa2 Nazwa3 Nazwa4 '),name()]" Czasami nazwy na siebie „zachodzą”. Jeżeli działa to na naszą korzyść – cieszymy się. Jeżeli nie: match="Item/@*[contains(' Nazwa1 Nazwa2 Nazwa3 '),concat(' ',name(),' ']" Mocno to niewydajne, ale czasami się przydaje.

Zostało jeszcze zestawienie sprzedaży według stawek:

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

Wołaliśmy to dopasowanie ileś tam razy pod rząd ze zmienionym parametrem.

<xsl:param name="val" select="'22 %'"/>

Za pierwszym razem jednak bez parametru. W takim przypadku wykorzystana zostanie wartość ustawiona w select="". Przy przekazaniu parametru przez <xsl:with-param ... wartość domyślna jest ignorowana.

Linie podsumowania produkujemy tylko w przypadku istnienia Item (bieżący szablon przetwarza węzeł Positions) ze stawką taką jak w parametrze.

<xsl:if test="*[@VatRate=$val]">

Nie pisałem o tym zbyt wiele, bo dla mnie to intuicyjne. Ale zaczynający przygodę z xsl’em mają z tym problemy. Co oznacza gwiazdka w powyższym test? Dowolnego węzła podrzędnego. Pytam: podrzędnego względem czego? Odpowiadam: podrzędnego względem kontekstu wywołania, czyli elementu, który spowodował wpadnięcie w bieżące match (nie węzła, nie elementu, bo match’ować – jak wiemy z pierwszego odcinka – można również inne rzeczy). <tr>

<td colspan="5"> </td> <td> <xsl:value-of select="format-number(sum(*[@VatRate=$val]/@Netto),'0.00')"/> </td> <td> <xsl:value-of select="*[@VatRate=$val]/@VatRate"/> </td> <td> <xsl:value-of select="format-number(sum(*[@VatRate=$val]/@VatAmount),'0.00')"/> </td> <td> <xsl:value-of select="format-number(sum(*[@VatRate=$val]/@Brutto),'0.00')"/> </td> </tr> </xsl:if> </xsl:template>

Koniec.

02/1.xsl 02/1.xml

Dane sprzedawcy i nabywcy zostały olane, bo to co chciałem pokazać siedziało w temacie pozycji. Dla rozrywki można popróbować dodać szablony dla match="Vendor" oraz match="Customer" (Zauważając, że na tym etapie to tak na prawdę ten sam szablon).

Pierwszy bajer

Zajmiemy się teraz spolszczeniem opisów wyniku transformacji. Znaczy zostanie ona zlokalizowana. Wykorzystamy funkcję document() – kolejny po mode="..." milowy kamień w poznawaniu transformacji (prywatnie najpierw odkryłem document()). Najpierw zdefiniujemy słownik zawierający polską wersję napisów:

02/!names-noNs.xml

Wykrzyknik na początku to moja prywatna konwencja sygnalizująca, że mamy do czynienia z bytem niezwiązanym z konkretnym elementem dokumentu. Myślnik i reszta, bo w następnym przykładzie słownik się zmieni.

Do transformacji, tuż pod <xsl:output (będzie to więc parametr globalny, widziany w całej transformacji) dodajemy:

<xsl:param name="names" select="document('!names.xml')/*"/>

A następnie radośnie z niego korzystamy podmieniając nazwy węzłów i atrybutów.

Dlaczego gwiazdka na końcu? Żeby nie wpadać w match="/" i móc korzystać od razu z zawartości dokumentu – u mnie to standard odwoływania się do plików zewnętrznych.

<xsl:template match="*"> <div class="inline"> <span class="nodeName">

Parametr zdefiniowany z użyciem select zaraz po name zachowuje się jak zestaw węzłów. Uwaga – skorzystanie z wersji <xsl:param name="..."><xsl:value-of select="..."/></xsl:param> da zawsze stringa z wartością pierwszego dopasowanego węzła z select!

<xsl:if test="not($names/*[@name=name(current())]/@value)">

Jeżeli w słowniku nie ma wartości zaznaczamy ten fakt kolorkiem i wrzucamy nazwę atrybutu.

<xsl:attribute name="style">color:#FC0</xsl:attribute> <xsl:value-of select="name()"/> </xsl:if> <xsl:value-of select="$names/*[@name=name(current())]/@value"/>

Dwa razy wyrażenie zakręcone trochę bardziej niż dotychczasowe. Warto zwrócić uwagę na sposób korzystania z name(...).

Świadomie nie wprowadzam rozróżnienia pomiędzy funkcjami xslt, funkcjami xPath oraz wyrażeniami xPath. Przy transformacjach te trzy zjawiska przenikają się tak bardzo, że rozróżnianie spowodowałoby więcej szkody niż pożytku. Zainteresowani mogą znaleźć komplet informacji w Internecie (linki na głównej stronie W3C).

</span> <xsl:apply-templates select="@*"/> </div> <div class="content"><xsl:apply-templates select="*"/></div> </xsl:template>

Jak się nietrudno domyślić sekwencję <xsl:if / <xsl:value-of trzeba by było wrzucić do trzech szablonów. Dlatego wyprowadzimy ją do osobnego:

<xsl:template name="nameFromDict">

Nowa konstrukcja – szablon nazwany. Moja druga nieulubiona po <xsl:for-each. Ale czasami przydatna - tutaj osiągnęlibyśmy to samo po zawołaniu <xsl:apply-templates select="." mode="nameFromDict"/> i zastąpieniu szablonu nazwanego „zwykłym” <xsl:template match="@* | *" mode="nameFromDict"> zdecydowałem się na ten przykład, w celu pokazania, że nazwane szablony posiadają standardowy kontekst wywołania.

<xsl:if test="not($names/*[@name=name(current())]/@value)"> <xsl:attribute name="style">color:#FC0</xsl:attribute> <xsl:value-of select="name()"/> </xsl:if> <xsl:value-of select="$names/*[@name=name(current())]/@value"/> </xsl:template>

Ostatecznie:

<xsl:template match="*"> <div class="inline"> <span class="nodeName"> <xsl:call-template name="nameFromDict"/> </span> <xsl:apply-templates select="@*"/> </div> <div class="content"><xsl:apply-templates select="*"/></div> </xsl:template> <xsl:template match="@*"> <span class="name"> <xsl:call-template name="nameFromDict"/> <xsl:text>:</xsl:text> </span> <span class="value"><xsl:value-of select="."/></span> </xsl:template> <xsl:template match="Item/@*" mode="head"> <th> <xsl:call-template name="nameFromDict"/> </th> </xsl:template>

Po praktycznym przykładzie trochę zabawy. Małym mykiem zlikwidujemy listę wołań atrybutów (wprowadzoną w celu uniknięcia problemów z ich kolejnością w węźle):

<xsl:template match="Item"> <tr> <xsl:apply-templates select="." mode="itemAtrributes"/>

Zamiast 9-ciu <xsl:apply-templates select="@Atrubut". wysyłamy do przetworzenia cały węzeł.

</tr> </xsl:template> <xsl:template match="*" mode="itemAtrributes"> <xsl:param name="list">Name Classification Price Count Unit Netto_VatRate VatAmount Brutto </xsl:param>

Domyślnie w wartości parametru ustawiamy listę nazw atrybutów w pożądanej kolejności.

<xsl:variable name="nextList" select="substring-after($list,' ')"/>

Żeby nie powtarzać kodu w warunku, za chwilę i przekazaniu parametru tworzymy listę kolejnych atrybutów.

Można było użyć w drugim przypadku <xsl:param. Działałoby to było poprawnie do czasu, kiedy szablon wykonałby się z powodu innego niż zaplanowane dopasowanie. Dlatego zmienne właściwe dla konkretnego szablonu lepiej definiować przez <xsl:variable zwłaszcza, że dzięki temu od razu widać co przychodzi (może przychodzić) z zewnątrz a co jest lokalne.

<xsl:apply-templates select="@*[name()=substring-before($list,' ')]"/>

Wysyłamy do standardowego przetworzenia atrybut o nazwie pierwszego z listy.

<xsl:if test="$nextList!=''"> <xsl:apply-templates select="." mode="itemAtrributes">

Wołamy ten sam szablon w tym samym kontekście, ale ze skróconą o jedną pozycję listą atrybutów.

<xsl:with-param name="list" select="$nextList"/> </xsl:apply-templates> </xsl:if> </xsl:template>

W celu sprawdzenia, że to naprawdę działa proponuję zmienić kolejność nazw domyślnej wartości list.

I od razu analogiczny szablon dla drugiej serii wywołań atrybutów:

<xsl:template match="*" mode="headAtrributes">
  <xsl:param name="list">Name Classification Price Count Unit Netto_VatRate VatAmount Brutto </xsl:param>
  <xsl:variable name="nextList" select="substring-after($list,' ')"/>
  <xsl:apply-templates select="@*[name()=substring-before($list,' ')]" mode="head"/>    
  <xsl:if test="$nextList!=''">
    <xsl:apply-templates select="." mode="headAtrributes">
      <xsl:with-param name="list" select="$nextList"/>
    </xsl:apply-templates>
  </xsl:if>
</xsl:template>

Od poprzedniego różni się tylko drobnym mode="head". Sprowadzenie tych dwóch szablonów do jednego wymagałoby przekazywania dodatkowego parametru i <xsl:choose wewnątrz. Konstrukcje mode="{$par}" na nasze szczęście nie są poprawne (można by się było za bardzo zagalopować).

Pora na mniej kolący w oczy sposób wygenerowania podsumowania sprzedaży według stawek VAT:

<xsl:template match="Positions">
  <table>
    <thead>
      <tr>

Zastosowanie przeróbki z poprzedniego kroku.

<xsl:apply-templates select="*[1]" mode="headAtrributes"/> </tr> </thead> <tbody><xsl:apply-templates select="*"/></tbody> <tfoot> <tr class="sum"><td colspan="5">...... <xsl:apply-templates select="*" mode="subSum">

Zamiast serii wołań z kolejnymi parametrami wysyłamy do wykonania wszystkie Item:

<xsl:sort select="translate(@VatRate, '%', '')" data-type="number" order="descending"/>

Ale nie w kolejności wystąpienia tylko malejąco według stawek (widzianych jako liczby a nie stringi, co ma miejsce domyślnie).

translate() to dziwna funkcja. Niby przerabia stringa, ale nie tak jak się większość osób spodziewa. To nie jest zamiana ciągu na ciąg. Tylko zamiana w wartości pierwszego argumentu wszystkich pierwszych znaku drugiego argumentu na pierwszy znak trzeciego argumentu (i tak po kolei drugi i trzeci argument mogą być różnej długości).

      </xsl:apply-templates>
    </tfoot>
  </table>
</xsl:template>

<xsl:template match="*" mode="subSum">
  <xsl:if test="not(preceding-sibling::*[@VatRate=current()/@VatRate])">

Linijkę rysujemy tylko dla pierwszego węzła z daną stawką. (Równie dobrze moglibyśmy to zrobić tylko dla ostatniego: not(preceding-sibling::*[@VatRate=current()/@VatRate])

<tr> <td colspan="5"> </td> <td> <xsl:value-of select="format-number(sum(../*[@VatRate=current()/@VatRate]/@Netto),'0.00')"/>

Sumujemy wszystkie pozycje z bieżącą stawką VAT. W ten sposób, ponieważ oś followig-or-preceding-sibling-or-self nie istnieje J

</td> <td><xsl:value-of select="@VatRate"/></td> <td> <xsl:value-of select="format-number(sum(../*[@VatRate=current()/@VatRate]/@VatAmount),'0.00')"/> </td> <td> <xsl:value-of select="format-number(sum(../*[@VatRate=current()/@VatRate]/@Brutto),'0.00')"/> </td> </tr> </xsl:if> </xsl:template>

Dość tych nowości w jednym przykładzie:

02/2.xsl 02/2.xml

Uważni czytelnicy dostrzegli nową pozycję w dokumencie. Wprowadziłem ją w celu pokazania jak zachowa się wartość tekstowa przy sortowaniu z data-type="number".

Nie wyciągajmy jednak z powyższego pochopnych wniosków. Oba wyrażenia: number('zw.')>number(0)

number('zw.')<number(0) zwracają false...

Drobna różnica pomiędzy przeglądarkami (a właściwie parserami): IE zbuntuje się, jeżeli nie znajdzie xml’a wstawionego przy pomocy document(), Mozilla nie. Można to sprawdzić zmieniając na chwilę nazwę pliku jednego ze słowników.

Posprzątajmy

W pierwotnej wersji pozycje słowników nazywały się Item. Ale taki element występuje w fakturze. Żeby nie robić zamieszania (bo nie było by to niepoprawne) zmieniłem na Elem. Do czynienia mamy jednak z dwoma dokumentami (na razie...), z których każdy prezentuje inną klasę zjawisk. Warto to zaznaczyć. Służą do tego przestrzenie nazw.

Przestrzeń nazw to nie to samo co reguły poprawności dokumentu! O ile dokument ma obowiązek być zgodny ze swoim DTD, o tyle nie ma wymagania zgodności z własną schemą. Zwłaszcza, że schema dla danej przestrzeni nazw może w ogóle nie istnieć – proszę spróbować znaleźć schemę xslt 1.0...

O teoretycznych szczegółach zjawiska nie będę się rozwodził – można poczytać na W3C. Dla naszych potrzeb wystarczy wiedzieć, że:

  1. <cos:Wezel xmlns:cos="konkretne-uri"/> to to samo co <Wezel xmlns="konkretne-uri"/>
  2. Atrybuty nie są przypisane do żadnej przestrzeni nazw dopóki nie zostanie to jawnie zadeklarowane prefiksem nawet, jeżeli znajdują się w węźle przypisanym do jakiejś przestrzeni.
  3. Prefiks jest aliasem odnoszącym się do pierwszej patrząc w górę drzewa dokumentu deklaracji przestrzeni a węzeł widziany jest najpierw jako <{jakies-uri}:NazwaWęzła ..., a dopiero później wnika się czy jego name() różni się od local-name().

name() to literalna nazwa węzła użytego w dokumencie. local-name() – jego nazwa bez prefiksu, Można wyobrazić sobie hipotetyczną sytuację, w której dopasowanie select="*[name()=ten:Tego'] wykona się w szablonie match="tamten:Tego – i będzie to poprawne, jeżeli prefiks ten: w dokumencie będzie wskazywał na tę samą przestrzeń nazw, co prefiks tamten: w transformacji.

Przystosowujemy szablon do pracy z przestrzeniami:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:doc="urn:szomiz:simple-invoice"
  xmlns:dct="urn:szomiz:simple-dictionary"
  exclude-result-prefixes="doc dct"

Dzięki exclude-result-prefixes unikamy wyrzucania na wyjście nieużywanych tam deklaracji. W przykładzie głównie w celu nieśmiecenia na wyjściu. Przy dokumentach mieszanych nierozważne wycięcie wszystkich prefiksów może się jednak zemścić wygenerowaniem znacznie większej niż to potrzebne objętości dokumentu.

>

Modyfikujemy istniejące szablony dodając tam gdzie to niezbędne prefiksy (tam gdzie używaliśmy nazw –gwiazdki oraz kropki będą działać dobrze):

<xsl:template match="doc:Item/@*" mode="head"...
<xsl:template match="doc:Item"><tr><xsl:apply-templates ...
<xsl:template match="doc:Positions"><table><thead ...
<xsl:template match="doc:Item/@*"><td> ...
<xsl:template match="doc:Item/@Price"><td> ...
<xsl:template match="doc:Item/@VatRate"><td> ...
<xsl:template match="doc:Item/@Name | Item/@Classification ...

Jeszcze trochę kosmetyki. W dokumencie zmieniłem nazwy atrybutów VatRate na vatRate – kolejna moja konwencja polegająca na tym, że elementy nazywane z małej litery przetwarzane są w bardziej zaawansowany sposób (a dokładniej rzecz ujmując: nie mogą być zmieniane przez aplikację w sposób dowolny).

Po raz kolejny zmieniamy sposób generowania zestawienia sprzedaży wg stawek VAT. Powtórka z document(). Wrzućmy do zewnętrznego pliku listę stawek:

02/vatRate.xml

Ustawiamy globalny parametr zawierający bieżący dokument (jak wejdziemy z przetwarzaniem do zewnętrznego przestaniemy widzieć węzły zasadniczego!):

<xsl:param name="main" select="/*"/> 
<xsl:template match="doc:Positions">
  <table>
    <thead><tr><xsl:apply-templates select="*[1]" ...
    <tbody><xsl:apply-templates select="*"/></tbody>
    <tfoot>
      <tr class="sum"><td colspan="5">...
      <xsl:apply-templates select="document('vatRate.xml')/*/*"/>

Od razu właściwe węzły – po drodze nic by się ciekawego nie działo.

</tfoot> </table> </xsl:template>

Nie tworzyłem globalnego parametru z tym dokumentem, ponieważ byłem pewien, że w transformacja wejdzie w powyższy szablon dokładnie jeden raz. Ale trzeba uważać, żeby nie wymuszać wielokrotnego ładowania tego samego (chociaż znając życie procesory to optymalizują – będę musiał kiedyś zepsuć jakąś transformację i popatrzeć w logi HTTPD).

<xsl:template match="dct:Elem">
  <xsl:if test="$main//doc:Item[@vatRate=current()/@name]">
    <tr>
      <td colspan="5">&#160;</td>
      <td>
        <xsl:value-of select="format-number(sum($main//doc:Item[@vatRate=current()/@name]/@Netto),'0.00')"/>
      </td>
      <td><xsl:value-of select="@vatRate"/></td>
      <td>
        <xsl:value-of select="format-number(sum($main//doc:Item[@vatRate=current()/@name]/@VatAmount),'0.00')"/>
      </td>
      <td>
        <xsl:value-of select="format-number(sum($main//doc:Item[@vatRate=current()/@name]/@Brutto),'0.00')"/>
      </td>
    </tr>  
  </xsl:if>
</xsl:template>

I mamy bardzo zgrabny i wydajny sposób generowania zestawienia.

Ktoś powie, że jest jeszcze <xsl:key – owszem jest, ale on nie bardzo chce działać w kontekście węzła innego dokumentu niż ten, w którym został zdefiniowany. Trzeba by było do z pozycji słownika wywołać jakiś element z faktury przekazując parametrem stawkę, co czyniłoby przykład nieczytelnym. Z czystego grupowania key’ami zrezygnowałem, bo to zdecydowanie nie temat na ten etap.

Trochę mało w trzecim pliku ;-) Zrobimy jeszcze wprowadzenie do kolejnego odcinka. W prosty sposób zmusimy HTML’a do posiadania informacji o tym, którego kawałka xml’a dotyczy:

<xsl:template match="*">

W szablonach rysujących HTML’a dodamy zmienną (szablon nazwany ze względu na oszczędność miejsca):

<xsl:variable name="xPath"> <xsl:call-template name="getPath"/> </xsl:variable>

Którą wstawimy do title tagów HTML’owych

<div class="inline" title="{$xPath}"> <span class="nodeName"> <xsl:call-template name="nameFromDict"/> </span>

w tych i jeszcze kilku innych miejscach.

<xsl:apply-templates select="@*"/> </div> <div class="content"><xsl:apply-templates select="*"/></div> </xsl:template>

W atrybutach zadziałać trzeba trochę inaczej:

<xsl:template match="@*"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/>

...ponieważ w szablonie nazwanym skorzystamy z osi. A osie są węzłów. A atrybut nie jest węzłem.

</xsl:variable> <span class="name" title="{$xPath}"> <xsl:call-template name="nameFromDict"/> <xsl:text>:</xsl:text> </span> <span class="value" title="{$xPath}"><xsl:value-of select="."/></span> </xsl:template> <xsl:template name="getPath"> <xsl:apply-templates select="ancestor-or-self::*" mode="pthElem"/> </xsl:template> <xsl:template match="*" mode="pthElem"> <xsl:text>/*[</xsl:text> <xsl:value-of select="count(preceding-sibling::*) + 1"/> <xsl:text>]</xsl:text> </xsl:template> <xsl:template match="@*" mode="pthElem"> <xsl:text>/@</xsl:text> <xsl:value-of select="name()"/> </xsl:template>

Ścieżki w tej postaci, bo takie zawsze będą działać. W każdym szablonie tworzone od nowa, bo przekazywanie jest upierdliwe (kto jeszcze pamięta, że przetwarzanie atrybutów przechodzi przez <xsl:template match="*" mode="itemAtrributes">?).

W prawdziwym życiu elementy powielarne posiadają jakieś unikalne identyfikatory. Wtedy można korzystać z nazw elementów i tych identyfikatorów.

Koniec.

02/3.xsl 02/3.xml

Pomachajmy myszką i pooglądajmy ścieżki.

Jeżeli potrafimy wrzucić je do title - potrafimy również do onclick’a. Skąd trzeba się dostać do xmla i czasami wrócić wynikiem transformacji konkretnego węzła do HTML’a....

I o tym będzie w następnym odcinku, pod tytułem „W te i nazad”

Jeszcze test na spostrzegawczość: dlaczego dwa elementy w ostatnim dokumencie są pomarańczowe?

Ładnie wygląda pierwszy odcinek w pokolorowanym HTML'u?
Pomóż przerobić bieżący!
Wdzięczność gwarantowana:-)
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 na 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 najmniej wykorzystanie i udostępnienie 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