generate-id()は使うべきではない!?

入力XMLにユニークなidがあれば良いのですが、丁度良いものがなくて出力XMLにidが必要な場合、よくgenerate-id関数を使用します.今までなんの疑問もなく使用してきたのですが、致命的な弱点に出会ってしまいました.

例えばスタイルシートに非常に微妙な修正を施して、修正前後でデグレードしていないかチェックするのに一番簡単な方法は出力XMLファイル(ものによってはHTMLファイル)をWinMergeなどで比較することです.修正前後を比較すれば、修正が適切であったか?思わぬ箇所に影響が及んでいないか?がよくわかります.ところがいざ比較してガッカリするのですが、generate-id()の値が違っている場合が多々あるのです.

以下の例はgenerate-id()を使用してXSL-FOを生成した場合です.

   <fo:block-container font-weight="bold"
                       absolute-position="auto"
                       width="182mm - 15.0mm - 15.0mm"
                       height="24.417mm - 3mm - 10mm"
                       space-before="24.417mm - 3mm - 10mm"
                       space-before.conditionality="retain"
                       font-family="Arial,Lucida Sans Unicode"
                       font-size="24pt"
                       line-height="1.0"
                       color="rgb-icc(#CMYK,0%,0%,0%,0%)"
                       id="d62e240">
      <fo:block start-indent="3mm + 15.0mm">Title</fo:block>
   </fo:block-container>

   <fo:block-container font-weight="bold"
                       absolute-position="auto"
                       width="182mm - 15.0mm - 15.0mm"
                       height="24.417mm - 3mm - 10mm"
                       space-before="24.417mm - 3mm - 10mm"
                       space-before.conditionality="retain"
                       font-family="Arial,Lucida Sans Unicode"
                       font-size="24pt"
                       line-height="1.0"
                       color="rgb-icc(#CMYK,0%,0%,0%,0%)"
                       id="d64e240">
      <fo:block start-indent="3mm + 15.0mm">Title</fo:block>
   </fo:block-container>

ご覧いただくとわかるように、id属性の一部が"d62e240"と"d64e240"で1文字だけ違っています.スタイルシートで景気よくgenerate-id()を使っていると、このような箇所が山ほど出てきてしまい、結果を比較してもid属性の差ばかりで、全然デグレードのチェックにならない場合が出てきてしまいます.

何故このようなことになってしまうのでしょうか?generate-id(node()?)は、入力パラメータのノードに応じてユニークな文字列を生成してくれますが、その生成する値はXSLTプロセッサの起動毎に異なってしまうようです.

16.6.4 generate-id

An implementation is under no obligation to generate the same identifiers each time a document is transformed. There is no guarantee that a generated unique identifier will be distinct from any unique IDs specified in the source document.

しかしこれでは実務上使い物になりません.同じXMLファイルを入力させたら同じid値を生成するようには出来ないのでしょうか?

答えは入力XMLファイルの要素の位置によりidを生成するようにすることです.次の関数はそれを行ってくれます.

    <xsl:function name="ahf:getHistoryId" as="xs:string">
        <xsl:param name="prmElem" as="element()"/>
        <xsl:variable name="ancestorElem" as="element()+" select="$prmElem/ancestor-or-self::*"/>
        <xsl:variable name="historyStr" as="xs:string*">
            <xsl:for-each select="$ancestorElem">
                <xsl:variable name="elem" select="."/>
                <xsl:variable name="name" as="xs:string" select="name()"/>
                <xsl:sequence select="if (position() gt 1) then '.' else ''"/>
                <xsl:sequence select="local-name()"/>
                <xsl:sequence select="if (exists($elem/parent::*)) then string(count($elem/preceding-sibling::*[name() eq $name]) + 1) else ''"/>
            </xsl:for-each>
        </xsl:variable>
        <xsl:sequence select="string-join($historyStr,'')"/>
    </xsl:function>

この関数をgenerate-id()の代わりに使えば上記の例は次のように同じid値となります.idの内容からわかるように、この入力XMLファイルはDITAです.

   <fo:block-container font-weight="bold"
                       absolute-position="auto"
                       width="182mm - 15.0mm - 15.0mm"
                       height="24.417mm - 3mm - 10mm"
                       space-before="24.417mm - 3mm - 10mm"
                       space-before.conditionality="retain"
                       font-family="Arial,Lucida Sans Unicode"
                       font-size="24pt"
                       line-height="1.0"
                       color="rgb-icc(#CMYK,0%,0%,0%,0%)"
                       id="dita-merge.bookmap1.frontmatter1.booklists1.toc1">
      <fo:block start-indent="3mm + 15.0mm">Title</fo:block>
   </fo:block-container>

この関数では、要素の名前と順序番号を"."で区切って階層順にidの値として使っています.このような工夫でid属性値の差は出なくなりますから、スタイルシートデグレードチェックは非常に助かります.

もちろんgenerate-id()が必要な箇所も存在します.例えばテンポラリーツリーを作ったりする場合、どうしてもgenerate-id()の方が簡単な場合も存在します.しかし、最終的な出力XMLをコンペアするなら、要素のid属性にgenerate-id関数を使用するのは避けたほうが賢明でしょう.