ツリーのノードの巡回

最近XSLTのレクチャーをやる機会がありましした.XSLTXMLファイルを入力としてそれをツリー構造(ソースツリー)として扱い、変換を行って同じくツリー構造を生成します.(結果ツリー)このソースツリーは言ってみればノードの集合体なのですが、これを巡回してツリーを構成するノードの情報を出すタイルシートを作成することをやってもらいました.

仕様は以下のようなものです.

1.ノード単位に<node>~</node>という要素を出力します.
2.ノードの種類をnode-typeという属性で出します.
3.ノードに名前があるときそれをnameという属性で出します.
4.ノードにnamespace-uri()が定義されているとき、namespace-uriとlocal-nameという情報を出します.
5.1のテキストノードで、そのノードの文字列値を出します.

入力のXMLは次のように簡単なものです.

<?xml version="1.0" encoding="UTF-8" ?>
<doc>
<title>Document title</title>
<chapter toc="yes">
<!-- これはコメントです-->
<?pi これは処理命令です.?>
<ah1:p xmlns:ah1="urn:x-antennahouse-1" xmlns:ah2="urn:x-antennahouse-2" ah2:break-before="page">The quick brown fox jumps over the lazy dog</ah1:p>
</chapter>
</doc>

最初に作ってもらったのは非常にオーソドックスなものでした.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="2.0">
    <xsl:output indent="yes"/>
    
    <xsl:template match="/">
        <root>
            <node>
                <xsl:attribute name="node-type">document-node</xsl:attribute>
                <xsl:value-of select="string(.)"/>
            </node>
            <xsl:apply-templates/>
        </root>
    </xsl:template>
    
    <xsl:template match="element()">
        <node>
            <xsl:attribute name="node-type">element</xsl:attribute>
            <xsl:choose>
                <xsl:when test="string(namespace-uri())">
                    <xsl:attribute name="namespace-uri" select="namespace-uri()"/>
                    <xsl:attribute name="local-name" select="local-name()"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:attribute name="name" select="name()"/>
                </xsl:otherwise>
            </xsl:choose>
            <xsl:apply-templates select="attribute::node()"/>
            <xsl:value-of select="string(.)"/>
        </node>
        <xsl:apply-templates/>
    </xsl:template>

    <xsl:template match="attribute()">
        <node>
            <xsl:attribute name="node-type">attribute</xsl:attribute>
            <xsl:choose>
                <xsl:when test="string(namespace-uri())">
                    <xsl:attribute name="namespace-uri" select="namespace-uri()"/>
                    <xsl:attribute name="local-name" select="local-name()"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:attribute name="name" select="name()"/>
                </xsl:otherwise>
            </xsl:choose>
            <xsl:value-of select="string(.)"/>
        </node>
    </xsl:template>

    <xsl:template match="text()">
        <node>
            <xsl:attribute name="node-type">text</xsl:attribute>
            <xsl:value-of select="string(.)"/>
        </node>
    </xsl:template>

    <xsl:template match="comment()">
        <node>
            <xsl:attribute name="node-type">comment</xsl:attribute>
            <xsl:value-of select="string(.)"/>
        </node>
    </xsl:template>
    
    <xsl:template match="processing-instruction()">
        <node>
            <xsl:attribute name="node-type">processing-instruction</xsl:attribute>
            <xsl:value-of select="string(.)"/>
        </node>
    </xsl:template>
</xsl:stylesheet>

ポイントは要素ノードで属性ノードも処理するようにxsl:apply-templatesしているところです.この結果は次のようになります.

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <node node-type="document-node">
Document title
The quick brown fox jumps over the lazy dog
</node>
   <node node-type="element" name="doc">
Document title
The quick brown fox jumps over the lazy dog
</node>
   <node node-type="text">
</node>
   <node node-type="element" name="title">Document title</node>
   <node node-type="text">Document title</node>
   <node node-type="text">
</node>
   <node node-type="element" name="chapter">
      <node node-type="attribute" name="toc">yes</node>
The quick brown fox jumps over the lazy dog
</node>
   <node node-type="text">
</node>
   <node node-type="comment"> これはコメントです</node>
   <node node-type="text">
</node>
   <node node-type="processing-instruction">これは処理命令です.</node>
   <node node-type="text">
</node>
   <node node-type="element"
         namespace-uri="urn:x-antennahouse-1"
         local-name="p">
      <node node-type="attribute"
            namespace-uri="urn:x-antennahouse-2"
            local-name="break-before">page</node>The quick brown fox jumps over the lazy dog</node>
   <node node-type="text">The quick brown fox jumps over the lazy dog</node>
   <node node-type="text">
</node>
   <node node-type="text">
</node>
</root>

はじめてやる人にはこのノードの多さに驚くようです.XMLを処理する上で、ホワイトスペースノードがあちこちに存在することはまず覚えていただかないといけませんね.

さてこのスタイルシートは動くには動いてくれるのですが、ノードがバラバラに処理されており、いまいち面白くありません.以下は一気にテンプレートを集約したバージョンです.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
 exclude-result-prefixes="xs"
>
    <xsl:output indent="yes"/>

    <xsl:template match="/">
        <root>
            <xsl:call-template name="process-node"/>
        </root>
    </xsl:template>
    
    <xsl:template match="child::node()|attribute::node()" name="process-node">
        <node>
            <xsl:attribute name="node-type">
                <xsl:choose>
                    <xsl:when test="self::document-node()">document-node</xsl:when>
                    <xsl:when test="self::element()">element</xsl:when>
                    <xsl:when test="self::attribute()">attribute</xsl:when>
                    <xsl:when test="self::text()">text</xsl:when>
                    <xsl:when test="self::comment()">comment</xsl:when>
                    <xsl:when test="self::processing-instruction()">processing-instruction</xsl:when>
                    <xsl:otherwise/>
                </xsl:choose>
            </xsl:attribute>
            <xsl:choose>
                <xsl:when test="string(namespace-uri())">
                    <xsl:attribute name="namespace-uri" select="namespace-uri()"/>
                    <xsl:attribute name="local-name" select="local-name()"/>
                </xsl:when>
                <xsl:when test="string(name())">
                    <xsl:attribute name="name" select="name()"/>
                </xsl:when>
                <xsl:otherwise/>
            </xsl:choose>
            <xsl:apply-templates select="attribute::node()"/>
            <xsl:value-of select="string(.)"/>
        </node>
        <xsl:apply-templates/>
    </xsl:template>
</xsl:stylesheet>

ずいぶん簡単になりました、要素も属性もすべてノードです.なので1つのテンプレートで処理できるのです.ポイントは    <xsl:template match="child::node()|attribute::node()" name="process-node">というところです.これを単に<xsl:template match="node()" name="process-node">とやると属性ノードが処理されてくれません.そのとおり、これは<xsl:template match="child::node()" name="process-node">と書いているのと同じだからです.attributeノードは意図時にパターンに記述さしてやらないといけません.

5.5 Patterns

Informally, a Pattern is a set of path expressions separated by |, where each step in the path expression is constrained to be an AxisStep that uses only the child or attribute axes.

あとこれにnamespace-nodeの内容を出力するにはどうしたらよいでしょうか?意外と難しいです.お暇な方挑戦してみてください.なかなかノードの巡回という基本的なことでも奥が深いです.