UNIX 用戶十分熟悉 管道的思想 — 即控制一個程序的輸出以使它成為另一個程序的輸入的機制。管道或許位於模塊化松散耦合代碼的最前面的幾個主要示例之後。每個 UNIX 命令都非常簡單並且目標明確;將它們串在一起可以產生復雜操作。通過這種相同的模塊化,使用 XSLT 的 XML 處理會獲益良多。
通過將轉換分成一組單獨的階段或步驟,可以改進代碼的易用性並進行重用。遺憾的是,在純粹的 XSLT 1.0 中,大多數用於處理轉換輸入的命令都被禁止用於輸出。在 XSLT 2.0 中已經取消了這個限制,即使在 XSLT 1.0(已經使用了多年)中,也可以使用通常由 XSLT 供應商提供的擴展函數來取消這個限制。
要理解本篇技巧文章,您應該熟悉 XSLT。
剖析表
我有一個小的 XSLT 模板,用於獲得文檔表並且僅顯示每行中的第一項。設計它是為了與 DocBook 中使用的表(它們基於 SGML 中流行的表模型)一起工作。 清單 1(db-table.XML)顯示了樣本表。
清單 1. DocBook 形式的簡單表(db-table.XML)
<table frame="all">
<title>Numbers and tongues</title>
<tgroup cols="3" align="left" colsep="1" rowsep="1">
<thead>
<row>
<entry>1</entry>
<entry>2</entry>
<entry>3</entry>
</row>
</thead>
<tfoot>
<row>
<entry>I</entry>
<entry>II</entry>
<entry>III</entry>
</row>
</tfoot>
<tbody>
<row>
<entry>one</entry>
<entry>two</entry>
<entry>three</entry>
</row>
<row>
<entry>uno</entry>
<entry>DOS</entry>
<entry>tres</entry>
</row>
<row>
<entry>otu</entry>
<entry>abuo</entry>
<entry>ato</entry>
</row>
</tbody>
</tgroup>
</table>
清單 2(db-onecol.xslt)是一個僅呈現表的第一列的轉換。
清單 2. 用於呈現 DocBook 樣式的表的第一列的 XSLT 轉換(db-onecol.xslt)
<?XML version="1.0" encoding="utf-8"?>
<xsl:transform
XMLns:xsl="http://www.w3.org/1999/XSL/Transform"
XMLns:exslt="http://exslt.org/common"
version="1.0"
>
<xsl:output method="text"/>
<xsl:template match="table">
<xsl:value-of select="title"/><xsl:text>
</xsl:text>
<xsl:for-each select="tgroup/thead/row">
<xsl:value-of select="entry[1]"/><xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:for-each select="tgroup/tbody/row">
<xsl:value-of select="entry[1]"/><xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:for-each select="tgroup/tfoot/row">
<xsl:value-of select="entry[1]"/><xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:transform>
這輸出了簡單的文本。實體 是放在 xsl:text 中的換行,這樣就不會將它們作為空白從樣式表中除去。其余部分很簡單。當遇到表元素時,就輸出標題,後跟表頭、主體和頁腳行中的第一項。我沒有使用 tgroup/*/row 或類似的命令簡化成一個 xsl:for-each 循環,因為 thead 、 tbody 和 tfoot 元素可以以任何順序出現在文檔中,而我希望按照特定的順序處理它們。下面的會話演示了這種轉換是如何運行的:
$ 4xslt db-table.XML db-onecol.xslt
Numbers and tongues
1
one
uno
otu
I
表模型不匹配
現在,在 清單 3中我有一個 XHTML 樣式的表(xHtml-table.XML),我希望用同樣的方法處理它。
清單 3. XHTML 樣式的表(xHtml-table.XML)
<table border="1" frame="box">
<caption>Numbers and tongues</caption>
<thead>
<tr>
<th>1</th>
<th>2</th>
<th>3</th>
</tr>
</thead>
<tbody>
<tr>
<td>one</td>
<td>two</td>
<td>three</td>
</tr>
<tr>
<td>uno</td>
<td>DOS</td>
<td>tres</td>
</tr>
<tr>
<td>otu</td>
<td>abuo</td>
<td>ato</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>I</td>
<td>II</td>
<td>III</td>
</tr>
</tfoot>
</table>
因為這個表有不同的元素名稱和略微不同的組織結構,所以不能簡單地重用 DocBook 表模板。我可以復制該模板並做一些修改,以創建用於 XHTML 元素的特殊版本,但這不是一種很模塊化的方法。另一種方法是將 XHtml 轉換成 DocBook 形式,然後通過 DocBook 模板處理它;其優點在於,一旦做了轉換,我還可以重用其它用於 DocBook 表的工具。
清單 4(xhtml-onecol.xslt)是使用 DocBook 表模塊來對 XHtml 表進行操作的轉換。
清單 4. 用於呈現 XHTML 樣式的表的第一列的 XSLT 轉換(xHtml-onecol.xslt)
<?XML version="1.0" encoding="utf-8"?>
<xsl:transform
XMLns:xsl="http://www.w3.org/1999/XSL/Transform"
XMLns:exslt="http://exslt.org/common"
version="1.0"
>
<xsl:import href="db-onecol.xslt"/>
<xsl:template match="/">
<xsl:apply-templates mode="xHtml"/>
</xsl:template>
<xsl:template match="table" mode="xHtml">
<xsl:variable name="db-table">
<xsl:call-template name="xHtml-table-to-db"/>
</xsl:variable>
<xsl:apply-templates
select="exslt:node-set($db-table)/table"/>
</xsl:template>
<xsl:template name="xHtml-table-to-db">
<xsl:copy>
<title><xsl:value-of select="caption"/></title>
<tgroup cols="{count(thead/tr/th)}">
<thead>
<row>
<xsl:for-each select="thead/tr/th">
<entry><xsl:apply-templates/></entry>
</xsl:for-each>
</row>
</thead>
<tfoot>
<row>
<xsl:for-each select="tfoot/tr/td">
<entry><xsl:apply-templates/></entry>
</xsl:for-each>
</row>
</tfoot>
<tbody>
<xsl:for-each select="tbody/tr">
<row>
<xsl:for-each select="td">
<entry><xsl:apply-templates/></entry>
</xsl:for-each>
</row>
</xsl:for-each>
</tbody>
</tgroup>
</xsl:copy>
</xsl:template>
</xsl:transform>
一個要點:我故意簡化了這些示例,以便於著重討論主要問題。樣式表使用了 拉樣式的 XSLT(意味著經常使用 xsl:for-each 和 xsl:value-of ),而不是使用 推樣式(它使用許多模板和方式)。我這樣做是因為拉樣式更為大家普遍熟悉,雖然推樣式在許多方面比較優越。例如,在實際項目中,我要編寫用來將 XHTML 表轉換成 DocBook 的模板,以作為標識轉換的變體。此外,模板還需要非常多的邏輯來處理 XHtml 和 DocBook 表的常見情形。
多步驟技術的難題出現在下面的行中:
<xsl:apply-templates select="exslt:node-set($db-table)/table"/>
這是從一個階段到下一個階段的“切換(hand-off)”。在第一個步驟中,XHtml 表被轉換成變量 db-table 內部的 DocBook 形式。這創建了非常類似於 清單 1 中的輸出的結果樹片段。為了將它作為第二個步驟的輸入,必須將它從結果樹片段轉換成節點集,這就是 exslt:node-set 函數所做的工作。好幾種處理器都支持這個擴展函數,甚至不支持 EXSLT 擴展的處理器幾乎也總是提供它們自己專用的節點集擴展(工作方式是相同的)。
我從這個新的節點集選擇表元素以開始第二個步驟,在這個步驟中,來自導入的 db-onecol.xslt 模塊的表模板執行其工作。我使用方式( xhtml )來選擇 XHtml 表,這樣該模板就不會妨礙 DocBook 模板的操作,DocBook 模板具有相同的匹配,但其導入優先權較低。
這種轉換的輸出與對純粹的 DocBook 表進行轉換的輸出相同。正象我想要的那樣,我可以重用 DocBook 代碼。
結束語
該示例是對我在實際項目中所遇到情形作了大量簡化的結果。我需要對 XHTML 源文件重用許多 DocBook 處理模板。通過在第一步驟中將 XHtml 內容轉換成 DocBook,然後在後續的步驟中重用標准 DocBook 模板,我節省了大量工作和調試。與之相比,多步驟 XSLT 的思想甚至更全面。除了促進代碼重用,它還可以將復雜的轉換分成易於理解的程序塊。下次面臨 XSLT 中的復雜問題時,您可以決定是否將它簡化或模塊化成一系列管道化的操作。