xsl:apply-templates

普段はほとんどXSLT2.0を使っていますが、どうしても使えない場合が多々あります.どういう場合かと言いますともう10年越しで運用しているXSLT1.0のスタイルシートをメンテナンスするような時です.もちろんXSLT2.0に書き直すに越したことはないのですが、それ自体がテストも含めて相当工数がかかりお客様のサーバーのコンフィグレーションも変更していただかねばなりません.お金もかかってしまうのでやはりXSLT2.0という訳にはゆきません.

そんなコードを見ていると次のようなものがありました.元が本来一つなのにインラインの要素に分割されてしまったものを統合するものです.(普通はわざわざこんなオーサリングはしないのですが、オーサリングがXMLエディタでなくWordなので、Wordの気分で勝手にインラインが分割されてしまうのです.)

[入力のXML]

<i>qui</i><i>ck</i>

[出力のXML]

<i>quick</i>

[コード]
<xsl:template match="i|b|u">
    <!--同じ名前の要素が連続する場合最初のもののみ処理します-->
    <xsl:if test="name(preceding-sibling::node()[1]) != name()">
        <!--続く同じ名前の要素の下位ノードを格納する変数です-->
        <xsl:variable name="childNodes">
            <xsl:copy-of select="child::node()"/>
            <xsl:for-each select="following-sibling::node()[1]">
                <xsl:call-template name="getChildNodes">
                    <xsl:with-param name="prmElemName" select="name()"/>
                </xsl:call-template>
            </xsl:for-each>
        </xsl:variable>
        <xsl:element name="{name()}">
            <xsl:apply-templates select="@*"/>
            <xsl:copy-of select="$childNodes"/>
        </xsl:element>
    </xsl:if>
</xsl:template>

<xsl:template name="getChildNodes">
    <xsl:param name="prmElemName"/>
    <xsl:variable name="curElemName" select="name()"/>
    <xsl:if test="string($prmElemName) = $curElemName">
        <xsl:copy-of select="child::node()"/>
        <xsl:for-each select="following-sibling::node()[1]">
            <xsl:call-template name="getChildNodes">
                <xsl:with-param name="prmElemName" select="$prmElemName"/>
            </xsl:call-template>
        </xsl:for-each>
    </xsl:if>
</xsl:template>

ところがこのコードには問題がありました.たまたまi,b,uのインライン要素が入れ子になって分割されている場合、最初の階層は統合できますが、それ以降の階層は統合できないのです.

[入力XML]
<i><b><u>qu</u></b></i><i><b><u>i</u></b></i><i><b><u>ck</u></b></i>

[出力XML⇒ i要素しか統合できません]
<i><b><u>qu</u></b><b><u>i</u></b><b><u>ck</u></b></i>

これをXSLT1.0でなんとか次のようにしたいというのが課題です.

<i><b><u>quick</u></b></i>

一瞬頭が凍りついてしまいました.XSLT2.0ならいろいろ「技」を駆使できるんでしょうが、ともかくXSLT1.0のSaxon 6.5.5の世界でやらねばなりません.でもはっとひらめいたのですが、要するに再帰すればできるんじゃないか?ということです.再帰といっても関数なんかはXSLT1.0では使えません.でもxsl:apply-templatesを使えばできてしまいます.次がその方法です.

(1) スタイルシートのバージョンを1.1にします.これはSaxon 6.5.5に特有ですが、もう今後XSLTプロセッサを変えることも考えられないので可とします.これによりテンポラリツリーにXPathを使えるようになります.

(2) 上記のコードで <xsl:copy-of select="$childNodes"/> となっているところを次のように書き換えます.実質3ステップの追加です.

<xsl:choose>
    <xsl:when test="$childNodes/*">
        <xsl:apply-templates select="$childNodes/node()"/>
    </xsl:when>
    <xsl:otherwise>
        <xsl:copy-of select="$childNodes"/>
    </xsl:otherwise>
</xsl:choose>

これだけです.結果はちゃんと

<i><b><u>quick</u></b></i>

になってくれます.XSLTはやはり筋の良い言語です.XSL-Listで変なテクニックを使うより xsl:apply-templates を使えというアドバイスを読んだことがありますがそのとおりだと思いました.

このサンプルコードは以下からダウンロードできます.