XSLT 3.0への道(35) xsl:accumulatorでスタックを作る

「XMLの鬼門:処理命令」で、oXygenなどのエディタが生成する処理命令は通常のXSLTスタイルシートでは処理が難しいことを説明しました.
ではこれを解決するのは何か?恥ずかしながら、以下のSaxonのドキュメントを見るまで知りませんでした.
saxon:assign
https://www.saxonica.com/html/documentation/extensions/instructions/assign.html

あたりまえですがXSLTでは変数は一回初期化したら、そのあとは値を変更できませんでした.つまりimmutableの性質を持ちます.当然、再代入する命令は提供されていません.でもsaxon:assignはこれを可能にしちゃうのです.

例えば

<xsl:variable name="stack" as="processing-instruction()*" saxon:assignable="yes" select="()"/>

グローバル変数でやっておいて、

<xslt:template match="processing-instruction()">

で処理命令を見張っていて、もし開始の処理命令にマッチしたら

<saxon:assign name="stack" select="(., $stack)"/>

とやってスタックにpushし、もし終了の処理命令にマッチしたら、

<saxon:assign name="stack" select="remove($stack,1)"/>

とスタックからpopしてやるというものです.こうして、node()の処理でこのスタックを参照して、先頭を拾って(peek)、空でなければそのnode()は「処理命令の中に存在する」とみなせば良いからです.

でもsaxon:assignは有償のSaxon PE/EEがないと使えません.そして上記のURLのドキュメントには、

This instruction works only with global variables. It should be regarded as deprecated, and may be withdrawn completely at some time in the future, since it is incompatible with many of the optimizations that Saxon now performs.
There are better ways of achieving the same effect. Consider constructs such as tunnel parameters, xsl:iterate, higher-order functions, or xsl:accumulator.

とあったからです.将来のバージョンではなくなっちゃうのでは困ります.そこで見つけたのがxsl:accumulator でした.xsl:accumulatorはSaxon-HE(つまりオープンソース版)でも動作してくれます.これでスタックを実装するのは実はごく簡単で、次のようにコーディングしてやれば自動的にやってくれるのです.

    <xsl:variable name="cCommentStartPiName" as="xs:string" static="yes"  select="'oxy_comment_start'"/>
    <xsl:variable name="cCommentEndPiName" as="xs:string" static="yes"  select="'oxy_comment_end'"/>
    <!--コメント開始の処理命令を判定する-->
    <xsl:function name="ahf:isCommentStartPi" as="xs:boolean">
        <xsl:param name="prmNode" as="node()"/>
        <xsl:sequence select="$prmNode/self::processing-instruction()[name() eq $cCommentStartPiName] => exists()"/>
    </xsl:function>
    <!--コメント終了の処理命令を判定する-->
    <xsl:function name="ahf:isCommentEndPi" as="xs:boolean">
        <xsl:param name="prmNode" as="node()"/>
        <xsl:sequence select="$prmNode/self::processing-instruction()[name() eq $cCommentEndPiName] => exists()"/>
    </xsl:function>

    <xsl:accumulator name="glCommentPi" as="processing-instruction()*" initial-value="()">
        <!-- 開始の処理命令にマッチしたらpushする-->
        <xsl:accumulator-rule match="processing-instruction()[ahf:isCommentStartPi(.)]" select="(., $value)"/>
        <!--終了の処理命令にマッチしたらpopする-->
        <xsl:accumulator-rule match="processing-instruction()[ahf:isCommentEndPi(.)]" select="remove($value,1)"/>
    </xsl:accumulator>

この宣言だけでスタックのpush/popを自動化できてしまいます.あとはテキストノードの処理で、スタックの状態を見に行けば良いだけです.このxsl:accumulatorの中を覗くのが以下の変数です.

<xsl:variable name="commentStartPi" as="processing-instruction()?" select="accumulator-before('glCommentPi') => head()"/>

accumulator-before('glCommentPi') => head()は、そのtext()がマッチした直前のスタックの先頭を拾ってきてくれます.もし$commentStartPi => empty()なら、コメントの処理命令に囲まれておらず、$commentStartPi =>exists()なら、コメントの処理命令に囲まれています.

次のサンプルトピックを使って

<?xml version="1.0"?>
<!DOCTYPE topic PUBLIC "-//OASIS//DTD DITA Topic//EN" "topic.dtd">
<topic id="topic_55762342DAC09974FDC8" xml:lang="en-US">
	<title>Typographic elements testing</title>
	<shortdesc>The typographic elements are used to highlight text with styles (such as bold, italic, and monospace). Never use these elements when a semantically specific element is available. These elements are not
intended for use by specializers, and are intended solely for use by authors when no semantically
appropriate element is available and a formatting effect is required.</shortdesc>
	<body>
    <section>
            <title>sup/sub element</title>
            <p>The superscript (<xmlelement>sup</xmlelement>) element indicates that text should be
                superscripted, or vertically raised in relationship to the surrounding text.
                Superscripts are usually a smaller font than the surrounding text. Use this element
                only when there is not some other more proper tag. This element is part of the
                <?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
これは通常のXSLTスタイルシートでは処理しようがありません."?>DITA
                highlighting domain.</p>
            <p>A subscript (<xmlelement>s<?oxy_comment_end?>ub</xmlelement>) indicates that text
                should be subscripted, or placed lower in relationship to the surrounding text.
                Subscripted text is often a smaller font than the surrounding text. Formatting may
                vary depending on your output process. This element is part of the DITA highlighting
                domain.</p>
            <p>The power produced by the electrohydraulic dam was 10<sup>10</sup> more than the
                older electric plant. The difference was H<sub id="sub_99876530FDA634CE2A34"
                >2</sub>O.</p>
        </section>
    
    </body>
</topic>
<?oxy_options track_changes="on"?>

次のようなテンプレートで検証してみると

    <xsl:template match="text()>
        <xsl:variable name="commentStartPi" as="processing-instruction()?" select="accumulator-before('glCommentPi') => head()"/>
        <xsl:variable name="isCommented" as="xs:boolean" select="$commentStartPi => exists()"/>
        <!-- ahf:getHistoryXpathStr()はノードに対応するXPathを表示-->
        <xsl:variable name="xpath" as="xs:string" select="ahf:getHistoryXpathStr(.)"/>
        <xsl:choose>
            <xsl:when test="$isCommented">
                <xsl:message select="'XPath='|| $xpath||' comment PI=',$commentStartPi"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:message select="'XPath='|| $xpath||' comment PI: none'"/>
            </xsl:otherwise>
        </xsl:choose>
        ...
    </xsl:template>

次のようなデバッグ出力を得ることができます.つまり、処理命令の始まりと終わりを捕捉できています

     [xslt] XPath=/topic/text()[1] comment PI: none
     [xslt] XPath=/topic/title[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/text()[2] comment PI: none
     [xslt] XPath=/topic/shortdesc[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/text()[3] comment PI: none
     [xslt] XPath=/topic/body[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/title[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/xmlelement[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[3] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/text()[3] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/text()[1] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/xmlelement[1]/text()[1] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/xmlelement[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[4] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/sup[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/sub[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[3] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[5] comment PI: none
     [xslt] XPath=/topic/body[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/text()[4] comment PI: none

これを応用すれば、PDFに以下のように出力することも可能です.

f:id:toshi_xt500:20200723205515p:plain
処理命令によるコメントをPDFで表す(コメントアイコンは見やすいように移動してあります)

XSLT 3.0は痒い所に手が届くようなxsl:accumulatorを用意してくれました.使わなければ損ですね.

※ ちなみにoXygenのコメント機能は、さらに複雑で次のような複数のauthorによるコメントの「範囲の入れ子」「コメントに対するコメント」ができちゃいます.このような場合、スタックの処理は上記で紹介したものよりずっと複雑になります.

f:id:toshi_xt500:20200723210849p:plain
入れ子のコメント、コメントに対するコメント