入力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"
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"
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"
id="dita-merge.bookmap1.frontmatter1.booklists1.toc1">
<fo:block start-indent="3mm + 15.0mm">Title</fo:block>
</fo:block-container>
もちろんgenerate-id()が必要な箇所も存在します.例えばテンポラリーツリーを作ったりする場合、どうしてもgenerate-id()の方が簡単な場合も存在します.しかし、最終的な出力XMLをコンペアするなら、要素のid属性にgenerate-id関数を使用するのは避けたほうが賢明でしょう.