XSLT3.0への道(19) static変数とuse-when属性

XSLT 3.0にずっと触れる機会がありませんでした.先日たまたま<xsl:styleeheet version="3.0">とコーディングする機会があり、久しぶりに3.0の機能を使ってみました.W3Cを見ると昨年の10月でWorking DraftのLast Callという状態になっていますね.

さてせっかくなので3.0で便利になった機能を一つ紹介してみたいと思います.それはstatic変数とそれを使用したuse-when属性です.static変数(パラメータ)は一般のプログラミング言語でもなじみのある呼称ですが、XSLT 3.0では次の箇所で触れられています.

9.6 Static Variables and Parameters

でもなかなかこれを見てもわかりづらいです.厳格ではなくパッと見ての特徴は

1. static="yes"と書いて変数を宣言する.
2. グローバル変数(パラメータ)でなければならない.
3. 変数の値はスタイルシートコンパイル時に決定される、必ずselect属性を書かねばならない.
4. select属性のXPath式で前方参照はできない.

というところでしょう.では何故このようなstatic変数(またはパラメータ)が有用なのかといいますと、use-when属性のなかで使えるからです.

例えばDITAに限らず、XSL-FOを生成するスタイルシートで用紙サイズを動的に変更したいというお客様からの要望はよくあります.用紙サイズをパラメータでスタイルシートに渡すとするとXSLT 2.0では例えばこんなコーディングになっていたと思います.(少し冗長なコードですが、大文字は外部とやり取りをするパラメータで内部からの参照は小文字の変数と私が勝手に決めているためです)

    <xsl:param name="PRM_PAPER_SIZE" as="xs:string" required="no" select="'A5'"/>
    <xsl:variable name="prmPaperSize" as="xs:string" select="$PRM_PAPER_SIZE"/>
    
    <xsl:attribute-set name="atsPage">
        <xsl:attribute name="page-width">
            <xsl:choose>
                <xsl:when test="$prmPaperSize eq 'A4'">210mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'A5'">148mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'B4'">257mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'B4'">182mm</xsl:when>
            </xsl:choose>
        </xsl:attribute>
        <xsl:attribute name="page-height">
            <xsl:choose>
                <xsl:when test="$prmPaperSize eq 'A4'">297mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'A5'">210mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'B4'">364mm</xsl:when>
                <xsl:when test="$prmPaperSize eq 'B4'">257mm</xsl:when>
            </xsl:choose>
        </xsl:attribute>
    </xsl:attribute-set>
    
    <xsl:template match="/">
        <fo:root>
            <fo:layout-master-set>
                <fo:simple-page-master master-name="spmCommon" xsl:use-attribute-sets="atsPage">
                    <fo:region-body/>
                </fo:simple-page-master>
            </fo:layout-master-set>
        </fo:root>
    </xsl:template>

出力はこうなります.FOの片割れですのでこのままでは動きません.あくまでxsl:attribute-setが反映されていることの確認用です.

   <fo:layout-master-set>
      <fo:simple-page-master page-width="148mm" page-height="210mm" master-name="spmCommon">
         <fo:region-body/>
      </fo:simple-page-master>
   </fo:layout-master-set>
</fo:root>

もちろん機能的にはこれでも良いのですが、私にはイマイチに見えます.スタイル定義の中にロジックが入ってしまっているからです.あまりキレイではないですよね.でも今までこの手のコーディングスタイルのスタイルシートを山ほど見てきました.

これがXSLT 3.0になると次のように書くことができます.

    <xsl:param name="PRM_PAPER_SIZE" as="xs:string" static="yes" required="no" select="'A5'"/>
    <xsl:variable name="prmPaperSize" as="xs:string" static="yes" select="$PRM_PAPER_SIZE"/>
    
    <xsl:attribute-set name="atsPage" use-when="$prmPaperSize eq 'A4'">
        <xsl:attribute name="page-width">210mm</xsl:attribute>
        <xsl:attribute name="page-height">297mm</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="atsPage" use-when="$prmPaperSize eq 'A5'">
        <xsl:attribute name="page-width">148mm</xsl:attribute>
        <xsl:attribute name="page-height">210mm</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="atsPage" use-when="$prmPaperSize eq 'B4'">
        <xsl:attribute name="page-width">257mm</xsl:attribute>
        <xsl:attribute name="page-height">364mm</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="atsPage" use-when="$prmPaperSize eq 'B5'">
        <xsl:attribute name="page-width">182mm</xsl:attribute>
        <xsl:attribute name="page-height">257mm</xsl:attribute>
    </xsl:attribute-set>

    <xsl:template match="/">
        <fo:root>
            <fo:layout-master-set>
                <fo:simple-page-master master-name="spmCommon" xsl:use-attribute-sets="atsPage">
                    <fo:region-body/>
                </fo:simple-page-master>
            </fo:layout-master-set>
        </fo:root>
    </xsl:template>

違いは歴然でuse-when属性がコンパイル時に評価されて、true()になったものだけが採用されます.こっちの方がかっこいいです.何よりもスタイル(xsl:attribute-set)がきれいに書けるではありませんか!

でも実は致命的な弱点があります.それはこの例はサンプルで用紙サイズを"A5"にしていますが、XSLTの実行時に外部から別のパラメータ値を指定して動かしてもパラメータ値は"A5"から変わってくれないのです!!!何故か?それはstatic="yes"だからでしょう.スタイルシートコンパイル時に値が確定してしまうのです.ではstaticなxsl:paramってほとんどxsl:variableと変わりなくなってしまう気がしますよね??これって何か矛盾している気がします.

ではstaticなxsl:paramは外部リンケージを取れないのでしょうか?取れなければなんの御利益もありません.
いえなんとか取ることができます.それにはsystem-property()関数をつかうことです.先ほどのxsl:paramは例えば次のように書くことが出来ます.

    <xsl:param name="PRM_PAPER_SIZE" as="xs:string" static="yes" required="no" select="system-property('paper.size')"/>
    <xsl:variable name="prmPaperSize" as="xs:string" static="yes" select="$PRM_PAPER_SIZE"/>

こうしておいて、例えばoXygenだったらantのbuild.xmlを次のように作ります.あとはantの起動パラメータでpaper.sizeを指定してやれば外部から動的に用紙サイズを指定することが出来ます.

<project name="com.antennahouse.pdf5.build"  basedir="." default="transform">
    <property name="input.file" value="${basedir}/input.xml"/>
    <property name="output.file" value="${basedir}/output.xml"/>
    <property name="style.file" value="${basedir}/${style}"/>
    
    <target name="transform">
        <property name="stylesheet" value="${basedir}${file.separator}${xsl.file}"/>
        <property name="transformer.classname" value="net.sf.saxon.Transform"/>
        <java classname="${transformer.classname}" failonerror="true">
            <arg line='-t'/>
            <arg line='-o:"${output.file}"'/>
            <arg line='-s:"${input.file}"'/>
            <arg line='-xsl:"${style.file}"'/>
            <sysproperty key="paper.size" value="${paper.size}"/>
        </java>
    </target>
</project>

実際Saxonのstatic変数の評価はあたりまえですが厳格で、system-property()関数を使用したスタイルシートはoXygenのIDEではコンパイルエラーとなります.無理もありません.oXygen自体がJavaアプリケーションなのですが、このようなシステムプロパティをセットして起動されている訳ではないからです.結果的に'atsPage'が未定義になってしまうのです.次の画面では「XTSE0710: No attribute-set exists named atsPage」になっているのを見ることが出来ます.でもantからpaper.sizeプロパティをセットして起動してやればコンパイルエラーにはならず動いてくれます.

イメージ 1

use-whenにstatic変数が使用できるようになると例えばデバッグのコードも楽に書けるようになります.今まではたぶん

    <xsl:param name="prmDebug" as="xs:boolean" select="true()"/>

と書いて、次のようにコードを書いていたでしょう.

        <xsl:if test="$prmDebug">
            <xsl:message select="'$prmPaperSize=',$prmPaperSize"/>
        </xsl:if>

これはstaticなxsl:paramを使えば、

    <xsl:param name="prmDebug" as="xs:boolean" static="yes" select="true()"/>

    <xsl:message select="'$prmPaperSize=',$prmPaperSize" use-when="$prmDebug"/>

で済みます.いちいち<xsl:if>を書く手間がありません.

XSLT 3.0にはその他にも魅力的な機能がいっぱいです.なかなか使う機会がないのですが、oXygenを持っていて使わない手はないのでまたいろいろ試してみたいと思います.