我撰寫的 XForms 書籍(請參閱 Resources)於 2003 年秋同時在書架上和網絡上出現。在很短的時間內,我收到了大量電子郵件,都是關於 XForms 問題的,通常還附有一兩頁漏洞百出的 XML 源文件。一般情況下,我是樂於答復電子郵件的,但是一頁一頁地翻看別人的 XML 尋找常見的打印錯誤並不是件令人愉快的事,而且速度太慢。我需要一種更好的辦法。
我是“建設性偷懶”的忠誠信奉者,於是,我決定編寫一個在線工具,接受 XForms 文檔作為輸入,然後報告其中錯誤或者可能錯誤的所有標記結構。我從電子郵件文檔中合理抽取了人們所犯錯誤的樣本。二者結合起來,便成了一個強大的工具,它可以讓表單的作者自己發現錯誤。
XForms 島
XForms 1.0 規范(請參閱 Resources)被定義為一些元素、屬性和內容模型。但是有一點它沒有定義,就是根元素,這留給了宿主語言去解決。最常見的兩種宿主語言是 XHtml 和 SVG,但在原則上,幾乎任何 XML 詞匯表都可以使用。因此,XForms 驗證程序的第一項任務就是從文檔中分離出 XForms 部分。為此,我杜撰了 XForms 島這個詞。
因為 XForms 將用途和表示分離開來,除了最小的表單文檔之外,所有文檔都至少有兩個 XForms 島,一個用於 XForms Model(定義表單做什麼),一個用於 XForms User Interface(定義表單的外觀)。
清單 1 所示的是一個簡單的 XForms+XHtml 文檔——可能過於簡單了,但是也包含了一個常見的錯誤。
清單 1. 一個普通的、帶有錯誤的 XForms+XHtml 文檔
<?XML version="1.0"?>
<html XMLns="http://www.w3.org/1999/xHtml"
XMLns:x="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/XML-events">
<head>
<title>Basic mixed document</title>
<x:model>
<x:instance>
<!-- @@@ forgot XMLns="" on <root> @@@ -->
<root>
<child/>
</root>
</x:instance>
<x:bind nodeset="child" required="1"/>
</x:model>
</head>
<body>
<x:input ref="child">
<x:label>Label</x:label>
</x:input>
</body>
</Html>
清單 1 顯然有兩個 XForms 島: x:model 和 x:input 。問題是代碼如何定位這兩個部分。事實上這並不是很難,因為可以標識為 XForms 島的元素必須符合兩個簡單的條件:
必須在 XForms 名稱空間中。
在 XForms 名稱空間中不能有任何祖先。
雖然這種測試可以使用 XPath 完成,但我更願意探索一種不同的 Python 用處理 XML 的方法。於是我編寫了一個過濾器函數,判斷給定的節點是否啟動了一個 XML 島,清單 2 顯示了該代碼。
清單 2. 選出 XForms 島
# extract islands
def island_filter(node, usedns):
"""
An 'island' element has
1) the XForms NS
2) no ancestors with the XForms NS
usedns is a string containing the XForms Namespace URI
"""
if node.type != "element": return False # skip non-elements
rc = True
if (get_element_ns(node) != usedns): rc = False
node = node.parent
while node is not None:
if (node.type=="element" and get_element_ns(node)==usedns):
rc = False
break
node = node.parent
return rc
def get_element_ns(elem):
ns = None
try:
ns = elem.ns().content
except libXML2.treeError, e:
pass
return ns
XPath 檢查
驗證程序存儲檢查出的 XForms 島,以備以後進行處理。清單 1 中的錯誤(如注釋中所述)在於不小心將 root 元素留在默認的名稱空間 XHtml 中了。這種錯誤以及其他幾種類似的問題,可以使用清單 3 中所示的 XPath 檢查探測到,清單 3 將返回可疑的元素節點。
清單 3. 用 XPath 檢查名稱空間的漏洞
//xf:instance//*[namespace-uri(.)=namespace-uri(/*) or
namespace-uri(.)=$usedns]
注意,因為 清單 3 沒有假定宿主語言的結構,所以大量使用了 XPath 的 // 縮寫形式,這種形式通過 descendant-or-self 軸搜索整個處理的文檔。還要注意的是,XPath 中的名稱空間前綴映射( xf: )不一定與目標文檔使用的相同( x: )。該測試檢查 x:instance 元素的後代,看它是在 XForms 名稱空間中,還是在根節點的名稱空間中。無疑名稱中的限定部分是一個很好的線索,因為完全合法的 XForms 文檔可能滿足這個條件。另一方面,這也是一個發現編輯錯誤的好機會,如 清單 1那樣,因此,驗證程序發出一條警告信息。
連接 ID 和 IDREF
另外一種常見的錯誤是匹配 ID 和 IDREF 。這種問題部分是由於歷史原因造成的,因為定義 ID 機制依賴於 DTD。一些工具(在很大程度上依賴於作者對 XML Schema 和 Infoset 的看法)也允許通過 XML Schema 數據類型定義 ID 。但是實際上,您很少會發現這樣的定義,遇到的常常是恰好命名為 id 的屬性。
這種情況不怎麼好,但實用的驗證程序工具必須能處理這種情況。驗證程序查找 XForms 中包含 IDREF 的所有屬性。首先嘗試使用內置的 id() 函數;如果沒有找到匹配的元素,則轉而求助於 XPath 測試,尋找命名為 id 或者 XML:id 的屬性(根據未完成的 W3C 草案,請參閱 Resources)。代碼如下:
//*[@id='idstr' or @XML:id='idstr']
驗證 XForms 島
最後一步,驗證程序要分析每個 XForms 島,並使用 RELAX NG 驗證它。這項工作比看起來要復雜,因為 XForms 的每個部分(如 label )都可能包含來自宿主語言的標記,更不用說允許到處存在的其他屬性了。
為此,驗證程序使用了高度模塊化的 RELAX NG 模式,將其集成到非常寬容的宿主語言中。所謂“高度模塊化”是指每個元素定義、元素的屬性集、元素的內容模型都被分配了惟一的名稱,可以單獨擴展。清單 4 說明了處理單個元素定義的過程,使用的是 RELAX NG 緊湊語法。
清單 4. RELAX NG 模塊化的元素定義
Common.Attributes = empty
Single.Node.Binding.Attributes = attribute bind { xsd:NCName } |
(attribute model { xsd:NCName }?, attribute ref { xsd:string })
UI.Common.Attributes &=
#host language to add Accesskey, navindex, etc. here
attribute appearance { xsd:QName { pattern = "[^:]+:[^:]+" } |
"minimal" | "compact" | "full" }?
Select = element select { Select.Attributes, Select.Content }
Select.Attributes &=
Common.Attributes,
Single.Node.Binding.Attributes,
UI.Common.Attributes,
attribute selection { "open" | "closed" }?,
attribute incremental { xsd:boolean }?
Select.Content = Label, List.UI.Common.Content, UI.Common.Content
注意,即使包含有標為 xsd:NCName 的 IDREF 屬性,也只對屬性進行詞法驗證。如前所述,實際的 ID - IDREF 連接檢查是在不同層次上進行的。分別定義不同成分的主要好處是容易擴展,比如為所有的表單控件添加 class 屬性。
事實上,這正是宿主語言模式要做的事。驗證程序的這一部分仍在開發之中,但清單 5 說明了如何定義宿主語言。
清單 5. XForms + 宿主語言定義
Common.Attributes &=
attribute id { xsd:NCName }?,
attribute XML:id { xsd:NCName }?,
attribute class { xsd:NMTOKENS }?
如果包含在 XForms 的主模式中, 清單 5 中所示代碼就可以擴展 XForms 模式,使得日常所用結構(如 class 和 id 屬性)能夠通過驗證。因為包含的宿主語言片段基本上沒有任何限制,這一部分還需要根據具體的用法進一步調整,如上述代碼中的通配符所示。
驗證程序工作的過程中將運行結果記錄到內存中的一個 XML 文件中。最後使用 XSL 轉換將結果轉化成 Html,並通過網絡發送出去。
相關標准
名稱空間一直是一個微妙的話題,對於作者而言更是如此。標准可以使事情更容易,針對這一問題,有兩種不同的正在開發之中的標准。
一組標准稱為 Document Schema Definition Languages(文檔模式定義語言或 DSDL,請參閱 Resources),目前它們都朝著成為 ISO Final Draft International Standard 的方向發展,進展各不相同。當前這組標准被分成十個部分,DSDL 意味著默認了整個驗證問題的復雜性。其中包括 RELAX NG 的定義(第 2 部分)、Schematron(第 3 部分)和一種從大型文檔中選擇驗證部分的類似 XForms 島的機制(第 4 部分)。DSDL 的其他部分涉及到各種不同的領域,像字符指令表驗證、結合不同模式語言的方法等。
另外一種相關標准和工具集是 OASIS 的 CAM,或者“Content Assembly Mechanism(內容組裝機制)”。這項技術可以使用業務規則定義、驗證和組合文檔,從而把模式片段組織起來,定義更大的復合文檔。
總而言之,混合名稱空間驗證是 XML 開發中一個值得挖掘的領域。這個 XForms 驗證程序仍在開發之中,也是一個很好的學習機會。