關於本系列教程
W3C 發布的最新規范 XSLT 2.0 是一種轉換 XML 文檔的語言。它包括很多新的特性,其中一些特性是專門為克服 XSLT 1.0 的不足而設計的。本系列文章從 XSLT 1.0 用戶的角度(希望解決老問題、學習新技術、發現值得期待的新特性),提供了對 XSLT 2.0 的高級概述和深入剖析。我們提供了來自常見應用程序的例子,並為那些希望升級的用戶提供了實用的建議。為了幫助您開始使用 XSLT 2.0,本文還提供了相應的遷移技術。
XPath 表達式的改進
本文深入地介紹 XSLT 2.0 的一些新特性,主要包括:
對輸入數據應用 XPath 表達式
數據排序
用表達式代替 XSLT 指令完成更多的工作
減少樣式表中變量引用的數量
此處將討論幾個新增的 XSLT 指令,但是關於這方面改進的更多信息請閱讀本系列的第一部分。
XPath 節點測試的改進
在 XPath 中,根據節點的種類(比如元素或屬性)或者名稱縮小節點集合的方式就是 節點測試。在路徑表達式的每個軸步驟中,節點測試出現在軸和謂詞(用於進行任意篩選)之間。
默認元素/類型名稱空間
XPath 1.0 中的一個特性可能會造成混亂:在 doc/chapter 這樣的路徑表達式中,名稱測試 doc 和 chapter 只能匹配沒有聲明名稱空間的元素節點,即便含有 XPath 表達式的元素綁定了默認名稱空間也是如此。這與帶前綴的名稱測試不同,比如在 pub:doc 中,就關聯了作用域中名稱空間聲明為 pub 的節點。
XSLT 2.0 和 XPath 2.0 並未改變對不帶前綴的名稱測試的默認解釋方式。但是,XPath 2.0 引入了默認元素/類型名稱空間 的概念。在 2.0 樣式表中,可以使用 [xsl:]xpath-default-namespace 標准屬性設置默認元素/類型名稱空間。(關於標准屬性的作用范圍 以及何時使用 xsl: 前綴,請參閱本系列文章的 第 4 部分。)不帶前綴的名稱測試將匹配具有相同本地名的元素節點(如果該節點的名稱空間和當前有效的默認元素/類型名稱空間相同,或者該節點沒有名稱空間並且也沒有默認元素/類型名稱空間)。因此,在下面的代碼片斷中,doc 與 chapter 將會匹配 “http://example.org/pub” 名稱空間中的元素節點。
<xsl:template match="doc" xpath-default-namespace="http://example.org/pub">
<xsl:for-each select="chapter">
<!-- and so on -->
如果您錯誤地認為沒有前綴的名稱是樣式表中當前作用域中的默認名稱空間,那麼可以通過在 XSLT 2.0 中設置 xpath-default-namespace 屬性盡快糾正錯誤。這樣可以不用為樣式表中的每個名稱測試都加上前綴,從而節省了時間。如果要在 XML 文檔和 XSLT 樣式表中使用默認名稱空間作為樣式首選項,則可以使用 xpath-default-namespace 這個新特性指定默認名稱空間聲明。
類別測試的改進
和 XPath 1.0 相比,XPath 2.0 中節點測試的語法更加一致,並在節點選擇中加入了對模式敏感(schema-aware)支持。(本系列的 第 3 部分 討論了一些支持模式敏感特性的 XSLT 2.0 處理程序,第 1 部分 列出了各種 XSLT 結構在模式敏感處理方式下的行為差異。)在 XSLT 1.0 中,可以在路徑表達式中使用類別測試 選擇符合條件的注釋、處理指令或文本節點 — 分別使用 comment()、processing-instruction() 和 text()。也可以使用 node() 類別測試選擇任意的節點。但是,要選擇任意元素或屬性節點,必須使用通配符語法(* 或 @*)。
XPath 2.0 引入了一些新的類別測試,包括 element()、attribute()、schema-element()、 schema-attribute() 和 document-node(),這些類別測試構成了能夠匹配的所有節點類別的完整集合。element() 和 attribute() 類別測試的名稱和類型參數是可以選擇指定的。如果不帶參數,element() 和 attribute() 將分別選擇任意的元素和屬性節點。如果指定了名稱,該測試只選擇具有該名稱的節點;如果指定了類型,則選擇與指定的類型相同或者為其派生類型的節點。比如,element(ship-addr, loc:USPostalAddress) 將選擇名稱為 ship-addr、類型為有效 loc:USPostalAddress(無論是直接類型還是派生自父元素)的節點。
schema-element() 和 schema-attribute() 類別測試只有一個不能省略的參數:分別是樣式表當前有效模式定義中可用的元素聲明和屬性聲明的名稱。這兩個類別測試將選擇類型與聲明的元素或屬性類型相同(或派生自該類型),並且名稱與聲明名稱相同的節點(或者,對於 schema-element(),位於其置換組 [substitution group] 中)。
在 XPath 1.0 中,要建立導航到根節點(XPath 1.0 對文檔節點的叫法)的測試是非常麻煩的,這進一步證明了 XPath 1.0 語法的不規范性。比如,在 XPath 1.0 中,要選擇上下文節點的父節點而且這個父節點必須是文檔節點,就必須這樣編寫代碼 parent::node()[not(self::*)],其中的謂詞將元素節點排除在外。因為只有文檔節點和元素才能作為其他節點的父節點,因此篩選過後僅留下了文檔節點。
無參數的 document-node() 測試可選擇文檔節點。其參數可以是元素類別測試或者模式元素類別測試。這種帶參數形式的 document-node() 類別測試將選擇有一個子元素與 element() 或 schema-element() 測試指定內容匹配的文檔節點。
字符編碼和排列法簡介
我們經常會遇到這樣一種情況,需要樣式表按照一定的順序排列處理的字符串數據以便讀者查看 — 最常見的方式就是通常所說的字母順序。對字符串數據進行比較並按一定順序排列的過程稱為排序。在經過解析 XML 文檔中,字符串數據由 Unicode® 字符組成,每個 Unicode 字符都有一個代碼點,即和該字符關聯的一個唯一的整數值。
可以使用字符串中每個字符的代碼點為基礎進行排序,如果樣式表只需要對英文數據排序那麼這種方法非常合適。但是在全球化的時代,很可能需要樣式表根據多種不同的語言和文化傳統對數據進行排序。即使樣式表只需要按照一種語言規則處理字符串數據,仍然有可能需要進一步控制如何在不同情形下比較字符串 — 比方說要考慮是否要區別對待大小寫字母、變音符號(diacritical mark)等等。
在 XSLT 1.0 中,字符串只能根據每個字符的代碼點進行比較,但是在 xsl:sort 指令中除外。如本文 “排序中的排列法” 一節所述,XSLT 2.0 引入了排序 URI 指令,從而允許進一步控制樣式表如何比較字符串。排序 URI 是一種實現定義的 URI,可用於為特定的字符串比較操作指定相應的語言和文化傳統。可以將排序 URI 作為字符串比較函數的參數(如 xsl:sort 和 xsl:key 的 collation 屬性中的 starts-with),或者作為 [xsl:]default-collation 屬性的默認值。後面這種屬性是標准屬性 之一,可以出現在樣式表的任何地方;關於標准屬性的作用范圍以及改變屬性值的影響,請參閱本系列的 第 4 部分。本文後面的比較 一節還將進一步介紹。
XPath 2.0 僅定義了一種排序 URI,即 Unicode 代碼點排列法,字符串根據每個字符的 Unicode 代碼點進行比較。XSLT 2.0 將其作為默認的排序 URI,XPath 2.0 的其他宿主語言可以使用不同的默認排序 URI。
關於標准化其他排序 URI 的提議框架,請參閱 RFC 4790。在大量的標准排列 URI 出現並得到廣泛支持之前,請參閱供應商的文檔看看處理程序支持哪些其他的排序 URI。使用全局變量保存樣式表需要的排序 URI,這樣很快就能從一組供應商特有的排序 URI 變更為另一組,或者變更為標准排序 URI。
數據組織的改進
XSLT 還具有一些其他的功能,比如能夠重新構造數據的結構。第 1 部分 介紹了一種需求甚廣的新特性:分組。本文將介紹對 XSLT 1.0 重構功能的改進:排序和聚合函數。在 2.0 中,兩者都能用於序列。序列是觀察舊概念的一種新方式,它拓展了節點集的概念,將原子值(非節點的基本數據)包括了進來。
值序列
有些情況下,可能需要編寫樣式表處理一些輸入樹,計算中間結果 — 比方說每個節點的值 — 然後進一步處理這些中間結果。比方說,假設需要計算某些交易活動的總價值以及每次交易所占的比重。使用 XSLT 1.0,可能需要編寫遞歸模板來計算每次交易的價值,然後加到一個總數中,最終返回用文本節點表示的總價值的結果樹片斷(RTF)。然後樣式表依次遍歷各個交易,重新計算每次交易的價值以及所占的比重。采用這種技術必須對需要聚合的不同類型的輸入數據編寫不同的模板。如果交易額的計算非常復雜,樣式表就快不起來,因為每筆交易都必須計算兩次。最好對每筆交易額只計算一次,使用這些中間結果計算總額然後再計算所占的百分比。
在 XSLT 1.0 中,只能使用字符串和 RTF 存儲中間結果。RTF 還必須用 node-set 擴展函數訪問存儲在片斷中的每個值。使用字符串非常笨拙,而且必須為字符串中的每個值保留固定的字符數,或者使用分隔符來劃分。比如,如果每個字段用十個字符,代碼如清單 1 所示。
清單 1. 將一組數據轉化成百分數的老方法
<!-- Place all transaction values in a single string value -->
<xsl:variable name="transaction_values">
<xsl:for-each select="transaction">
<xsl:variable name="value" select="@unit_price * @quantity"/>
<xsl:value-of select="substring(concat($value,' '),1,10)"/>
</xsl:for-each>
</xsl:variable>
<!-- Calculate the total value of the transactions -->
<xsl:variable name="total">
<xsl:call-template name="calculate_total">
<xsl:with-param name="valueList" select="$transaction_values" />
</xsl:call-template>
</xsl:variable>
<!-- Format each value as a percentage -->
<table>
<tr><th>Transaction ID</th><th>Value</th><th>Percentage of total</th></tr>
<xsl:for-each select="transaction">
<xsl:variable name="value" select="substring($transaction_values,10*position()-9,10)"/>
<!-- Format the output here with ($value div $total)*100 -->
</xsl:for-each>
</table>
XPath 2.0 引入了序列 的概念,序列是一種有序列表,可以包含零個或多個值 — 節點、整數、字符串等等。實際上,XPath 2.0 中所有的表達式和 XLST 2.0 中的指令都對序列求值,因此,單獨的節點或者原子值實際上是包含一個項的序列。XPath 2.0 中不再有節點集,只有節點序列。一些 XSLT 指令,如 xsl:for-each 和 xsl:copy,和 XPath 函數,如 count() 和 sum(),原來只能操作節點集或者單個節點,現在已經可以操作任意序列了。如果變量 var 在某個序列中含有多個值,那麼在 XPath 2.0 中就可以使用 $var[1] 選擇第一項,就像在 XPath 1.0 中選擇節點集變量中的第一個節點一樣。
再考慮 清單 1 中的例子。如果首先將所有的交易額收集到一個數字序列中,就不需要用遞歸模板計算原來列表中的總值了。XSLT 2.0 版本的處理方式如清單 2 所示。
清單 2. 將一組數值轉化成百分比的 XSLT 2.0 方法
<!-- Place all transaction values in a single sequence -->
<xsl:variable name="transaction_values" as="item()*">
<xsl:for-each select="transaction">
<xsl:sequence select="@unit_price * @quantity"/>
</xsl:for-each>
</xsl:variable>
<!-- Calculate the total value of the transactions -->
<xsl:variable name="total" select="sum($transaction_values)"/>
<!-- Format each value as a percentage -->
<table>
<tr><th>Transaction ID</th><th>Value</th><th>Percentage of total</th></tr>
<xsl:for-each select="$transaction_values">
<!-- Format the output here with (. div $total)*100 -->
</xsl:for-each>
</table>
在這個示例中,需要注意如下幾點:
xsl:for-each 指令和 sum 函數在這裡操作的是序列。
點號(.)在這裡稱為上下文項 表達式,可以是節點或者原子值,視處理的序列中的當前項而定。
執行 xsl:sequence 指令的結果是通過計算 select 表達式(這裡是單個數值)所得到的序列,xsl:for-each 的結果是將每次迭代的序列連接起來所得到的序列。(有關 XSLT 2.0 的其他示例,請參見本系列 第 5 部分 的第一個例子。)
第一個 xsl:variable 元素中的 as 屬性告訴 XSLT 處理程序將變量的值作為一個序列,而不是將其轉換為 RTF。(如果需要樹狀結構的變量,有關 RTF 的後繼者及隱含文檔節點 [implicit document node] 的更多信息,請參閱本系列的 第 1 部分。)
通過使用將在 本文後面 討論的 for 運算符,上述 XSLT 2.0 版本的樣式表還可進一步縮短。
可以使用函數和運算符(F&O)規范中描述的幾個新函數處理序列。這些函數可以按照位置插入和刪除成員、查找特定值出現的位置、按照類似子字符串的方式截取子序列等。另一個可能經常用到的函數是 distinct-values(),如果只需要獲得一組不重復的鍵,它比建立分組更快捷且更具有可讀性,而且能夠區分不同的數據類型而不會引發錯誤。(distinct-values() 的例子請參閱本系列 第 5 部分 “選擇函數” 一節。)另外,empty 和 exists 函數返回的布爾值可以幫助迅速判定序列是否有成員。
在一些向後兼容方式有效的情況下,如果需要原子值,那麼將選取序列的第一個成員。更多細節請參閱本系列第 6 部分。
排序的改進
如果需要對節點集進行排序然後執行和節點順序關系有關的計算,XSLT 1.0 也許曾經讓您困惑不已,因為排序節點只能直接在 xsl:for-each 或 xsl:apply-templates 中使用。比方說,假設需要獲得十位銷售額最高的雇員完成的總銷售額,可能就要編寫清單 3 所示的代碼,按照銷售額對雇員排序然後查找十位雇員的銷售額。接下來再選擇比它高或者相同的雇員,如果只能取十個的話還要去掉與其相同的那些雇員。
清單 3. 計算前十位數據的原始方法
<xsl:variable name="number_ten_text">
<xsl:for-each select="sales_force/employee">
<xsl:sort data-type="number" select="number(@total_sales)" order="descending"/>
<xsl:if test="position()=10">
<xsl:value-of select="@total_sales"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="number_ten" select="number($number_ten_text)"/>
<xsl:variable name="above_tenth"
select="sales_force/employee/@total_sales[. > $number_ten]"/>
<xsl:variable name="tIEd_for_tenth"
select="sales_force/employee/@total_sales[. = $number_ten]"/>
<xsl:text>Total sales for top ten = </xsl:text>
<xsl:value-of select="sum($above_tenth |
$tIEd_for_tenth[position()+count($above_tenth) <= 10])"/>
也許您更願意對原來的節點集排序,將排序後的節點存儲到變量中,然後選擇前十個成員。但是在 XSLT 1.0 中做不到,因為只能按照文檔順序或者逆文檔順序處理全部節點集。也可將排序的節點復制到結果樹片斷中,但這樣就不得不使用 node-set 擴展函數。在 XSLT 2.0 中,節點 序列 允許按照文檔順序以外的順序處理。雖然從上下文節點出發沿著某個軸仍然只能按照文檔順序返回節點,但是得到節點之後樣式表可以根據需要重新排列節點。XSLT 2.0 中的 xsl:perform-sort 指令大大簡化了上述任務。(下面最後一個元素中的 le 運算符就是 值比較 運算符的一個例子。)
清單 4. 2.0 中計算前十位數據的方法
<xsl:variable name="sorted_employees" as="item()*">
<xsl:perform-sort select="sales_force/employee">
<xsl:sort select="number(@total_sales)" order="descending"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:text>Total sales for top ten = </xsl:text>
<xsl:value-of select="sum($sorted_employees[position() le 10]/@total_sales)"/>
在 XSLT 1.0 中可能會遇到這樣一個可用性問題:如果 xsl:sort 元素沒有 data-type 屬性,則以默認方式將排序鍵作為字符串操作,即使這樣會因為將排序鍵轉化成數字而大費周折。(這就是清單 3 在 xsl:sort 中同時使用 number() 和 @data-type 的原因。)比如,默認情況下 11 要排在 22 之前但在 3 之後。在 XSLT 2.0 中,如果沒有 data-type 屬性,排序的時候將考慮排序鍵的實際類型,如果樣式表選擇整數類型的排序鍵,排序的時候將把它們作為整數處理。這就消除了一種常見的錯誤,但是如果樣式表要同時用於 XSLT 1.0 和 2.0 處理程序必須記住這一點。
排序中的排列法
除了一種例外,XSLT 1.0 總是按照每個 Unicode 字符的 代碼點 比較字符串。這個例外就是 xsl:sort 指令,它的 lang 屬性告訴處理程序應該按照和指定語言的文化傳統一致的方式排序字符串。默認情況下,假定處理程序將根據系統環境選擇語言。如果不同的處理程序使用不一樣的默認行為,那麼這種默認方式常常會造成一種混亂的局面,一種處理程序可能將字符串 “resume” 和 “résumé” 看作是相同的,另一種則可能視為不同。即使有 lang 屬性,相異的規則也可以適用於不同的情形。比方說,有時候變音符號可能並不重要,不過 lang 屬性卻沒有辦法指出這一點。
XSLT 2.0 為 xsl:sort 指令引入了新屬性 collation,用於在排序的時控制如何比較字符串,有關排序 URI 的詳細信息,請參閱 “字符編碼和排列法簡介”。XSLT 2.0 的默認排序 URI 是 Unicode 代碼點排列法,因此對於不帶 collation 和 lang 屬性的 xsl:sort 指令,其默認處理方式對所有的 XSLT 2.0 處理程序都是一致的。
范圍表達式
迭代固定的次數是 XSLT 1.0 中可能需要使用遞歸模板的另一種形式。比方說,簡單地從一迭代到十,如果模板需要迭代的次數和輸入文檔沒有什麼關系,使用 XSLT 1.0 完成這個任務就非常麻煩。人們有時候會竭盡全力避免使用遞歸執行這類迭代,請參閱技巧 “節點集計數”中的例子。XPath 2.0 中的范圍表達式 簡化了迭代固定次數的任務。表達式 (1 to 10) 創建包含從一到十這些整數值的 序列。可以用 <xsl:for-each select="1 to 10"> 編寫執行十次的循環,用上下文項表達式連續選擇從一到十這些數字。
數字聚合函數
本文中的一些例子讓您記起包含原子數字值的元素和屬性是多麼常見。可能不僅僅要對數據排序和比較,還需要將一組數據聚合成一個數。XPath 1.0 提供了 sum 和 count 函數,這兩個參數都以節點集作為參數。兩者結合起來可以計算平均值。但是只能通過遞歸模板或者擴展函數來完成其他兩種常見的操作:求最大值和最小值。
在 XSLT 2.0 中,聚合函數集合已經擴展為 sum()、count()、avg()、min() 和 max()。這些函數的參數可以是任何序列,而不僅僅是節點集,只要序列的值和操作相適應即可。count() 函數可用於任何序列。sum() 和 avg() 函數要求數值能進行加運算,可以是任何數值類型,如 xs:yearMonthDuration 和 xs:dayTimeDuration。min() 和 max() 函數要求為數據定義了 lt(小於,一種新的 比較運算符),有幾種數據類型可進行該運算。如果是字符串數據,則可以指定使用 collation。
新增和改進的字符串函數
XSLT 和 XPath 2.0 提供了許多字符串操作功能,1.0 樣式表作者曾為這些問題困擾不已。一些新的函數只不過是為了方便和提高可講習性,比如 upper-case 和 lower-case 函數。在 1.0 中可通過 translate() 改變大小寫,但是很容易出錯,而且可能不如專門的函數效率高。此外,F&O 規范還增加了 ends-with 來補足 starts-with 和 contains 函數。
還可以通過各字符串的 序列 來聚合字符串,新增的 string-join 函數以序列為參數,這與 concat() 有所不同,它可以有很多參數,每個參數要求是字符串。新增的 tokenize 函數執行相反的操作,用分隔符標記字符串之間的邊界。該函數可以使用其他新函數和新 XSLT 指令中所用的正則表達式特性。一旦將字符串分解成序列,就可以用新的序列操作函數如 insert-before() 來修改它。
本系列的 第 6 部分 討論了表達式中的類型不匹配,並建議使用類型轉換函數來保證計算結果得到正確的類型。F&O 中同時提供了 fn:string()(1.0 中 string() 的後繼者)和 xs:string(),僅當參數序列的長度不為 1 時兩者才有不同。如果參數是空序列,fn:string(seq) 將返回長度為零的字符串,另一個函數則返回空白序列。如果參數有多個成員,通常情況下兩者都將引發錯誤,但是如果采用了向後兼容方式,則 fn:string(seq) 不會造成錯誤。(有關向後兼容方式的信息,請參閱本系列的 第 3 到 6 部分。)但是 fn:boolean 和 xs:boolean 與此不同,這兩個函數以不同的方式處理字符串。xs:boolean() 能夠轉換的字符串只有 true、false、"1" 和 "0",不過它可以轉換所有的數字。輸入文檔中 xs:untypedAtomic 類型的值也只能是這四個,其他都會出現類型錯誤。1.0 中的 boolean 函數變成了 fn:boolean(),它能夠將所有非零長度字符串轉化成 true,這與 1.0 一樣。請注意,xs:boolean("false") 返回布爾值 false。xs:boolean("0") 返回 false,和 xs:boolean(0) 一樣。(在這一段以外,F&O 規范中標准函數的命名都沒有 fn: 前綴。遺留樣式表不需要在標准函數名加上前綴也能在 2.0 處理程序上處理。但類型轉換函數必須 有前綴。使用 boolean() 的遺留樣式表將調用 2.0 中的 fn:boolean(),該函數與 1.0 兼容。)
因此樣式表能夠以適合文化傳統的方式比較字符串,contains、starts-with、substring-before 和 substring-after 字符串處理函數都有可選的 排序 URI 參數,這與新增函數 compare、ends-with、deep-equals 以及 distinct-values 一樣。通過選擇適當的排序 URI,可以控制 starts-with('encyclopædia volume 3', 'encyclopedia', $collation-uri) 返回 true 還是 false。如果忽略排序 URI,則會采用默認排列法。默認排列法也影響 一般比較和值比較 運算符如何比較字符串,因此如果改變默認值,一定要記住可能需要使用 Unicode 代碼點 排列法的 compare 函數,或者使用新的 codepoint-equal 函數,以便再進行按代碼點進行比較。
XPath 2.0 新增函數 string-to-codepoints 和 codepoints-to-string 提供了操縱字符串字符的方法。string-to-codepoints 函數將輸入字符串參數轉化成整數 序列,每個整數對應輸入字符串中每個字符的 代碼點 值,codepoints-to-string 則完成逆過程。您可能會發現如果轉換的結果不是 XML,這種方法對於為轉義字符定義特殊約定將非常方便 — 比方說是 Java 或者其他編程語言代碼。如果要將 ASCII 字符之外的字符用問號代替(代碼點是 63),可以這樣寫:
codepoints-to-string(for $cp in string-to-codepoints($string) return
if ($cp lt 32 or $cp gt 126) then 63 else $cp)
這個例子還用到了新的值比較運算符(詳見 下一節 和嵌套在 ForExpr 中的 IfExpr。
有關其他新增函數的信息,請參閱本系列 第 1 部分,其中包括 normalize-unicode 和用於控制 URI 編碼與轉義的函數。
值比較和一般比較
請注意,codepoints-to-string 的例子還用到了兩個新的 XPath 值比較 運算符:lt(小於)和 gt(大於)。此外還有 eq(等於)、le(小於等於)、ne(不等於)和 ge(大於等於)。原來的運算符 — <、>、<=、>=、= 和 != — 稱為一般比較 運算符。值比較用於原子值之間的關系,不檢查整個集合。保留一般比較是為了需要檢查值集的情況,不過還要注意將在 本文後面 介紹更顯式的 some 運算符。
您可能遇到過這樣的情況,期望一般比較運算符的操作數最多包含一個節點,但是由於樣式表中的缺陷造成一個操作數包含多個節點。如果比較的一個操作數包含多個值,新的值比較運算符將報告錯誤,因此使用這些運算符可以迅速有效地檢測錯誤。
另一個令人不快的問題是,XSLT 1.0 中的 <、>、<= 和 >= 不等運算符總是將比較的值轉化成數字,因此不能用於比較字符串。也許經過痛苦的調試之後才會發現這一點,因為任何字符串都會成功地轉化成數字,即使是 NaN 也如此。新的值比較運算符可以對字符串執行詞法比較,不會自動將字符串值轉化成數字,因而不但消除了一種常見的錯誤來源,而且提供了一種有用的新功能。不等值比較運算符可用於任何有序的數據類型,比如和時間有關的類型。
讀取文本文件
我們知道,XSLT 的轉換輸入必須是 XML 文檔,XSLT 可以通過 document 函數讀入其他 XML 文檔。XSLT 2.0 增加了新的 unparsed-text 函數,可以將文件或者其他文檔形式作為字符串讀入。對於整個文檔來說它是沒有結構的字符串,但是利用新增加的字符串函數,我們可以根據需要執行進一步處理。對這一功能的創造性使用必定會隨著時間的流逝而出現。一種很有可能的用法是代替向樣式表傳遞大量的參數。可以用下列代碼將字符串分解成文本行:
<xsl:variable name="xList" as="xs:string*"
select="tokenize(unparsed-text('listFile'),' ')"/>
這樣就建立了一個字符串 序列,各個字符串分別對應文件中的一行。這個文件可以是流行的 UNIX®/Linux™ shell 命令生成的,比如 ls 或 find。如果文件中包含 XML 文件名,就可以逐個將這些文件名傳遞給 document 函數,該函數可以讀取些 XML 文檔。(注意,document 和 unparsed-text 都屬於 XSLT函數集,而不屬於 "fn:" F&O 集合,並且不能在 XQuery 和 XForms 中使用。)可以指定文件的字符編碼,或者讓處理程序根據 XSLT 規范指示的線索去猜測。
新增的 XPath 運算符
XPath 2.0 規范含有三個語法部分(規范的 productions 部分),分別為 IfExpr、ForExpr 和 QuantExpr。各個部分將一些關鍵字和子表達式按照一定的順序結合在一起。除此之外還有一種新的表達式,即范圍表達式,我在 前面 已經介紹過。
if 運算符
有時候需要根據一些輸入數據計算出一個值,而描述公式最好的辦法是用 if-then-else 形式,盡管可能更希望用函數。在 XSLT 1.0 中通常不得不求助於程序轉移,比如使用 xsl:choose,它需要計算中間變量並建立內部和外部表達式。有時候可能會以想不到的方式使用函數,比如 Oliver Becker 在清單 5 中所用的 1.0 技巧。此處的代碼可以獲取全部聊天記錄,不論主題行前面是否包含 “Re:”。
清單 5. 去掉前綴的原始方法
<xsl:sort select="concat(
substring(subj,1,number(not(starts-with(subj,'Re: ')))*string-length(subj)),
substring(substring-after(subj,'Re: '),1,
number(starts-with(subj,'Re: '))*string-length(substring-after(subj,'Re: '))))"
data-type="text" />
清單 5 依賴於 substring() 在某些邊界情況下的行為以及布爾值到數字的轉換。得到的公式很難讀懂,但至少按照 1.0 xsl:sort 的要求是一個公式。(上面的例子還可進一步簡化,即使在 1.0 中。)
在 2.0 xsl:sort 中可使用完整序列構造器,因此可以用 xsl:choose,但是也可在一個表達式中用 if 運算符,這樣更容易閱讀,如清單 6 所示。
清單 6. 使用 IfExpr 去掉前綴
<xsl:sort select="if (not(starts-with(subj,'Re: ')))
then subj
else substring-after(subj,'Re: ')"
data-type="text" />
由於 IfExpr 是表達式而不是程序性的 xsl:if,因此必須有 else 子句以保證表達式有一個結果。
for 運算符
XPath 1.0 提供了 sum() 函數,用於將節點集中的數字值相加。但是如果需要對這些節點計算某些公式 — 比方說把節點的兩個屬性相乘 — 然後再把結果加起來,就只能使用另一個遞歸指定的模板了。此外,也可以創建一個結果樹片斷,為原始節點集中的每個元素建立一個節點,該節點的值就是將公式應用於原始節點集中的各個節點所得到的結果。但是這樣就只能使用 node-set 擴展函數把這些值加起來。
而在 XPath 2.0 中,sequences 和 for 運算符可以大大簡化這種任務。通過這種新的運算符,可以將變量與一個或多個序列中的各個值綁定在一起,為輸入序列中的所有數據組合計算出新的值。比如,為了計算采用不同貨幣結算(不同的匯率)的一些交易的總美元價值,在 XSLT 2.0 樣式表中可用下面的表達式:
sum(for $trans in transaction return $trans/@exchange-rate * $trans/@value)
量化表達式
在 XPath 1.0 中,涉及到節點集的比較被稱為存在性量化的(existentially quantifIEd)。這種奇特的說法的含義是只要對節點集中某些節點比較成立,則整個集合的比較也成立 — 換句話說,只要存在一個節點則結果為真。雖然這可能正是需要的結果,但也是造成錯誤的一個常見原因。
比方說,假設需要判斷包含個人信息的 XML 文檔中是否有姓 “Smith” 的雇員。可以用 employee/@surname='Smith' 解決著個問題。現在假設將條件改為檢查是否沒有 姓這個姓的雇員。您也許以為只要用 employee/@surname!='Smith' 就行了。但是上述代碼實際上是問,“是否存在不姓 Smith 的雇員?” 要檢查是否沒有這樣的雇員,應該寫成 not(employee/@surname='Smith')。
XPath 2.0 的量化表達式可以明確說明如何執行這樣的比較。如果表達式的結果對於某些值或者值的集合為真,則 some 表達式為真,而只有當表達式的結果對於每個值或值的集合都為真,every 表達式才為真。語法類似於 for 表達式,但結果總是一個布爾值。和 ForExpr 一樣,QuantExpr 可以綁定一個或多個變量。
要測試是否有雇員姓 Smith,可以這樣寫:
some $i in employee satisfIEs $i/@surname eq 'Smith'
檢查是否沒有這樣的雇員則用:
every $i in employee satisfIEs $i/@surname ne 'Smith'
這裡的 eq 和 ne 都是 值比較 運算符。
參數傳遞
XSLT 2.0 有四個調用模板的指令,比上一版本增加了一個,而且這四個指令都能傳遞參數。新增的指令是 xsl:next-match,類似於 xsl:apply-imports,但是可以觸發相同優先級的模板而不僅僅是較低導入優先級的模板。文檔節點和元素的內置規則也能導致模板被激活,這種情況下將傳播參數。內置規則不使用參數,但是在 2.0 中可以將參數傳遞給下一個模板。
XSLT 1.0 中一種常見的情況是反復傳遞只需要轉給嵌套更深的模板的參數。在 2.0 中可以把參數指定為通道參數(tunnel parameter),只在使用的地方聲明。通道參數實際上更像是全局區,只不過在模板調用棧中可以改變它的值。為了把參數放入通道中,只需要為 xsl:with-param 指令加上 tunnel="yes"。通道中的參數可以反復讀取,但是要記住只有在最初將參數放入通道的指令的處理過程中調用的模板才能使用它。這就是說通道參數可以在多層模板調用棧中累積。
規則很簡單,接收參數的模板必須聲明本地使用的每個參數,當然參數名不能沖突。對於模板中的所有變量引用(比如 $foo),都必須要有一個解析引用的方法。XSLT 2.0 中新增了一些可能的參數類型,下表中標有新 的為 XSLT 2.0 新增的,其他則是 XSLT 1.0 原有的。
新: ForExpr 或 QuantExpr 中並且在表達式作用域內的的變量集合
在模板前面用 xsl:variable 建立的變量集合
模板使用傳統的 xsl:param 指令接收的參數
新:通道參數,通過 xsl:param tunnel="yes" 指令在模板中激活
在樣式表或其他位置定義的全局變量或參數
如果沒有在模板中聲明通道參數,而模板正好又沒使用它的話,就不用擔心和模板中的變量引用發生名稱沖突。上述第三和第四條說明了通道參數(激活的時候)和同名傳統參數之間的區別。
通道參數可以讀取多次。因此如果某個模板調用了使用通道參數的模板,則不需要使用 xsl:with-param tunnel="yes" 傳遞相同的參數。但是,如果需要為更深層的模板改變參數值則可以這樣做。
如果全局層次的 xsl:variable 包含序列構造器,則其用法與模板類似,但是要注意 xsl:variable 不接受 xsl:param 子元素。就是說這些變量永遠不會接收參數,不論通道參數還是傳統參數。樣式表參數不能指定為通道參數,但是可以全局訪問。通道參數不允許在 xsl:function 中使用,只能用於模板之間的傳遞。
模板需要控制所接收參數的某些方面。利用 2.0 中對數據類型的敏感特性,傳入參數可能需要轉換成特定的類型以便在模板中使用。比如,模板可能需要用接收的參數和已知類型的值進行 值比較。xsl:param 和其他指令新增加了 as 屬性來指定數據類型。(本系列 第 1 部分 的清單 2 給出了一個簡潔的例子。)有意思的是,也可以同樣使用 xsl:with-param 中的 as 屬性,如果變量中包含一個序列構造器而又不希望使用隱含文檔節點的時候這一點可能很有用。(有關 @as 的更多信息,請參閱本文中的 “讀入文本文件” 和 清單 2,以及 第 1 部分 中關於隱含文檔節點的例子和 第 5 部分 中關於樣式表函數的例子。
可以在 xsl:param 元素中指定參數是可選或必須的,除非該元素是 xsl:function 的子元素,這種情況下參數是不可少的。如果出現了 required="yes" 屬性,則不能使用 select 屬性或序列構造器設置默認值。
表 1. 必須參數和可選參數
xsl:param 元素的位置 @required 的設置(沒有默認值) @tunnel 的設置(沒有默認值) 提供參數值時的動作(或者調用模板時在通道中發現參數值) 參數值不 可用時的動作 備注 xsl:stylesheet 否 (不可用) 正常 根據默認方式計算 這是 1.0 中樣式表參數的情形。 是 正常 錯誤 按匹配或名稱調用 xsl:template 否 否 正常 根據默認方式計算 無法為啟動時指定調用的模板設置參數。 是 從通道中取出參數 根據默認方式計算,但是只能在本地使用 是 否 正常 錯誤 是 從通道中取出參數 錯誤 根據匹配調用 xsl:template,不聲明該名稱的參數 NA NA 忽略 無意義 根據名稱調用 xsl:template,不聲明該名稱的參數 NA NA 若啟用了向後兼容模式,則忽略,否則報錯 無意義 有關兼容模式的更多信息,請參閱本系列 第 6 部分。 xsl:function(必須) (不需要) (不可用) 正常 錯誤 參數名稱 僅在 xsl:function 中使用,根據在參數列表中的位置指派傳入值。 內置模板規則 NA NA 傳遞給自動調用的任何模板 無意義 未被寫入,顯然沒有 xsl:param 子元素,也沒有變量引用。表 1 中的 “NA”(不適用)表示如果沒有 xsl:param 指令的話,也就無法設置 required 或 tunnel屬性。有關 xsl:function 的信息,請參閱本系列 第 1 部分;有關啟動時選項的信息,請參閱 第 3 部分。
結束語
您也許對本系列文章沒有涉及到的其他 XSLT 2.0 特性感興趣。可以參考 XSLT 2.0 規范的附錄 J.2 。請關注 developerWorks 更多關於 XSLT 和 XPath 的文章。