自分は以前から、di要素なる物は欲しいと思っていたところ。理由は皆さんが述べられているように、論理的に明確になるし、取り出すのも楽だから。
ところで、哀さんが提示されているXSLTでは、思っているようなグループ化は出来ないのでは?(テンプレートを呼び出している、処理中のカレントノードリストがどんな物か分からないので、何とも言えないが)
自分ならこうしてみる。
1:<xsl:template match="dl">
2:<dl>
3:<xsl:for-each select="dt[name(preceding-sibling::*[position() = 1]) != 'dt']">
4:<xsl:variable name="id" select="concat('group', string(position()))"/>
5:<dt class="{$id}" id="{$id}">
6:<xsl:apply-templates select="child::node() | attribute::*"/>
7:</dt>
8:<xsl:variable name="firstDD" select="following-sibling::dd[position() = 1]"/>
9:<xsl:for-each select="following-sibling::dt[count(following-sibling::dd[position() = 1] | $firstDD) = 1] | following-sibling::dd[count(preceding-sibling::dt[position() = 1]/following::dd[position() = 1] | $firstDD) = 1]">
10:<xsl:element name="{name()}">
11:<xsl:attribute name="class">
12:<xsl:value-of select="$id"/>
13:</xsl:attribute>
14:<xsl:apply-templates select="child::node() | attribute::*"/>
15:</xsl:element>
16:</xsl:for-each>
17:</xsl:for-each>
18:</dl>
19:</xsl:template>
自身の直前の兄弟要素名が「dt」でないdt要素群を処理すべきカレントノードリストとし(3行目)、同じグループかそうでないかは、dt要素の場合は自身のfollowing-sibling軸の最初のdd要素と、dd要素の場合は自身のpreceding-sibling軸の最初のdt要素のfollowing-sibling軸の最初のdd要素とを、処理しているカレントノードdt要素のfollowing-sibling軸の最初のdd要素(=$firstDD)と同一かそうでないで判断している(9行目)。XPath1.0には同一のノードかそうでないかを判断する関数が無い為、ノードの和集合を取ってそれをカウント関数に掛ける事でそれを実現している。そういえば、XSLTにはgenerate-id()関数がある事を思い出した。これを使えば、ノードの同一性を判断するのは可能。いや、generate-id()なんて使った事ないから、忘れてたよ。単にノード集合同士を比較すると同じ文字列値を持つノードが有るか無いかで判断されるので、もし違うグループ内に同じ文字列値を持つdd要素をあると、それも選択されてしまうので。
...しかしながら、何とも、もどかしいです。やっぱりdi要素が欲しいと思う、今日この頃。
XSLT2.0の勉強をしようと仕様書を眺めていたら、どうもXPath2.0が重要らしいと言う事が分かった。XPath2.0はXQueryと言うものでも使う事を想定しているので、XPath1.0に較べ随分と複雑になったが、XPath2.0の使用を前提とするXSLT2.0を覚えるには不可欠なので、まずこちらから勉強を始めてみた。
XPath2.0はワーキングドラフトながら仕様書の和訳が有るので、これを主な資料とし、検証にはSaxonを使って具体的にどうなっているのかを調べ、それを書いておこうと思う。当然ながら、ここに書いてある事は鵜呑みにしないでほしい。検証にはSaxonしか使わないので、その実装が間違っている可能性もあるし、そもそも僕の理解自体が間違っている可能性が大なので...。
普通にXSLTを使っていてもあんまり気にならないのだが、XPath1.0のノード集合型には重複も無く順序も無い。
具体例を挙げてみる。
<?xml version="1.0"?>
<doc>
<a num="1"/>
<b num="2"/>
<b num="3"/>
<a num="4"/>
<b num="5"/>
</doc>
上記のXMLの『doc要素の子要素全てとそれに続けてa要素のみを足したものを出力』しようと、以下の様なXSLTを適用してみる。
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/doc">
<doc>
<xsl:for-each select="child::* | child::a"/>
<xsl:copy-of select="self::node()"/>
</xsl:for-each>
</doc>
</xsl:template>
</xsl:stylesheet>
結果は元のXMLと変わらない。何故なら、|
演算子はノードの和集合を取るもので、当然ながら重複するノードは除かれる。よって、この場合、式child::* | child::a
で返される値は式child::*
で返される値と等価なのだ。XPath1.0では、この演算子の他にノード集合を結合できないので、希望通りの結果を得たければ、2つの式を別々に処理するしかない。
また、順序も無い為に、b要素,a要素の順で出力しようと、<xsl:for-each select="child::b | child::a">
にしてみても、結果は先と同様になる。この「順序が無い」というのは少し語弊あるかもしれない。仕様上では、ノード集合は文書順(document order)となっており、文頭からの出現順に並び替えるとされている。故に、式child::b | child::a
としようとも、式child::a | child::b
としようとも同じであり、結局はこの場合も式child::*
で返される値と等価なのである。よって、希望通りの出力を得たいならば、ソートしなければならない(この場合は単純だから、<xsl:sort select="name()" data="text" order="descending"/>
とすれば良い)。
これは返されるノード集合値の事であって、ロケーションパスを評価している間は、勿論、軸によって文書順・逆文書順といった方向性を持っている。例にとれば、式child::*[position() = last()]/preceding-sibling::*[position() = 1]
から返されるノード集合は<a num="4"/>
であり、逆文書順に数えている事が分かる。しかしながら、この場合は1つのノードのみのノード集合が帰ってくるので文書順という事を意識する事は無いが、もし、[position() > 1]
とした場合は複数のノードを含むノード集合が帰ってくるので、その値は文書順に並び替えられるので注意が必要である。
もっと言えば、先の式child::*[position() = last()]/preceding-sibling::*[position() = 1]
と式(child::*[position() = last()]/preceding-sibling::*)[position() = 1]
では返されるノードが違う。後者は<a num="1"/>
が返される。何故なら、後者の方は、先にchild::*[position() = last()]/preceding-sibling::*
を評価し、その時点で文書順に並び替えられ、そのノード集合のposition() = 1
と言うように解釈されるからである。
XPath2.0では、式の値は常にシーケンスとされており、ノード集合型という考え方ではない。では、先の『doc要素の子要素全てとそれに続けてa要素のみを足したものを出力』するにはどうすれば良いのか。結論から言うと、以下の様にする事で可能である。
【XSLT】<?xml version="1.0"?> <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/doc"> <doc> <xsl:for-each select="(child::* , child::a)"/> <xsl:copy-of select="self::node()"/> </xsl:for-each> </doc> </xsl:template> </xsl:stylesheet>
【出力結果】<?xml version="1.0"?> <doc> <a num="1"/> <b num="2"/> <b num="3"/> <a num="4"/> <b num="5"/> <a num="1"/> <a num="4"/> </doc>
括弧で囲んであるものはシーケンス式と言い、コンマによって結合する事が出来る。上のxsl:for-each
の場合、まず式child::*
と式child:a
が評価される。この式の値はXPath1.0と同様であるが、返されるのがシーケンスという違いがある。イメージで言うと式child::*
は(child:*[1],child:*[2],child:*[3],child:*[4],child:*[5])
というシーケンスが、式child::a
は(child:a[1],child:a[2])
というシーケンスが返され、それがコンマによって結合され、(child:*[1],child:*[2],child:*[3],child:*[4],child:*[5],child:a[1],child:a[2])
というシーケンスに成り、それを順番に処理するという形になるのである。式(child::b,child::a)
も同様である。こちらの場合も(child:b[1],child:b[2],child:b[3],child:a[1],child:a[2])
に成り、b要素,a要素の順に出力される。
無論、コンテキストというものも有効である。その対象がノードではなく、シーケンスの内容(仕様書ではシーケンスの要素、itemと呼ばれている)に変わっただけである。例えば、count((child::*,child::a))
ならば7という数値が返り、(child::*,child::a)[position() = 6]
ならば2つ目の<a num="1"/>
のノードが返ってくる(厳密に言うと返ってくるのは、シーケンス) 。
順序があるならば、「逆文書順でノードを絞り込めば逆順になるのでないか」と考えるかもしれない。先の例でみると、式child::*[position() = last()]/preceding-sibling::*
ならば、返らされるシーケンスは
になると考えがちだが、実はそうはならない。XPath2.0では、ロケーションパスによる式をパス式と呼んでいる。このパス式で得られるノード・シーケンスはXPath1.0同様、やはり文書順に並び替えられるのだ。これは一見おかしいように思えるが、考えてみれば当然だ。もし、XPath2.0でこれを逆順にすると、XPath1.0の使用を前提とするXSLT1.0スタイルシートを、バージョンに「2.0」しようと思い、(child:*[5],child:*[4],child:*[3],child:*[2])(child:*[4],child:*[3],child:*[2],child:*[1])<xsl:stylesheet version="2.0">
と変えただけで出力される結果が違くなってしまい、後方互換を保つことが出来なくなる。よって、このような場合は、やはりXSLT1.0同様にソートをするか、若しくはシーケンスの順序を反対にするreverse()関数を使い<xsl:for-each select="reverse(child::*[position() = last()]/preceding-sibling::*)">
にすれば、希望通りの出力が得られるのである。
ちなみ、XPath1.0のノード集合で示したXSLTの例文をversion="2.0"
にしただけでは、出力結果はXSLT1.0の時と同じになる。何故なら|
演算子は、XPath2.0でもやはりノード・シーケンスの和集合を取り重複するノードは除かれ、その結果を文書順で返すからである(これについては、またいずれ)。
ちなみにこの新聞、シドニーで高橋尚子が金メダルを穫った時もトップ記事として扱いませんでした。
ニッカンのニュース価値観は、理解し難い。
ネットを眺めていると、XPath2.0周辺に対する芳しくない意見をたびたび目にする。便利になった分だけ、仕様が複雑になった為だ。
意見を述べる程の知識が無い僕にとってみれば、理解する事が最優先であって意見は二の次というところであるが、到底理解しているとは言い難い。特にstrongly-typed language(強く型付けられた言語)
と言うところが僕にとっての鬼門で、この辺に関する仕様がよく分かっていない。何度か読み返してみるもいまいち良く分からないので、その辺は実践で確認してみて、そのエラーメッセージを参考に関連する仕様書を読むという形でやっている。まあ、良く理解するには、勧告され参考書が揃ってからと言うところでしょうか。
XSLT1.0では、任意の繰り返しと言う作業が結構面倒い。例えば、指定された数値分だけアスタリスクを出力したいとなると、一般的には再帰処理をする事になると思う(数値分のノード集合をxsl:for-each
で処理する言う方法もある。詳しくは『dW : XML : ヒント: ノード・セットによるカウント』の記事を見て下さい)。
【XML】<?xml version="1.0"?> <root> <num>4</num> <num>32</num> </root>
【XSLT1.0】<?xml version="1.0"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <result> <xsl:apply-templates select="root/num"/> </result> </xsl:template> <xsl:template match="num"> <out> <xsl:call-template name="repeat"> <xsl:with-param name="num" select="number(.)"/> </xsl:call-template> </out> </xsl:template> <xsl:template name="repeat"> <xsl:param name="num"/> <xsl:if test="1 <= $num"> <xsl:text>*</xsl:text> <xsl:call-template name="repeat"> <xsl:with-param name="num" select="$num - 1"/> </xsl:call-template> </xsl:if> </xsl:template> </xsl:stylesheet>
【出力】<?xml version="1.0"?> <result> <out>****</out> <out>********************************</out> </result>
この繰り返しと言う作業は、XPath2.0のシーケンスのお陰で随分と記述が楽になる。
【XSLT2.0(該当するテンプレートのみ)】
<xsl:template match="num">
<out>
<xsl:for-each select="1 to .">
<xsl:text>*</xsl:text>
</xsl:for-each>
</out>
</xsl:template>
to演算子は、2つの被演算子とその間にある整数を含むシーケンスを作ります。式1 to 4
ならば(1,2,3,4)
というシーケンスを作り、式10 to 10
ならば(10)
と言った具合です(最新のワークングドラフトによると、10 to 3
の様に2つ目の被演算子が1つ目のものより小さい場合は、空のシーケンスを返すとされています)。上例では、式1 to .
によってnum要素の数値分の整数を持つシーケンスが作られ、そのシーケンスをxsl:for-each
で処理するという形になり、数値分のアスタリスクが出力されます(何故、1 to number(.)
にしないのかと言うと、to演算子はXMLスキーマで言うところのinteger型しかとれないからです。number()関数はdouble型の数値を返すため、使用するとSaxonではエラーになってしまいます)。
.
シーケンスは色々な型を含められ、数値型・文字列型やノード型といったものを混ぜる事が出来ます。よって、以下の様な事も可能です。
【XML】<root> <item>3</item> <item>ダァー!!</item> </root>
【XSLT2.0】<xsl:template match="root"> <result> <xsl:for-each select="('1',2,item)"> <out> <xsl:value-of select="."> </out> </xsl:for-each> </result> </xsl:template>
【出力】<result> <out>1</out> <out>2</out> <out>3</out> <out>ダァー!!</out> </result>
この例では、式('1',2,item)
によって、('1',2,item[1],item[2])
というシーケンスが作られ、それを順次処理しています。xsl:value-of
の裏で色々な型処理が行われているのはご存知だと思いますが、ここで注意する事はselect
属性の式.
はself::node()
の短縮形という意味ではないと言う事です。この.
は、現在処理されているコンテキスト要素(カレント要素?)を返す式です。シーケンスという概念のXPath2.0では、ノード以外も処理が可能です。数値や文字列といったものはノードとは違い、軸というものが存在しません。よって、ここをself::node()
とすると、処理できずにエラーになってしまいます。但し、パス式内やノードを処理する際においては、XPath2.0では、self::node()
の短縮形としての.
は有効です。ですが、XPath2.0では混乱を避ける為に短縮形は使わない方が良いように思えます。self::node()
の短縮形というものが存在しないのかも知れない(仕様書を見るに『.
はself::node()
の短縮形である』とは書かれていない)。コンテキスト要素がノードの時に限って、.
とself::node()
が等価であると考えた方が無難かも。
ちなみに、xsl:apply-templates
では、式('1',2,item)
を処理する事は出来ません。xsl:for-each
では色々な型を含むシーケンスを処理できますが、xsl:apply-templates
ではノードのみを含むシーケンスしか処理できないからです。