headerbanner

Lekce XSLT transformace a XML Schema validace, aneb jak v PHP vytvořit menu z XML (6)

Po definici XML podkladu a šablony použijeme upravené původní xsd, xsl a php soubory. První je XML Schema pro validování podkladového XML, soubor allweb.xsd:

 
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" >
  <xs:element name="allweb">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="menu">
          <xs:complexType>
            <xs:sequence> 
              <xs:element name="menugroup" minOccurs="1" maxOccurs="unbounded" type="tmenugroup" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>   
          <!-- oproti původnímu souboru menu.xsd ze začátku seriálu
          jsou zde přidané elementy jsou společné pro všechny strany webu-->
        <xs:element name="inhead"  minOccurs="0" maxOccurs="1"/>
        <xs:element name="charset"  minOccurs="0" maxOccurs="1"/>
        <xs:element name="pageheader"  minOccurs="0" maxOccurs="1"/>
        <xs:element name="footer"  minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>       
 
  <xs:complexType name="tmenugroup">
    <xs:sequence>
       <xs:element name="menuitem" minOccurs="1" maxOccurs="unbounded" type="tmenuitem"/>
    </xs:sequence>
  </xs:complexType>
 
  <xs:complexType name="tmenuitem">
    <xs:sequence>
       <xs:element name="inanchor" />
       <xs:element name="url" minOccurs="0" maxOccurs="1" />
       <xs:element name="title" minOccurs="0" maxOccurs="1" />
       <xs:element name="desc" minOccurs="0" maxOccurs="1" />
       <xs:element name="key" minOccurs="0" maxOccurs="1" />
       <xs:element name="pagetype" minOccurs="0" maxOccurs="1" />
       <xs:element name="yesnoinmenu" minOccurs="0" maxOccurs="1" />
       <xs:element name="lang" minOccurs="0" maxOccurs="1" />
       <xs:element name="menugroup" minOccurs="0" maxOccurs="unbounded" type="tmenugroup" />
    </xs:sequence>
  </xs:complexType>
 </xs:schema>
 

K tomu asi není vysvětlení potřeba. Dalším souborem bude šablona pro získání menu, soubor allmenu.xsl. Změny jsou vyznačeny:

 
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" version="1.0" encoding="utf-8" />
 
<xsl:strip-space  elements="*"/>
 
<!--zachytí kořen (ne uzel, ale kořen - tedy před prvním uzlem dokumentu) -->
<xsl:template match="/" >
 
    <!--když existují nějaké položky menu, pracuj, jinak nedělej nic -->
    <!--//////////////////////////////////////////////////////// -->
    <!--ZMĚNA OPROTI PŮVODNÍMU - MENU JE AŽ PO ELEMENTU ALLWEB -->
    <!--//////////////////////////////////////////////////////// -->
    <xsl:if test="/allweb/menu/menugroup/menuitem/*">
      <!--hledej template pro menu -->
      <xsl:apply-templates />
    </xsl:if> 
</xsl:template>
<xsl:template match="allweb" >
    <xsl:apply-templates /> 
</xsl:template>  
<xsl:template match="menu" >      
    <!--<menu> vypíše startovací tag pro HTML 5 (uzavře do tagu celé menu) -->
    <!--hledej template pro dítě menu, tedy pro menugroup -->      
    <xsl:apply-templates >
    <xsl:with-param name="level" select="0" />
    </xsl:apply-templates >
    <!--</menu>uzavře menu (díky předchozímu apply-templates se toto vypíše až po projití celého stromu xml dokumentu)  -->
</xsl:template>
 
<xsl:template match="menugroup" >  
  <xsl:param name="level" /> 
  <ul> <!--vypíše začátek menu nebo podmenu - vždy uzavřeno v ul -->
  <!--následné apply-templates bude hledat template pro dítě menugroup, tedy pro menuitem -->
  <!--pokud bychom potřebovali pomocí tříd označit i úroveň menu,-->
  <!--pomohl by nám parametr level, který ovšem nakonec nepoužiji,-->
  <!--je zde jen pro ukázku vytvoření a předávání parametrů -->
  <xsl:apply-templates >
    <!--nyní bude parametr level definován v těle elementu with-param, -->
    <!--a to pomocí xsl:value-of, -->
    <!--v jednodušších případech lze definovat v atributu select -->
    <xsl:with-param name="level"> 
       <xsl:choose>
          <!--když je otcem tohoto elementu element menu, -->
          <!-- jde o nultou úroveň, jinak zvyš o jedničku -->
          <xsl:when test="name(..)='menu'">
            <xsl:value-of select="0"/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="$level+1"/>
          </xsl:otherwise>
      </xsl:choose>
    </xsl:with-param>
   </xsl:apply-templates ><!--v tomto okamžiku aplikuj další template -->
  </ul>
</xsl:template>
 
<xsl:template match="menuitem" >
  <!--parametr předaný z vyšší úrovně musí být deklarovaný na začátku template -->
  <xsl:param name="level"/>
  <!--hledej template pro elementy uvnitř elementu menuitem -->
  <!--taková template bude nalezena jen pro inanchor -->
  <!--pro ostatní nalezne obecnou template *, která nevypíše nic -->
  <!--elementy url, title a ostatní nebudou tedy vypisovány -->
  <xsl:apply-templates >
    <!--předej dále úroveň menu (ve výstupu nakonec není použita, je to jen ukázka) -->
    <xsl:with-param name="level" select="$level" />
    <!--předej dále parametr označující že to není první položka menu ve skupině menugroup -->
    <!--parametr obsahuje předchozí menuitem ve stejné menugroup, takže když jde o první menuitem -->
    <!--zůstane prázdný a pak ho můžeme testovat - pro prázdný vložíme class="first"-->
    <xsl:with-param name="nofirst" select="preceding-sibling::menuitem[1]" />
    <!--obdoba předchozího, je prázdný když jde o poslední menuitem-->
    <xsl:with-param name="nolast" select="following-sibling::menuitem[1]" />
  </xsl:apply-templates >
</xsl:template>
 
<!--nejdůležitější šablona-->
<!--položka inanchor zvolena proto, že nesmí být prázdná-->
<!--ostatní (title atd.) mohou úplně chybět,-->
<!-- pak je tento element menuitem jen oddělovačem, nikam neodkazuje  -->
<xsl:template match="inanchor" >
  <xsl:param name="level" />  <!--nevyužívá se  -->
  <xsl:param name="nofirst"/>
  <xsl:param name="nolast"/>
  <li> <!--otvírací značka - bude vždy, i když uvnitř nemusí být tag A  -->
    <!--když jsem v první menuitem ve skupině menugroup, nastavím class (může se hodin pro design) -->
    <xsl:if test="not($nofirst)">  
      <xsl:attribute name="class">
        <xsl:value-of select="'first'" />
      </xsl:attribute>
    </xsl:if>  
    <!--když jsem v poslední menuitem ve skupině menugroup, nastavím class (může se hodin pro design)  -->
    <xsl:if test="not($nolast)">  
      <xsl:attribute name="class">
        <xsl:value-of select="'last'" />
      </xsl:attribute>
    </xsl:if>
 
    <!--//////////////////////////////////////////////////////// -->
    <!--ZMĚNA OPROTI PŮVODNÍMU - PARAMETR ID -->
    <!--//////////////////////////////////////////////////////// -->
    <!--když jde o url zrovna zobrazované stránky, přidej id="current"  -->
     <xsl:if test="normalize-space(../url)=$pageurl">  
      <xsl:attribute name="id">
        <xsl:value-of select="'current'" />
      </xsl:attribute>
    </xsl:if>   
    <!-- obsah LI podle toho, zda má obsahovat A nebo ne -->
    <xsl:choose>
      <!-- když v je menuitem neprázdný element url -->
      <xsl:when test="normalize-space(../url)">
        <a> <!-- počáteční značka tagu A -->      
           <xsl:attribute name="href">
            <!-- obsah atributu href, k němu přidána koncovka .html -->
            <xsl:value-of select="../url" /><xsl:value-of select="'.html'" />
          </xsl:attribute>
          <!-- hledej template pro dítě elementu inanchor  -->
          <!-- ale jelikož dítětem je pouze text, vypíše tento text  -->
          <xsl:apply-templates />  
        </a><!-- uzavři tag -->
      </xsl:when>
      <!-- v opačných případech, když je url prázdné - bude to neodkazující oddělovač -->
      <xsl:otherwise>
          <!-- hledej template pro dítě elementu inanchor  -->
          <!-- ale jelikož dítětem je pouze textový obsah, nehledá šablonu a vypíše tento text  -->
          <xsl:apply-templates />
      </xsl:otherwise>
    </xsl:choose>
  </li><!-- uzavři tag LI -->
</xsl:template>
 
<!-- pokud by neexistovala následující šablona pro všechny elementy, -->
<!-- které nemají vlastní šablonu (respektive nejsou zachyceny pomocí match=...) -->
<!-- pak by se vypsaly obsahy těchto elementů -->
<!-- například TITLE, DESC atd. -->
<xsl:template match="*" />
</xsl:stylesheet > 
 

Potom potřebujeme soubor pro získání obsahu elementů url podkladového XML spouboru, abychom mohli generovat všechny strany v cyklu. Je to soubor allurls.xsl:

 
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" version="1.0" encoding="utf-8" /> 
 
<xsl:strip-space  elements="*"/>         
 
<xsl:template match="/" >
    <xsl:for-each select ="//menuitem[url/text() and not(url=preceding::url)]">
     <xsl:if test="url[text() and not(url=preceding::url)]"> 
 
      <!-- do výstupu vlož obsah url, příponu, znak |, jazyk a za to znak ~  
      tedy například nejaka-strana.html|cs~ -->
      <xsl:choose>
          <xsl:when test="pagetype and pagetype/text()">
            <xsl:value-of select = "concat(url,'.',pagetype/text(),'|')" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select = "concat(url,'.html','|')" />
          </xsl:otherwise>
      </xsl:choose>
       <xsl:choose>
          <xsl:when test="lang and lang/text()">
            <xsl:value-of select = "concat(lang/text(),'~')" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select = "'cs~'" />
          </xsl:otherwise>
      </xsl:choose>
     </xsl:if>
    </xsl:for-each>
</xsl:template> 
</xsl:stylesheet > 
 

Dalším souborem je allforpages.xsl, který generuje tagy pro sekci head:

 
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" version="1.0" encoding="utf-8" />
 
<!-- generuje výstup bez zbytečných mezer -->   
<xsl:strip-space  elements="*"/>          
 
<!-- proměnná pro znak nového řádku  -->
<xsl:variable name='nl'><xsl:text>
</xsl:text></xsl:variable>
 
 
<xsl:template match="/">
  <xsl:for-each select ="//menuitem[url=$pageurl and not(url=preceding::url)]">
      <xsl:value-of select = "$nl" />
      <!-- 
      META tag charset musí být v prvních 1024 bytech HTML dokumentu, proto
      ho zapíšeme jako první - viz:
      http://www.w3.org/International/questions/qa-html-encoding-declarations
       -->
      <xsl:value-of select = "concat('&#60;meta charset=&#34;',//charset,'&#34; &#62;')" disable-output-escaping="yes" />
       <xsl:value-of select = "$nl" />
      <title><xsl:value-of select = "title"/></title>        
      <xsl:value-of select = "$nl" />
      <xsl:value-of select = "'&#60;meta name=&#34;description&#34; content=&#34;'" disable-output-escaping="yes" />
      <xsl:value-of select = "concat(desc,'&#34;&#62;',$nl)" disable-output-escaping="yes" />
 
      <xsl:value-of select = "'&#60;meta name=&#34;keywords&#34; content=&#34;'" disable-output-escaping="yes" />
      <xsl:value-of select = "concat(key,'&#34;&#62;',$nl)" disable-output-escaping="yes" />
 
      <!-- nyní navíc oproti předchozímu xsl zapíše společné elementy hlavičky (CSS atd.) -->
      <xsl:value-of select = "//inhead" disable-output-escaping="yes" />  
 </xsl:for-each>
</xsl:template >
</xsl:stylesheet>
 

Všimněte si, že kromě TITLE nepoužíváme přímo zápis meta tagů . Tyto tagy totiž v HTML narozdíl od XHTML nemusí být uzavřené. Pokud bychom takový neuzavřený tag zapsali mimo element se jmenným prostorem xsl, pak by se z transformační šablony stal nevalidní XML dokument, což není přípustné. Na druhou stranu - podle definice HTML 5 můžeme tagy klidně uzavírat a vytvořit "XHTML 5" - ale to už je na jiný článek.

Posledním souborem je PHP skript, který provede nejprve validaci, pak zajistí transformaci a nakonec uloží výsledky. Skript skeletoncreator.php spolu se všemi ostatními potřebnými soubory najdete zde. Soubory jsou trochu přehledněji uspořádané, celou strukturu můžete rozbalit do nějakého adresáře vašeho lokálního serveru Apache. Příště se podíváme na spuštění.