重複したtopic/@idを拾い出す

DITAでオーサリングされる方ならだれでもtopic/@idはDTDでいうところのID属性だから、そのトピックの中でユニークじゃなければならないと教わります.ではmapやbookmapで多くのトピックを束ねる場合はどうなのでしょうか?マップに対応する文書全体でtopic/@idはユニークでなければならないのでしょうか??
実はDITAの仕様、DITA-OTの実装は文書全体でtopic/@idがユニークであることを強制していません.同じtopic/@idがあっても別ファイルならば関係ありません.同じファイル名でもファイルシステム上で階層が別になっていれば問題になりません.ずいぶんと緩やかなのです.
ただしCMSは違います.CMSにもいろいろありますが、トピックを一意のオブジェクトIDで識別するような(つまりCMSの中で物理的なファイルシステムのような階層を持たない)アーキテクチャCMSの場合、システム全体でtopic/@idはユニークであることが要求されます.もちろんCMSのシステムがそれを保証しています.
さて今回はたまたま文書全体でtopic/@idをユニークにしなければならないという要件が発生してしまいました.フツーはそんなこと要らないので、ごく特殊な場合です.
topic/@idをユニークにしなければならないということは、実は「もしもユニークでない場合」、そのユニークでないtopic/@idを利用者にわかるように出してやらねばなりません.このユニークでないというのは、いつもトピックをOxygenのようなXMLエディタで、テンプレートからこしらえればなんの問題もなくOxygenが自動的にユニークなtopic/@idを振ってくれます.でもですね、いったん作ったトピックを、エディタの「ファイル」- 名前をつけて保存」としてしまうと一発でtopic/@idの重複は発生してしまいます.
これって意外と発生しやすいので、XSLTスタイルシートで処理するときのチェックは非常に大事になります.
ユニークでない、つまり重複しているものを拾い出すって、意外とやったことがないものです.
でもググれば世界には同じことを考えている方はかならずいらっしゃいます.今回見つけたのはXSLTの鬼才Dimitre Novatchevの方法です.
stackoverflow.com

$vSeq[index-of($vSeq,.)[2]]

たったこれだけです. $vSeq は重複をチェックするシーケンスです.この謎解きは上記のStackoverflowの記事か以下の解説を読むと良いです.

XPath 2.0 Gems: Find all duplicate values in a sequence (Part 2)
dnovatchev.wordpress.com

私の場合、最終はPDFでしたので、すべての文書のマップとトピックは「マージ後中間ファイル」にまとまります.以下が模式的に、その中間ファイルを処理してチェックしているコードです.(全部は載せてないのでイメージだけご理解ください)

    <!-- map or bookmap -->
    <xsl:variable name="root"  as="element()" select="/*[1]"/>
    <xsl:variable name="map" as="element()" select="$root/*[contains-token(@class,'map/map')][1]"/>
    <xsl:variable name="topics" as="element()*" select="($root/* except $map)/descendant-or-self::*[contains-token(@class,'topic/topic')][@oid => exists()]"/>

    <xsl:variable name="oidSeq" as="xs:string*">
        <xsl:for-each select="$topics">
            <xsl:variable name="topic" select="."/>
            <xsl:sequence select="$topic/@oid => string()"/>
        </xsl:for-each>
    </xsl:variable>

    <xsl:template match="/">
        <xsl:call-template name="oidDupicateCheck"/>
        <xsl:copy>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="oidDupicateCheck">
        <xsl:variable name="duplicates" as="xs:string*" select="$oidSeq[index-of($oidSeq,.)[2]]"/>
        <xsl:if test="$duplicates => exists()">
            <xsl:call-template name="errorExit">
                <xsl:with-param name="prmMes" select="ahf:replace($stMes1012,('%dup'),($duplicates => string-join(',')))"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>

テストしてみるとバッチリ動きました.いやXSLTXPath)って奥が深いです.
※ しかし、はてなのコードハイライトは実にイマイチですねぇ.