作為 apache 的開放源碼 Xalan 處理器的貢獻者之一,我對開發人員對 XSLT 的廣泛應用有著深刻的印象。樣式表已經確立了自身作為一種非常通用的工具的地位,它的作用不只是呈現 XML 文檔以進行顯示,還可以自動生成新文檔。
當然,應用廣泛意味著人們要不斷推陳出新,提供一些 XSLT 不能完全為他們提供的功能,Xalan 團隊常常在繼續堅持可移植解決方案的願望和希望滿足這些需要之間左右為難。
XSLT 確實具有 擴展(請參閱 參考資料)的概念,擴展提供了一種增強樣式表語言的體系結構化的方法。利用該擴展,Xalan 開發人員可以提供一些額外的功能,而不會與標准產生沖突。但我們確實不能負擔將每個請求的擴展都直接構建到處理器中的任務;那將會形成一個有許多不常用功能的巨型庫。
Xalan 確實允許您編寫和插入自己的擴展。但是擴展通常僅限於定義新的樣式表操作,而不是修改現有的操作,並且通常需要人們用傳統的編程語言編寫代碼。(XSLT 的將來版本可能會允許您使用 XSLT 語言編寫擴展。)此外,並不是所有 XSLT 處理器都支持用戶編寫的擴展,並且編寫、訪問和調用這些擴展的詳細情形也各不相同,所以這不是一個可移植性很強的解決方案。(例如,為基於 Java 的 Xalan-J 處理器編寫的擴展就不能從 C++ 版本的處理器 Xalan-C 調用,反過來也是如此 — 請參閱 參考資料以獲取到 Xalan 的鏈接。)
在這兩篇文章中,我將為您展示增強 XSLT 樣式表的另一種方法,它可以完成擴展不能完成的一些工作,而且它在任何 XSLT 處理器中都可以工作:編寫一個將定制功能 編譯到其它樣式表中的樣式表!我們基本上可以利用 XSLT 樣式表本身就是 XML 文檔這一事實,並且可以自動應用一組修改,以添加或修改其行為。
令人啟發的用例:“為什麼我得到了那樣的輸出呢?”
下面是一個實際的示例:Stefan Kost 是一位 Xalan-J 用戶,提交了以下增強請求:
您認為向 xsl:output 添加一個屬性(例如 xalan:debug )怎麼樣,該屬性將使所有調用向產生的樹中發出一條注釋:
XML:
<testtag/>
xsl:
...
<xsl:template match="testtag">
<h1>tralala</h1>
</xsl:template>
generated Html:
<!-- testtag:beg -->
<h1>tralala</h1>
<!-- testtag:end -->
當然,我們要問的第一個問題是:用戶是否確定自己需要該行為。Xalan 已經內置了一些調試功能,這些功能可以告訴您樣式表正在做什麼。例如,在 Java 版本的 Xalan(我對它比較熟悉)中,您可以編寫並插入 TraceListener 對象,在樣式表執行時,將告知該對象 Xalan 正在做什麼。
Xalan 的命令行工具 org.apache.xalan.xslt.Process 中內置了一個基本的 TraceListener ,用於支持一組有用的命令行選項:
-TT (跟蹤模板,Trace Template) ,每當 Xalan 開始處理新模板時就生成一條消息,該消息告知您模板的匹配模式、模板名稱(如果它有名稱的話)及其方式(同樣,如果指定了方式)。這讓您可以基本了解樣式表的執行流程。該消息是用與 <xsl:message> 相同的格式寫到屏幕的,其中包括有關該模板是在哪個樣式表文件中發現的,以及位於該文件什麼地方的信息,以便於您可以在編輯器中調用樣式表,並確切地了解具體是哪個模板以及它試圖做什麼。(這裡我應當說明的是,該位置信息並非始終都是可用的,這取決於 Xalan 從哪裡裝入樣式表。)
-TTC (跟蹤模板的子模板,Trace Template Children) ,為模板中的每個處理步驟生成一條消息,從而允許您更詳細地查看樣式表的執行。跟 -TT 選項一樣,該選項提供了文件/行/列信息。
-TS (跟蹤選擇,Trace Selection) ,每當樣式表偽指令執行 select 操作以從源文檔檢索數據時就生成一條消息。它告訴您檢索是在樣式表中的什麼地方進行的(文件/行/列)、執行了哪種檢索以及選擇的 XPath 是什麼,並且列出由本次搜索返回的源文檔節點。匹配節點的列表通常是相當簡潔的,只包括節點名和節點的內部句柄。您可以通過添加 -L 選項來改善這一點,添加 -L 選項還可以跟蹤源文檔的位置信息,但是該數據並非始終都可用,並且使用 -L 選項確實會消耗額外的內存。
-TG (跟蹤生成,Trace Generation) ,每當樣式表在輸出文檔中生成(寫入)內容時就生成一條消息。把該消息看作 Xalan 的輸出 SAX 流的摘要。
顯然,如果願意,您可以組合這些跟蹤選項,或者您可以編寫自己的 TraceListener 以生成更復雜的日志記錄 — 甚至可能使用跟蹤事件來促使樣式表調試器具備動畫執行、斷點和性能分析功能。
但是 Stefan 對這些解決方案都不是完全滿意。編寫好的 TraceListener 需要花費一些功夫,而且顯然它不能移植到其它 XSLT 處理器。Xalan-J 跟蹤選項也是不可移植的,它們的輸出略顯冗長,並且通過消息很難確切地了解某部分輸出是從哪裡產生的。Stefan 確實希望能使跟蹤信息並入樣式表的輸出,這樣他就可以在生成的文檔的上下文中直接檢查跟蹤信息。
遺憾的是,他所建議的將非標准的偽指令添加到 <xsl:output> 的解決方案也是不可移植的,並且會使 Xalan 代碼更復雜。一般來說,給處理器的基本行為添加功能是不可取的,除非將大量使用這些功能;每次您都必須決定是否進行某些工作,它損失了一些性能、增加了出錯的風險,並且(當然)需要更多的用戶內存用於額外的代碼。
Stefan 可能只是重寫了他的樣式表,以便於它在每個模板開始執行和結束執行時生成一些注釋。但是我必須承認他的做法是值得商榷的,特別是如果您只希望在設法調試某個問題時才添加跟蹤信息。Stefan 希望的是一種在他需要時 自動將該行為添加到每個模板的方法。
唔。 自動將行為添加到每個模板元素— 聽上去象是樣式表可以做的事!如果您編寫這樣一個樣式表,那麼您可以確切地控制將注釋添加到哪裡以及注釋中的內容,而不是聽從於其他人有關哪些信息是有用的猜測。
樣式化樣式表的基礎知識
首先我將創建一個樣本樣式表,對其應用跟蹤程序。下面是一個根據 Stefan 的說明而創建的簡單版本:
清單 1. 樣本樣式表
<?XML version="1.0"?>
<xsl:stylesheet XMLns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<!-- For simple examples, you usually want most of the document
passed through unchanged. This is the standard "identity"
transformation for doing that. -->
<xsl:template match="@*|node()" priority="-1">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Stefan wants to replace <testtag/> elements with a fixed header...
not very interesting, but OK as an example. -->
<xsl:template match="testtag">
<h1>tralala</h1>
</xsl:template>
</xsl:stylesheet>
他希望每當 <xsl:template> 啟動和結束時將一條注釋插到輸出文檔中。要那樣做,您應當找到每個模板並做相應的更改。
找到模板很容易;只需編寫一個匹配那些模板的模板。可以使用 <xsl:copy> 來復制模板元素本身,以及有關它們的任何名稱空間聲明。
清單 2. 匹配模板元素
<xsl:template match="xsl:template">
<xsl:copy>
...
</xsl:copy>
</xsl:template>
將注釋生成器插入模板主體稍微有些復雜,因為 XSLT 對於寫到輸出中的內容的順序有特別的規則。首先,我們需要顯式地復制模板的屬性,在將任何子元素添加到元素之前, 必須設置這些屬性。接下來,因為 XSLT 規定已修改模板中的任何 <xsl:param> 元素都 必須位於其它所有子元素(空格除外)之前,因此必須注意那些元素 — 否則您的注釋生成器可能會被插在 <xsl:param> 的前面,因而破壞樣式表。只有到那時,您才可以修改模板主體的其余部分(除了 <xsl:param> 之外的所有內容)以使其自動且神奇地添加注釋。
清單 3. 復制現有的模板內容
<xsl:template match="xsl:template">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="xsl:param"/>
...
<xsl:apply-templates select="node()[not(name()='xsl:param')]"/>
...
</xsl:copy>
</xsl:template>
現在您需要完成對余下工作的編碼,以在輸出文檔中生成適當的注釋,為了便於閱讀,注釋兩端可以加空格。您需要與下面大致相似的內容,以便於將模板的 match= 屬性值作為注釋文本的一部分顯示。(與 Stefan 的建議相比,我的版本略嫌冗長,這是因為我希望一看就能知道該過程生成了哪些注釋。)
清單 4. 生成跟蹤注釋的第一次失敗的嘗試
<!-- THIS WON'T WORK AS SHOWN. SEE BELOW! -->
<xsl:text>
</xsl:text>
<xsl:comment>
<xsl:text>[TraceXsl Begin] match="</xsl:text>
<xsl:value-of select="@match"/>
<xsl:text>"</xsl:text>
</xsl:comment>
<xsl:text>
</xsl:text>
將注釋插入模板結尾(end-of-template)的做法與此類似。
但是正如我在上面指出的,這樣編寫的代碼無法工作。如果您嘗試了,您將發現在樣式化樣式表的時候 <xsl:text> 和 <xsl:comment> 都會被解釋。但是您希望將其中的一些指令寫到生成的樣式表,並且僅當 樣式表運行的時候才執行這些指令!
幸運的是,XSLT 的設計人員預料到了這個問題,並提供了兩種解決方案。
一種解決方案是使用 <xsl:element> 和 <xsl:attribute> 偽指令(請參閱 參考資料)顯式地構建期望的輸出元素。這是一種功能非常強大的機制,它允許您構造您需要的幾乎任何文檔結構,並且可以充分使用 XPath 和 XSLT 的強大功能來設置內容以及名稱和名稱空間。但是,它稍微有點冗長,即使您利用了以下事實: <xsl:element> 根據指定元素名稱的上下文采用了正確的名稱空間:
清單 5. 利用 <xsl:element> 生成跟蹤注釋
<!-- THIS ONE WORKS! -->
<xsl:element name="xsl:text" XML:space="preserve">
</xsl:element>
<xsl:element name="xsl:comment">
<xsl:text>[TraceXSL Begin] match="</xsl:text>
<xsl:value-of select="@match"/>
<xsl:text>"</xsl:text>
</xsl:element>
<xsl:element name="xsl:text" XML:space="preserve">
</xsl:element>
另一種解決方案是使用 <xsl:namespace-alias> 機制(請參閱 參考資料)。利用名稱空間別名,您可以將一個名稱空間用於樣式表中的文字結果元素(和屬性),並在寫出生成的文檔時將其轉換成另一個名稱空間。要使用該功能,在用於多個樣式表的樣式表上面定義一個臨時的名稱空間,並告訴 XSLT:當它看到該名稱空間時,它應當實際輸出標准的 XSL 名稱空間。
v
<xsl:stylesheet version="1.0"
XMLns:xsl="http://www.w3.org/1999/XSL/Transform"
XMLns:tracexsl="http://www.ibm.com/xsl-example/tracexsl">
<xsl:namespace-alias stylesheet-prefix="tracexsl" result-prefix="xsl"/>
....
然後您可以用一種將注釋成功地寫到生成的樣式表中的格式重新編寫注釋生成器:
>清單 7. 利用 <xsl:namespace-alias> 生成跟蹤注釋
<!-- THIS ONE WORKS TOO! -->
<tracexsl:text XML:space="preserve">
</tracexsl:text>
<tracexsl:comment>
<xsl:text>[Tracexsl Begin] match="</xsl:text>
<xsl:value-of select="@match"/>
<xsl:text>"</xsl:text>
</tracexsl:comment>
<tracexsl:text XML:space="preserve">
</tracexsl:text>
在這兩篇文章中,我使用了 namespace-alias 解決方案,因為我認為它更便於人們閱讀。(正如您將在第 2 部分中看到的,對於 tracexsl: 名稱空間,我還有另一個好的用途。)但是這兩種方案都是起作用的。
請注意,在這兩種解決方案中,我都必須讓處理器在某些生成的 text 元素中保留空格。除了少數的特定異常外,XSLT 通常假定樣式表中的空格是無意義的。 <xsl:text> 碰巧就是其中的一個例外 — 但在這裡的樣式表中,XSLT 處理器不知道是 <xsl:element> 還是 <tracexsl:text> 將轉換成 <xsl:text> ,而且,如果未顯式地告知它不要丟棄新行,那麼它就會丟棄新行。
把一切都匯總起來,下面是您獲得的。
tracexsl1.xsl — 用於跟蹤樣式表的新樣式表。
tracexsl-sample1.xsl — Stefan 的樣本樣式表的可運行版本,如 清單 1所示。
當我使用 Xalan 對 Stefan 的樣式表運行 tracexsl1.xsl 時,我獲得了 tracexsl-sample1.xsl.withTrace 。
請注意,在生成的樣式表中, tracexsl: 前綴被綁定到與 xsl: 相同的名稱空間,當樣式表運行時 將執行這些元素。其它 XSLT 處理器可能更希望更改前綴,或者在更高的級別上定義它,而不是在每個生成的標記上定義它,但是意義是相同的。
這個樣式表稍微有點“丑陋” — 但是請記住,您不必手工編寫它(並且您可以重新生成它,而不是必須維護它),因此“丑陋”不是什麼嚴重的問題。
實際問題是,它完成了您希望它完成的功能了嗎?
tracexsl-sample1.XML — Stefan 的樣本源文檔,如上所示。
最後,我可以使用 Xalan 對源文檔運行生成的 tracexsl-sample1.xsl.withTrace ,它生成了 tracexsl-sample1.XML.traceResult — 嗨,它起作用了!
為了進行比較,我還編寫了一個使用 <xsl:element> 的版本:
tracexsl1a.xsl — 使用 <xsl:element> 進行跟蹤。
對 Stefan 的樣本樣式表運行 tracexsl1a.xsl。
tracexsl-sample1a.xsl.withTrace — 樣式化的樣式表。
請注意,該版本生成了一個更為整齊的帶注釋的樣式表 — 帶有的名稱空間聲明比較少 — 但是缺點在於,為了生成它必須多花點功夫,並且您不能立即看到您的跟蹤生成器添加了哪些偽指令。
對 Stefan 的樣本輸入文件運行 上述文件……
您得到了 tracexsl-sample1a.XML.traceResult — 這又一次顯示了 Stefan 尋求的結果!
深入研究
好的,我已經說明了概念……但實際上它還不是一個可用的工具。
一個問題就是生成的注釋所提供的信息比期望的要少。如果樣式表使用了 mode ,或者是根據 name 調用模板的,那麼我們可能無法知道實際執行了哪幾個模板。它不會說明該模板正在依據源文檔的哪部分運行。您大致知道發生了什麼……但是無法 確切地知道發生了什麼,或者為什麼會發生。
第二個問題就是在任何重要的樣式表執行中都會產生 許多注釋 — 這樣可能會由於注釋太多而難以找到那些您真正感興趣的注釋。
還有一個更嚴重的問題:生成的注釋可能會破壞某些樣式表。例如,Stefan 就明顯希望在每條注釋的前後使用換行。對於大多數 Html 處理而言,這沒問題,在浏覽器的格式化過程中通常會丟棄額外的空格 — 但是對於 XML 而言卻並非如此,XML 中的空格可能是有意義的。在某些情形下,插在每個模板輸出前後的注釋甚至可能破壞某些內容,例如當模板的輸出出現在不允許使用注釋的上下文(例如某個注釋內)中時,或者當輸出與其它文本進行連接以便在輸出文檔中生成一句話時。
但是,將注釋工具編寫成樣式表的一個優點是,您可以調整它以滿足您的需求,而不必依賴於某些內置在 XSLT 處理器中的功能。在本系列文章的 第 2 部分中,您將:
改進跟蹤消息,以通知您更多有關哪些模板正在運行、為什麼它們正在運行以及它們正在處理什麼的信息。
添加選擇性跟蹤(可從命令行控制的!),以便於可以只跟蹤您感興趣的樣式表部分
此外,作為獎勵,我將為您演示如何為給定的節點生成出色的近似的 XPath。
請繼續關注第二篇文章!