🤔

大規模XSLTフレームワークでxsl:packageを使う試行

2023/08/14に公開

XSLT 3.0では、ルート要素に<xsl:stylesheet><xsl:transform>に加えて<xsl:package>が使えるようになりました。ただし、この<xsl:package>は先の2つとは異なる用途が想定されています。ライブラリ用途なのです。

本記事では、「XSLT3.0への道(31) パッケージを試してみる(その2)」の記事で提示されている「modeが複数packageを跨ることができないため、大規模なXSLTフレームワークで採用することは難しいだろう」という意見に対し、どうにかできないだろうか検討してみようという趣旨で試行を行います。

関連資料

前提:xsl:includexsl:import

xsl:package以前のXSLTでは単一XSLTしか使えなかったかというとそんなことはなく、xsl:includexsl:importという機構があります。両者の違いについて端的には次になります:

  • xsl:includeではその箇所に@hrefされた記述内容を挿入する
  • xsl:importでは@hrefされた記述内容(import元)を、xsl:importを記述したスタイルシートが上書きする

この違いは対象が呼び出し元と呼び出し先に存在するときに顕在化します。

  • xsl:includeで名前の被る名前付きテンプレートやfunctionはNGで、xsl:importでは上書きします。
  • xsl:includeではmatchテンプレートはpriorityと記述位置に従って優先され、xsl:importでは呼び出した側のものが優先されます。

他にxsl:apply-importsという厄介機構もありますが、ここでは割愛します。

違いはさておいて、これらの機構を活用することで複数のXSLTスタイルシートを束ねて構成する大規模なフレームワークが構築されてきました。一方で、野放図にimportやincludeを使ってスタイルシート群を拡張していくと、ある問題にあたります。
それはどのスタイルシートでどんなテンプレートがあるか分からない・適用順が不明瞭になるというものです。特に、importで呼び出す場合、呼び出し元でpriorityを設定しても呼び出し側には関係ないなどがあり、開発が混沌としやすい(DITA-OTのpdf2プラグインでは、拡張の際にmain処理のshellをimportさせるのではなく、個別のスタイルシートを別個に呼び出す形を推奨しているようだ。アンテナハウスのPDF5-MLプラグインの拡張では、includeで呼び出してpriorityを活かすように推奨している、のかな?)

xsl:packageによるmodeの制約

xsl:packageが解決するのは、「スコープ・実装の明確化」となります。そのためにtemplate適用に於ける完結性を求めるのですが、これが結構パラダイム転換が難しい。

  • 今まで:他のスタイルシートがどんなテンプレート書いてあるか分からないけどapply-templatesすればまとめて動くから安心だぜ
  • xsl:package:他のライブラリがどんなmatchテンプレート書いてあるか分からないけれどお前からは呼べないから安心だぜ

記述上は<xsl:mode>で明示したmodeが、そのpackage内でapply-templatesされるテンプレート群が完結していることを求められます。

「XSLT3.0への道(31) パッケージを試してみる(その2)」にある話を簡易的に示します。

処理対象XML
<topic class="+ topic/topic" xml:lang="ja">
    <title class="+ topic/title ">SAMPLE TITLE</title>
    <body class="+ topic/body">
        <p class="+ topic/p ">これは仮のトピックです。</p>
        <p  class="+ topic/p ">パラグラフの中では<ph class="+ topic/ph ">スパン</ph><b class="- topic/ph hi-d/b ">ボールド</b>が使えます。</p>
    </body>
</topic>
original.xsl
<!-- original.xsl -->

<xsl:template match="p"  mode="main">
  <fo:block>
    <xsl:apply-templates mode="main"/>
  </fo:block>
</xsl:template>

<xsl:template match="ph"  mode="main">
<!-- 拡張前の処理 -->
</xsl:template>
extends.xsl
<xsl:template match="ph"  mode="main">
<!-- 拡張後の処理 -->
</xsl:template>

これが動くのは、original.xslとextends.xslで同じmodeについてのtemplateを記述できるからです。xsl:packageを使う場合、これはエラーになる、という話です。

後で使うスタイルシートと出力XMLの想定が違うのは違う日に書いているから。

解決方法の模索

これを、モジュラブルなスキーマ記述に近い構造に直すことで解決できないかと考えました。モジュラブルなスキーマ記述というのは、まさにDITAのそれを意図しています。

実装試行

メインの処理スタイルシートから読み込まれるサブパッケージを作成し、メインで良い感じに取り出して使うようにします。

試行にあたっての制約は次:

  • それぞれのサブパッケージにしか無いマッチtemplateを用意する
  • 適用が競合するmatchテンプレートで正しいものを適用させる
  • それぞれのサブパッケージで共通で読み込まれるサブサブパッケージがある

入力ファイル

先に例示で出したものを流用。DITAトピックっぽい何かです。簡単のため、classを手打ちで入れています。サブパッケージの適正な適用について扱うために'topic/ph hi-d/b'のclassが使いたかった。

sample.xml
<topic class="+ topic/topic" xml:lang="ja">
    <title class="+ topic/title ">SAMPLE TITLE</title>
    <body class="+ topic/body">
        <p class="+ topic/p ">これは仮のトピックです。</p>
        <p  class="+ topic/p ">パラグラフの中では<ph class="+ topic/ph ">スパン</ph><b class="- topic/ph hi-d/b ">ボールド</b>が使えます。</p>
    </body>
</topic>

想定失敗ケース

<body>内、<p><ph>についてはsub1サブパッケージが、<b>についてはsub2サブパッケージで定義されたtemplateが適用されるものとします。
普通に書こうとすると、次のところで困るわけです。

sub1.xsl(fail)
<xsl:template match="p" mode="sub1">
<p><xsl:apply-templates mode="sub1"/></p>
</xsl:template>

<b>はsub2サブパッケージ内でtemplateが書かれるワケですから、sub1で定義されるmodeであるsub1にはそのままでは含められないのです。

sub2.xsl(fail)
<xsl:template match="b" mode="sub2"> <!-- mode="sub1"はsub1.xslパッケージで使っているのでエラー、ではmode="sub2"はsub1モードからどうやって呼ぶ? -->
  <span style="font-weight:bolder"><xsl:apply-templates mode="#current"/></span>
</xsl:template>

共通で読み込まれるパッケージ

common.xsl
<xsl:package xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="3.0"
    package-version="1.0"
    name="urn:pkg:common">
    <xsl:template name="common" visibility="public">
        <xsl:comment>do nothing</xsl:comment>
    </xsl:template>
</xsl:package>

実際に何か処理するようにしてもよいのですが、今回はメイン処理で上書きした場合の処理をみるために何もしない名前付きtemplateを用意しました。

  • <xsl:package>の記述について、ここで簡単に。XSLTスタイルシートのルートとして、XSLTのXML名前空間宣言と@version指定が必要。<xsl:package>を使う場合は3.0以降ですね。
  • @nameにパッケージ名・@package-versionにパッケージのバージョン情報を指定。パッケージ呼び出しの際にこの情報が参照されます。パッケージ名はURI。

複数のファイルで同じパッケージ名でライブラリを構成したい、という場合は考えられていないはず。W3Cの例にあるのが今までのスタイルシートをまとめてパッケージの記述に変換するような例ですし、配布用形式だと考えて作業用とは別に考えた方が良いのかも。あまりちゃんと調べていません。

<xsl:template>@visibility="public"という記述があります。これは<xsl:packagel>によるパッケージ機構において、外部から呼び出し可能か、書き換え可能かなどを指定するプロパティです。templateの既定は@visibility="private"なので、外から呼べないようになっています。

<xsl:expose>については今回使わずに動いたのでとりあえず割愛。

サブパッケージ1,2

sub1.xsl
<xsl:package xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    package-version="1.0"
    name="urn:pkg:sub1"
    xmlns="http://www.w3.org/1999/xhtml"
    exclude-result-prefixes="#all">
    <xsl:use-package name="urn:pkg:common" package-version="1.0">
        <xsl:accept component="template" visibility="public" names="common"/>
    </xsl:use-package>
    
    <xsl:mode name="sub1.inbody" on-no-match="shallow-skip" visibility="public"/>
    <xsl:mode name="sub1.inline" on-no-match="shallow-skip" visibility="public"/>
    
    <xsl:template name="sub1.inbody" visibility="public">
        <xsl:comment>sub1 in-body</xsl:comment>
        <xsl:apply-templates select="." mode="sub1.inbody"/>
    </xsl:template>
    
    <xsl:template name="sub1.inline" visibility="public">
        <xsl:comment>sub1 in-line</xsl:comment>
        <xsl:apply-templates select="." mode="sub1.inline"/>
    </xsl:template>
    
    <xsl:template name="p.content" visibility="abstract">
        <xsl:context-item use="required" />
    </xsl:template>
    <xsl:template name="sub1.span.content" visibility="abstract">
        <xsl:context-item use="required" />
    </xsl:template>
    
    <xsl:template match="*[@class => contains-token('topic/p')]" mode="sub1.inbody">
        <p>
            <xsl:call-template name="common"/>
            <xsl:comment>sub1 p</xsl:comment>
            <xsl:call-template name="p.content"/>
        </p>
    </xsl:template>
    <xsl:template match="*[@class => contains-token('topic/ph')]" mode="sub1.inline">
        <span>
            <xsl:call-template name="common"/><xsl:call-template name="sub1.span.content"></xsl:call-template></span>
    </xsl:template>
</xsl:package>

先程用意したcommon.xslパッケージを<xsl:use-package>呼び出しています。パッケージ名から実体ファイルの場所が分かりませんが、この紐付けは処理系側が行います。Saxonの場合はconfigファイルに設定用の構造があります。<xsl:accept>でパッケージ内から使う機構を指定します。

このパッケージのポイントは3つほど。

  • <p class="+ topic/p >"を処理するためのmode="sub1.inbody"と、<p>内の<ph>`を処理するためのmode="sub1.inline"がある
  • 処理テンプレート内で<xsl:apply-templates />ではなく<xsl:call-template name="p.content/>を使っている

match="*[@class => contains-token('topic/p')]"は、入力ファイルの<ph class="+ topic/ph"><b class="- topic/ph hi-d/b">に適用可能なtemplateです。

<xsl:apply-templates/>は基本的にchild::*を対象にしますが、上のように現在のコンテキストを対象にすることもできます。無限ループになりがちなのでこれを使うときのtemplate順はきちんと設計しましょう。ちなみに私は今回の作業中に2桁回は無限ループをやらかしました。

sub2.xsl
<xsl:package xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    package-version="1.0"
    name="urn:pkg:sub2"
    xmlns="http://www.w3.org/1999/xhtml"
    exclude-result-prefixes="#all">
    <xsl:use-package name="urn:pkg:common" package-version="1.0">
        <xsl:accept component="template" visibility="public" names="common"/>
    </xsl:use-package>
    <xsl:mode name="sub2.inline" on-no-match="shallow-skip" visibility="public"/>
    
    <xsl:template name="sub2.inline" visibility="public">
        <xsl:apply-templates select="." mode="sub2.inline"/>
    </xsl:template>
    
    <xsl:template name="sub2.span.content" visibility="public">
        <xsl:context-item use="required" />
        <xsl:apply-templates mode="sub2.inline"></xsl:apply-templates>
    </xsl:template>
    
    <xsl:template match="*[@class => contains-token('hi-d/b')]" mode="sub2.inline">
        <xsl:comment>sub2 b</xsl:comment>
        <span style="font-weight:bolder">
            <xsl:call-template name="common"/>
            <xsl:call-template name="sub2.span.content"/>               
        </span>
    </xsl:template>    
</xsl:package>

<b class="- topic/ph hi-d/b">を処理するtemplateを持つpackageです。

main処理

main.xsl
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns="http://www.w3.org/1999/xhtml"
    exclude-result-prefixes="#all">
    <xsl:output method="xhtml" html-version="5"
    omit-xml-declaration="yes"
    include-content-type="no"/>
    <xsl:mode name="head" on-no-match="shallow-skip"/>

    <xsl:mode name="main" on-no-match="shallow-skip"/>
    <xsl:use-package name="urn:pkg:sub1" package-version="1.0">
        <xsl:accept visibility="public" component="template" names="sub1.inbody"/>
        <xsl:accept visibility="public" component="template" names="sub1.inline"/>
        <xsl:accept visibility="public" component="mode" names="sub1.inbody"/>
        <xsl:accept visibility="public" component="mode" names="sub1.inline"/>
        
        <xsl:override>
            <xsl:template name="p.content">
                <xsl:context-item use="required"/>
                <xsl:apply-templates  mode="main"/>
            </xsl:template>
            
            <xsl:template name="sub1.span.content">
                <xsl:context-item use="required"/>
                <xsl:apply-templates  mode="main"/>
            </xsl:template>
            
        </xsl:override>
    </xsl:use-package>
    <xsl:use-package name="urn:pkg:sub2" package-version="1.0">
        <xsl:accept visibility="public" component="template" names="sub2.inline"/>
        <xsl:accept visibility="public" component="mode" names="sub2.inline"/>
        <xsl:override>
            <xsl:template name="sub2.span.content">
                <xsl:context-item use="required"/>
                <xsl:apply-templates  mode="main"/>
            </xsl:template>
            
        </xsl:override>
    </xsl:use-package>
    <xsl:use-package name="urn:pkg:common" package-version="1.0">
        <xsl:override>
            <xsl:template name="common" visibility="public">
                <xsl:attribute name="xml:lang"
                    select="
                    let $lang := if (exists(/*/@xml:lang))
                                  then (/*/@xml:lang) else ('en') 
                    return
                    if(exists(ancestor-or-self::*/@xml:lang)) 
                            then (
                                (ancestor-or-self::*/@xml:lang)[1]
                                ) 
                            else ($lang)"></xsl:attribute>
            </xsl:template>
        </xsl:override>
    </xsl:use-package>
    <xsl:template match="/">
        <html >
            <xsl:call-template name="common"/>
            <xsl:call-template name="head"/>
            <xsl:call-template name="body"/>
        </html>
    </xsl:template>
    <xsl:template name="head">
        <head>
            <meta charset="UTF-8"/>
            <xsl:apply-templates mode="head"/>
            <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
        </head>
    </xsl:template>
    <xsl:template match="title" mode="head">
        <title>
            <xsl:value-of select="."/>
        </title>
    </xsl:template>
    <xsl:template name="body">
        <xsl:apply-templates select=". except(title)" mode="main"/>
    </xsl:template>
    <xsl:template match="*[@class => contains-token('topic/body')]" mode="main">
        <body>
            <xsl:apply-templates mode="#current"/>
        </body>
    </xsl:template>

    <xsl:template match="*[@class => contains-token('topic/p')]" mode="main">
        <xsl:call-template name="sub1.inbody"/>
    </xsl:template>

    <xsl:template match="*[@class => contains-token('topic/ph')]" mode="main">
        <xsl:choose>
            <xsl:when test="@class => contains-token('hi-d/b')">
                <xsl:call-template name="sub2.inline"/>        
            </xsl:when>
            <xsl:otherwise>
                <xsl:call-template name="sub1.inline"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="text()" mode="main">
        <xsl:value-of select="."/>
    </xsl:template>
    
</xsl:stylesheet>	

headのmodeやテンプレートは出力の体裁を調えるためのものなので説明を割愛。注目は<xsl:override>ですね。ここで名前付きテンプレートの処理内容を書き換えます。ここにmainスタイルシートのmodeでapply-templatesをかますことで、非パッケージと類似の挙動を実現します。

<xsl:call-template name="sub1.inbody"/>match="*[@class => contains-token('topic/p')]"の中で呼んでいるのはタイミングが微妙にみえますね。実際微妙です。sub1パッケージでbody処理用のテンプレート実装をサボった結果とでもいいましょうか。printfデバッグこと<xsl:comment>で出力されたHTMLのコメント部分を見ると溜め息が出ます。

ちゃっかりmainスタイルシート内でcommonテンプレートを上書きしています。ここで上書きした処理がsub1,sub2内で使われている処理内容を上書きしていればとりあえず今回の試みは成功といっていいでしょう。ちなみに上書きした処理では普通にカスケードされるxml:langを態々コピーする処理になっています。

Saxon12でpackageを使うためのconfig

config.xml
<configuration xmlns="http://saxon.sf.net/ns/configuration"
    edition="HE"
    label="sample">
    <xsltPackages>
        <package name="urn:pkg:sub1" version="1.0" sourceLocation="sub1.xsl" />
        <package name="urn:pkg:sub2" version="1.0" sourceLocation="sub2.xsl" />
        <package name="urn:pkg:common" version="1.0" sourceLocation="common.xsl"/>
    </xsltPackages>  
</configuration>

@nameはXSLT中で使われている名前、@sourceLocationはXSLTスタイルシートの物理的な位置ですね。XML Catalogに慣れている方だと馴染みやすいでしょう。逆に何故XML Catalogじゃダメなのかとも思いますが、バージョン情報と、実はパッケージごとにparamを渡したりできるといった違いがあります。

match="*[@class => contains-token('topic/ph')]"では、今までの<xsl:apply-templates>は何だったのかと思うような<xsl:choose>分岐があります。これは別の解としてはmode="main.inline"を作って複数のtemplateにpriorityを設定する方法も考えられます。が、行数が増えるのでどちらがいいかは微妙です。

実行

java -jar C:\Saxon\saxon-he-12.3.jar -xsl:.\main.xsl -s:.\sample.xml -config:.\config.xml -o:out.html
out.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"  xml:lang="ja">
   <head>
      <meta charset="UTF-8"/>
      <title>SAMPLE TITLE</title>
      <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
   </head>
   <body>
      <!--sub1 in-body-->
      <p xml:lang="ja"><!--sub1 p-->これは仮のトピックです。</p>
      <!--sub1 in-body-->
      <p xml:lang="ja"><!--sub1 p-->パラグラフの中では<!--sub1 in-line--><span xml:lang="ja">スパン</span><!--sub2 b--><span style="font-weight:bolder" xml:lang="ja">ボールド</span>が使えます。</p>
      </body>
</html>

今回の試行の結論

大規模フレームワークの構成に使えなくはない。が、packageの思想からすると正道ではないかもしれない。

The thinking is that if package A uses packages B and C, then B and C were
developed and tested independently and have no knowledge of each other or
dependence on each other. When a template rule in B calls apply-templates,
it's not expecting a template rule in C to be invoked, because it was written
with no knowledge of C.
(「XSLT3.0への道(31) パッケージを試してみる(その2)」記事内からのpackageについてのMichael Kay博士の意見の引用[2]

なぜって、パッケージの中身は知っていなきゃ制御できそうにないですからね今回の書き方。サブパッケージなどから構成する前提ということもありますが、巨大フレームワークでuse-packageはどれくらい長くなるのやら。

結論、よくわかりませんでした。いかがでしたか?

脚注
  1. http://www.functx.com/ ↩︎

  2. 孫引きについてはご寛恕願いたく。 ↩︎

Discussion