Ajánlás
eNeL makróidomítási okosságai után újabb technológiaközeli írással igyekszünk borzolni a kedélyeket. Fogadjátok sok szeretettel Ignis_veneficus barátunkat, az ipari szövegtranszformáció feketeöves mesterét, a TEI és az XML avatott kezű varázslóját. Az írás nagyon száraz, ezt előre mondom, de van, amikor ez kell. A prc fizikai tartalomjegyzékkel való kiegészítésének támogatásáért mindenesetre örökké hálásak leszünk neki (is).
Aki nem érti, ne pánikoljon, nem vele van a baj, a megfejtés ott a poszt végén egy hasonló dobozban :-)
Dworkyll
Intro
Az XSLT az XSL Transformation rövidítése, vagyis (pongyolán fogalmazva) XML transzformáció, amivel egy XML-ből másik XML-t lehet előállítani. Egy teljes programozási nyelv.
Mivel az e-könyv generálása során XHTML-t állítunk elő (mind az epub, mind a mobi generáló részére), és a hozzá tartozó egyéb leíró fileok (opf és ncx) szintén XML, adja magát, hogy az XSLT-t felhasználjuk ezek előállítására, és hasonló kis sriptek írhatóak benne szinte percek alatt.
A továbbiakban nagyon szakmai, programozói lesz.
Hozzávalók
- Bemeneti XHTML
- XSLT processor (ami futtatni tudja az XSLT-t), én a java-s xalan-t használom (gyors, egyszerű, parancssorból futtaható, és integrálható java programokba)
- XML editor. (használható helyette egyszerű TXT editor is. pl notepad, Jedit, stb)
Példaprogram: mbp_toc -> ncx
A következő XSLT a előző postban vázolt problémára ad megoldást, vagyis, hogyan csináljunk a Mobipocket Creator által létrehozott tartalomjegyzékből ncx-et.
A kód
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<xsl:output method="XML" version="1.0" encoding="UTF-16" indent="yes" doctype-public="-//NISO//DTD ncx 2005-1//EN"
doctype-system="http://www.daisy.org/z3986/2005/ncx-2005-1.dtd"/>
<xsl:template match="/html">
<ncx>
<head>
<meta name="dtb:uid" content=""/>
<meta name="dtb:depth">
<xsl:attribute name="content">
<xsl:call-template name="getDeep"/>
</xsl:attribute>
</meta>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<navMap>
<xsl:apply-templates select="body/ul"/>
</navMap>
</ncx>
</xsl:template>
<xsl:template match="ul">
<xsl:apply-templates select="li"/>
</xsl:template>
<xsl:template match="li">
<navPoint id="{generate-id(.)}">
<xsl:attribute name="playOrder">
<xsl:number level="any"/>
</xsl:attribute>
<navLabel>
<text><xsl:apply-templates select="a"/></text>
</navLabel>
<content>
<xsl:attribute name="src">
<xsl:call-template name="getFileName">
<xsl:with-param name="filename" select="a/@href"/>
</xsl:call-template>
</xsl:attribute>
</content>
<xsl:apply-templates select="following-sibling::*[position()=1 and local-name()='ul']"/>
</navPoint>
</xsl:template>
<xsl:template name="getDeep">
<xsl:for-each select="//li">
<xsl:sort select="count(ancestor::ul)" data-type="number"/>
<xsl:if test="position()=last()">
<xsl:value-of select="count(ancestor::ul)"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template name="getFileName">
<xsl:param name="filename"/>
<xsl:variable name="new">
<xsl:choose>
<xsl:when test="contains($filename,'%5C')">
<xsl:value-of select="substring-after($filename,'%5C')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring-after($filename,'/')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:when test="$new and $new!=''">
<xsl:call-template name="getFileName">
<xsl:with-param name="filename" select="$new"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filename"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Az XML jól megkülönböztethető részekből áll:
- Fejléc
- XSLT definíció
- Output definíció
- "html" elem feldolgozása
- "ul" elem feldolgozása
- "li" elem feldolgozása
- "getDeep" "függvény"
- "getFileName" "függvény"
Fejléc és XSLT definíció
A fejléc és az XSLT definíció adott. Ez minden esetben szinte ugyanígy néz ki. Amiben eltérés lehet, az a alapértelmezett névtér a:
xmlns="http://www.daisy.org/z3986/2005/ncx/
(névtér: egy XMl file több XML struktúrából is tartalmazhat elemet, és valahogy meg kell különböztetni egymástól, hogy melyik elem melyik struktúra része. A névtér egy előtagot definiál. A példánkba két névtér van, az alapértelmezett /ez jelenleg most az ncx-hez tartozik/ és egy xsl: kezdetű, ami az XSLT struktúra része)
Kimenet definíció
A kimeneti formátum definiálja a kimeneti XML header részét. Jelen esetben egy utf-16 karakterkódolást, és az NCX struktúrát, és így fog kinézni:
<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
Elemek feldolgozása
A következő három rész különböző elemek feldolgozását végzi el.
Az XSLT-ben egy elem feldolgozást a
<xsl:template match="...">
vezeti be. A match attributumban levő feltétel esetén fut le. Jelenleg három ilyen feltételt készítettünk:
- /html: A gyökér HTML elem feldolgozása
- ul: UL elem feldolgozása
- li: LI elem feldolgozása
"ul" elem
Most felrúgva a kódban szereplő sorrendet, először a "ul" feldolgozását nézzük meg.
Egy sort tartalmaz:
<xsl:apply-templates select="li"/>
Értelmezése: dolgozzon fel minden olyan "li" elemet ami közvetlenül az "ul" elemben található.
"html" elem
A gyökér HTML feldolgozása:
A feldolgozás során elkezdjük előállítani a kimeneti XML-t
(ezek azok a elemek, amelyek nem xsl:-lel kezdődnek)
Az első lényeges dolog a
<xsl:attribute name="content">
rész. Ez létrehoz egy attribútumot az előző elemhez (ami dtb:depth), a "content" névvel, és meghívunk egy "függvényt", melynek a neve "getDeep" (erről később, tulajdonképpen kiszámolja a tartalomjegyzék mélységét)
<xsl:call-template name="getDeep"/>
</xsl:attribute>
Ebben a részben még egy lényeges dolog van
<xsl:apply-templates select="body/ul"/>
ami nem csinal mást, csak feldolgozza az összes "body" elemben lévő "ul" elemet (az elérés relatív, de ha a path előtt "/" van, lehet abszolút elérést is megadni)
"li" feldolgozása
Egyik leghúzósabb rész, a li feldolgozása következik. Ez készíti el a fejezet bejegyzéseket a tartalomjegyzékbe. Itt is a kimeneti XMLT-t állítjuk elő.
Első pontunk a
<navPoint id="{generate-id(.)}">
ami egy "navPoint" elemet hoz létre, egy "id" attribútummal. Az "id" értéke számított értéket tartalmaz (ezt mutatja benne a kapcsos-zárójel), mégpedig egy egyedi azonosító generálást az aktuális elemből. (generate-id() függvény)
A következő rész szintén egy attribútumot ad hozzá:
<xsl:attribute name="playOrder">
A "playOrder" attribútumot. Az értéke egy másik XSLT elem, amely a kérdéses elem ("li") sorszámát adja meg, mivel a "level" attribútum "any", a teljes dokumentumon belül.
<xsl:number level="any"/>
</xsl:attribute>
Ezt követi a "content" elem "src" attribútumának beállítása:
<xsl:attribute name="src">
Ami szintén egy függvény hívás, de ennek már átadunk egy paramétert is: a "li" elemben lévő "a" elem "href" attribútumát. (a függvény megpróbálja az elérési útból a tényleges fájlnevet kiszedni)
<xsl:call-template name="getFileName">
<xsl:with-param name="filename" select="a/@href"/>
</xsl:call-template>
</xsl:attribute>
<xsl:apply-templates select="following-sibling::*[position()=1 and local-name()='ul']"/>
Itt meghívjuk feldolgozásra az "li" elemet közvetlenül követő "ul" elemet.
A "following-sibling::" definiálja, hogy a következő elemekkel foglalkozunk, a "*", hogy nem érdekel a típusuk (ha itt megadtuk volna hogy "ul", de akkor a pozicíótól függetlenül minden "ul" a listába került volna), de az első legyen és a típusa legyen ul
( a különbség:
following-sibling:ul[position()=1] : a következő ul elemek közül az első
following-sibling:*[position()=1 and local-name='ul'] a következő elemek közül az elsőt, ha ul a neve)
(a following-sibling:: szerkezet neve tengely, vagy axis)
Függvények
Ezek olyan részek, melyeket máshonnan lehet meghívni, mint egy függvényt.
getDeep
A kettő közül az egyszerűbb: megszámolja a tartalomjegyzék szintjeit.
<xsl:for-each select="//li">
Végigmegy az összes "li" elemen (a "//" előtte azt jelenti, hogy az aktuális pozíciótól függetlenül, minden "li" elemre hajtsa végre)
<xsl:sort select="count(ancestor::ul)" data-type="number"/>
<xsl:if test="position()=last()">
<xsl:value-of select="count(ancestor::ul)"/>
</xsl:if>
</xsl:for-each>
<xsl:sort select="count(ancestor::ul)" data-type="number"/>
Sor sorrendezi az elemeket (itt most az összes "li" elemet), mint számokat (ez a data-type), az a "select" attribútumban adjuk meg, hogy mi alapján.
Jelen estben egy összetett műveletet hajtunk végre. Megszámoltatjuk a felmenő "ul" elemeket.
(minden elemnek van egy szülő eleme, itt a szülő elem szülő elemet is vizsgáljuk egészen a kezdeti html elemig. az "ul" elemek száma meghatározza, hogy a tartalomjegyzék melyik szintjén található a vizsgált "li" elem)
(Az "ancestor::" kifejezést tengelynek, axis-nak hívja a szakirodalom)
<xsl:if test="position()=last()">
Egy másik programvezérlési utasítás: feltétel. Minden esetben a "test"-ben leírtak kerülnek kiértékelésre (else nincs, azt máshogy kell megoldani, látunk rá példát). Itt az adott elem pozícióját (sorszámát) hasonlítjuk össze az összesével (ez a last). Ezzel biztosítjuk, hogy azt az elemet vizsgáljuk, aki a tartalomjegyzék legalján áll.
<xsl:value-of select="count(ancestor::ul)"/>
Szimpla érték kiolvasás. A felmenő "ul" elemek számát adjuk vissza
A függvény visszatérési értéke szimplán írunk a "kimeneti file"-ba
getFileName függvény
A függvény tipikus példája annak, hogy mire nem jó a XSLT: A file path-át kellene levágni. Ez tetszőleges programozási nyelven kb. 2 sor.
<xsl:param name="filename"/>
Definiáljuk, hogy kaphatunk paramétert a filename név alatt.
<xsl:variable name="new">
Létrehozunk egy új változót "new" néven. (az, hogy változó, kicsit meredek, mert csak egyszer kaphat értéket, később nem változhat.)
<xsl:choose>
Szerkezet a ellenőrzi a "when" "test"-ben definiált feltételt, és csak és kizárólag az első igaz értékűt hajtja végre.
<xsl:when test="">
Ez kiegészülhet egy "xsl:otherwise"-szal, ami a "minden más esetben". (egy when-otherwise-szal lehet megoldani egy if-else serkezetet)
<xsl:when test="contains($filename,'%5C')">
Első feltétel, hogy a filename praméter tartalma-e "%5C" karaktersorozatot (html-es /jel kódolás)
amennyiben igen
<xsl:value-of select="substring-after($filename,'%5C')"/>
Levágjuk ami utána következik (ez lesz a "new" változó értéke)
Az otherwise részben ugyanezt végezzük el, de itt már a rendes "/" karakterrel
Az értékadás után vizsgáljuk a változó értékét
<xsl:when test="$new and $new!=''">
Ha van tartalma, akkor talált valamit a fenti két karakter(sorozat) után.
<xsl:call-template name="getFileName">
Meghívjuk önmagát, de itt más az új változóval, mint paraméter.
<xsl:with-param name="filename" select="$new"/>
</xsl:call-template>
Mivel az XSLT-ben nincs hagyományos ciklus (for, while), ilyen problémákat csak rekurzióval lehet megoldani. (az meg gyorsan stack-owerflow hibát dobhat)
Amennyiben nincs adat az uj változóban:
<xsl:value-of select="$filename"/>
Visszaadjuk a paramétert (ebben az esetben már csak a filenevet tartalmazza)
Verdikt
Az XSLT bizonyos feladatokat nagyon leegyszerűsít, és sok problémát levesz az egyszerű script-készítő válláról (nem kell a XML szabvánnyal foglalkozni, pl. karakterkódolás, space-kezelés, elemek végének keresése stb)
Nagyon egyszerűen lehet benne elemeket/attributumok megcímezni. Pl. az összes link url megfogása XSLT-ben egy kifejezés:
//a/@url
vagy pl: keressük az összes olyan elemet, ami három elemet tartalmaz, ebből az első "p" és az nem tartalmaz "i"-t:
//*[count(child::*)=3 and p[location()=1 and not(child::i)]]
És akkor mindenki elgondolkodhat, hogy a kedvenc script/programozási nyelvében hogyan tudná megoldani.
Mire használható
Minden olyan esetben, amikor az elemekkel történnek műveletek, áthelyezés, törlés, létrehozás, másolás stb. Amikor nem a text tartalommal kell dolgozni.
Mire nem használható
Minden olyan esetben amikor a elemek/attribútumok szöveges tartalmával kell dolgozni. pl Regexp-es find-replace-re nem ajánlott.
Bináris adatokkal történő műveletre nem ajánlott.
(a fenti getFileName függvény majdnem határeset. A kód mennyiségéből is látszik (majdnem a leghosszabb rész), hogy egy viszonylag egyszerű feladatot milyen nehéz megoldani.
Két link
Két link, amelyből többet lehet megtudni:
- XSLT leírás (XSLT elemek definíciója, használata, leírása)
- Xpath leírás (Xpath az elemek címzése, axis-ok leírása, függvények definícóját tartalmazza)
Oké,akkor aki magamfajta hályogkovács, és elméleti tudás nélkül csak használni akarja Ignis megoldását, kövesse az alábbi lépéseket:
- töltsd le a innen a xalan utolsó „binary release”-ét
- csomagold ki egy szívednek kedves helyre
- tedd melléjük az alábbi xalan.bat file-t (.bat-ra visszanevezve). Ez fogja majd az xsl file-t meghajtani az imént letöltött java futtatókörnyezeten
- tedd melléjük az alábbi xsl-t. Ez végzi a transzformációt magát. Ha akarod, ezen már bütykölhetsz magad is.
- Töltsd le ezt a toc_creator.bat file-t. Ezt, a belső, xalan.bat-ot és xsl-t elérő útvonalak pontosítása után (ezt elég egyszer megcsinálni, mert úgysem fogod a xalan készletet hurcolászni) mindig oda kell másolni a publikációs alkönyvtárakba, ahol az adott mbp_toc.html van. Ha ezt futtatod, ez fogja megcsinálni magát a toc.ncx-et.
- Ezt a vbscriptet eNeL anyagából vágtam, a kész toc.ncx hivatkozását ez szúrja be az opf-be. Ezt is mindig a publikációs könyvtárba kell bemásolni, a txt kiterjesztést meg idejekorán levágni.
- Ha az új opf alapján buildeled meg a publikációt, akkor abban már benne lesz a toc.ncx is. A Mobi desktop kijelzi a fejezetcímet, és Kindle tud fejezetenként lapozni. A legfölső szintű töréspontok meg ott lesznek a progress bar-on.
- Gondolj hálás szívvel Ignisre és eNeLre :-)
Dworkyll
Update #1
A kindle féle változat, amelyben minden egy szinten van:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<xsl:output method="XML" version="1.0" encoding="UTF-16" indent="yes" doctype-public="-//NISO//DTD ncx 2005-1//EN"
doctype-system="http://www.daisy.org/z3986/2005/ncx-2005-1.dtd"/>
<xsl:template match="/html">
<ncx>
<head>
<meta name="dtb:uid" content=""/>
<meta name="dtb:depth" content="1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle>
<text><xsl:apply-templates select="title"></xsl:apply-templates></text>
</docTitle>
<navMap>
<xsl:apply-templates select="//li"/>
</navMap>
</ncx>
</xsl:template>
<xsl:template match="li">
<navPoint id="{generate-id(.)}">
<xsl:attribute name="playOrder">
<xsl:number level="any"/>
</xsl:attribute>
<navLabel>
<text><xsl:apply-templates select="a"/></text>
</navLabel>
<content>
<xsl:attribute name="src">
<xsl:call-template name="getFileName">
<xsl:with-param name="filename" select="a/@href"/>
</xsl:call-template>
</xsl:attribute>
</content>
</navPoint>
</xsl:template>
<xsl:template name="getFileName">
<xsl:param name="filename"/>
<xsl:variable name="new">
<xsl:choose>
<xsl:when test="contains($filename,'%5C')">
<xsl:value-of select="substring-after($filename,'%5C')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring-after($filename,'/')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:when test="$new and $new!=''">
<xsl:call-template name="getFileName">
<xsl:with-param name="filename" select="$new"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filename"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Az utolsó 100 komment: